From 05b07eb8f26d3cea08e7153fb465dab5f07357ff Mon Sep 17 00:00:00 2001 From: Ansgar Becker Date: Wed, 5 May 2010 21:39:15 +0000 Subject: [PATCH] Implement editing capabilities into TMySQLQuery, and make query results editable by using all the same events as in DataGrid. Most probably some bugs to fix now. * Fixes issue #723 * Fixes issue #873 --- source/const.inc | 1 + source/grideditlinks.pas | 2 +- source/helpers.pas | 230 ++----- source/main.dfm | 57 +- source/main.pas | 1212 ++++++++++++----------------------- source/mysql_connection.pas | 918 +++++++++++++++++++++++--- source/table_editor.pas | 6 + 7 files changed, 1353 insertions(+), 1073 deletions(-) diff --git a/source/const.inc b/source/const.inc index 153d4dd3..9a97670a 100644 --- a/source/const.inc +++ b/source/const.inc @@ -318,6 +318,7 @@ const SContainsNulCharFile = 'This file contains NUL characters. They have been converted to ASCII spaces (SP).'; SContainsNulCharGrid = 'This cell contains NUL characters. They have been converted to ASCII spaces (SP). Press ESC to cancel editing.'; SUnhandledTreeLevel = 'Unhandled tree node level.'; + MSG_NOGRIDEDITING = 'Selected columns don''t contain a sufficient set of key columns to allow editing. Please select primary or unique key columns, or just all columns.'; PKEY = 'PRIMARY'; KEY = 'KEY'; diff --git a/source/grideditlinks.pas b/source/grideditlinks.pas index c948665e..e1d4082f 100644 --- a/source/grideditlinks.pas +++ b/source/grideditlinks.pas @@ -7,7 +7,7 @@ interface uses Windows, Forms, Graphics, Messages, VirtualTrees, ComCtrls, SysUtils, Classes, StdCtrls, ExtCtrls, CheckLst, Controls, Types, Dialogs, Mask, DateUtils, Math, - mysql_structures, helpers, texteditor, bineditor; + mysql_connection, mysql_structures, helpers, texteditor, bineditor; type // Radio buttons and checkboxes which do not pass key to their parent control diff --git a/source/helpers.pas b/source/helpers.pas index db70ab71..03d87ba1 100644 --- a/source/helpers.pas +++ b/source/helpers.pas @@ -31,46 +31,6 @@ type TFileCharset = (fcsAnsi, fcsUnicode, fcsUnicodeSwapped, fcsUtf8); - // Structures for result grids, mapped from a TMySQLQuery to some handy VirtualTree structure - TGridCell = record - Text: String; - NewText: String; // Used to create UPDATE clauses with needed columns - IsNull: Boolean; - NewIsNull: Boolean; - Modified: Boolean; - end; - PGridCell = ^TGridCell; - TGridColumn = record - Name: String; - Datatype: TDatatypeIndex; // @see mysql_structures.pas - DatatypeCat: TDatatypeCategoryIndex; - MaxLength: Cardinal; - IsPriPart: Boolean; - ValueList: TStringList; - end; - PGridColumn = ^TGridColumn; - TGridColumns = Array of TGridColumn; - // Delphi reminder, from Rudy Velthuis (usenet): - // Records and arrays are passed as pointers, but in the preamble of the - // called function the pointed-to data is copied onto the stack. So either - // use "const x: TBlah" to pass by reference, or create a pointer type. - // Objects are passed as pointers. - // (Orthogonal to this, when specifying "var", a pointer (possibly to a pointer) - // is passed, allowing the called function to alter the caller's reference.) - PGridColumns = ^TGridColumns; - TGridRowState = (grsDefault, grsDeleted, grsModified, grsInserted); - TGridRow = packed record - Cells: Array of TGridCell; - State: TGridRowState; - HasFullData: Boolean; - end; - PGridRow = ^TGridRow; - TGridRows = Array of TGridRow; - TGridResult = class(TObject) - Rows: TGridRows; - Columns: TGridColumns; - end; - TOrderCol = class(TObject) ColumnName: String; SortDirection: Byte; @@ -86,57 +46,6 @@ type SubParts : TStringList; end; - // General purpose editing status flag - TEditingStatus = (esUntouched, esModified, esDeleted, esAddedUntouched, esAddedModified, esAddedDeleted); - - TColumnDefaultType = (cdtNothing, cdtText, cdtTextUpdateTS, cdtNull, cdtNullUpdateTS, cdtCurTS, cdtCurTSUpdateTS, cdtAutoInc); - - // Column object, many of them in a TObjectList - TTableColumn = class(TObject) - Name, OldName: String; - DataType: TDatatype; - LengthSet: String; - Unsigned, AllowNull: Boolean; - DefaultType: TColumnDefaultType; - DefaultText: String; - Comment, Collation: String; - FStatus: TEditingStatus; - constructor Create; - destructor Destroy; override; - private - procedure SetStatus(Value: TEditingStatus); - public - property Status: TEditingStatus read FStatus write SetStatus; - end; - PTableColumn = ^TTableColumn; - TTableColumnList = TObjectList; - - TTableKey = class(TObject) - Name, OldName: String; - IndexType, Algorithm: String; - Columns, SubParts: TStringList; - Modified, Added: Boolean; - constructor Create; - destructor Destroy; override; - procedure Modification(Sender: TObject); - end; - TTableKeyList = TObjectList; - - // Helper object to manage foreign keys in a TObjectList - TForeignKey = class(TObject) - KeyName, ReferenceTable, OnUpdate, OnDelete: String; - Columns, ForeignColumns: TStringList; - Modified, Added, KeyNameWasCustomized: Boolean; - constructor Create; - destructor Destroy; override; - end; - TForeignKeyList = TObjectList; - - TRoutineParam = class(TObject) - Name, Context, Datatype: String; - end; - TRoutineParamList = TObjectList; - TDBObjectEditor = class(TFrame) private FModified: Boolean; @@ -831,9 +740,10 @@ var i, MaxSize: Integer; tmp, Data, Generator: String; Node: PVirtualNode; - GridData: TGridResult; + GridData: TMySQLQuery; SelectionOnly: Boolean; NodeCount: Cardinal; + RowNum: PCardinal; begin // Only process selected nodes for "Copy as ..." actions SelectionOnly := S is TMemoryStream; @@ -861,7 +771,7 @@ begin ' th, td {vertical-align: top; font-family: "'+Grid.Font.Name+'"; font-size: '+IntToStr(Grid.Font.Size)+'pt; padding: '+IntToStr(Grid.TextMargin-1)+'px; }' + CRLF + ' table, td {border: 1px solid silver;}' + CRLF + ' table {border-collapse: collapse;}' + CRLF; - for i:=0 to Length(GridData.Columns) - 1 do begin + for i:=0 to Grid.Header.Columns.Count-1 do begin // Skip hidden key columns. if not (coVisible in Grid.Header.Columns[i].Options) then Continue; @@ -885,13 +795,12 @@ begin ' ' + CRLF + ' ' + CRLF + ' ' + CRLF; - for i:=0 to Length(GridData.Columns) - 1 do begin + for i:=0 to Grid.Header.Columns.Count-1 do begin // Skip hidden key columns. if not (coVisible in Grid.Header.Columns[i].Options) then Continue; // Add header item. - Data := GridData.Columns[i].Name; - tmp := tmp + ' ' + CRLF; + tmp := tmp + ' ' + CRLF; end; tmp := tmp + ' ' + CRLF + @@ -899,22 +808,23 @@ begin ' ' + CRLF; StreamWrite(S, tmp); - Grid.Visible := false; if SelectionOnly then Node := Grid.GetFirstSelected else Node := Grid.GetFirst; while Assigned(Node) do begin // Update status once in a while. if (Node.Index+1) mod 100 = 0 then ExportStatusMsg(Node, NodeCount, S.Size); + RowNum := Grid.GetNodeData(Node); + GridData.RecNo := RowNum^; tmp := ' ' + CRLF; - for i:=0 to Length(GridData.Columns) - 1 do begin + for i:=0 to Grid.Header.Columns.Count-1 do begin // Skip hidden key columns if not (coVisible in Grid.Header.Columns[i].Options) then Continue; Data := Grid.Text[Node, i]; // Handle nulls. - if GridData.Rows[Node.Index].Cells[i].IsNull then Data := TEXT_NULL; + if GridData.IsNull(i) then Data := TEXT_NULL; // Unformat numeric values - if (GridData.Columns[i].DatatypeCat in [dtcInteger, dtcReal]) and (not Mainform.prefExportLocaleNumbers) then + if (GridData.DataType(i).Category in [dtcInteger, dtcReal]) and (not Mainform.prefExportLocaleNumbers) then Data := UnformatNumber(Data); // Escape HTML control characters in data. Data := htmlentities(Data); @@ -941,7 +851,6 @@ begin ' ' + CRLF + '' + CRLF; StreamWrite(S, tmp); - Grid.Visible := true; Mainform.ProgressBarStatus.Visible := False; Mainform.ShowStatusMsg(STATUS_MSG_READY); end; @@ -960,9 +869,10 @@ var i, MaxSize: Integer; tmp, Data: String; Node: PVirtualNode; - GridData: TGridResult; + GridData: TMySQLQuery; SelectionOnly: Boolean; NodeCount: Cardinal; + RowNum: PCardinal; begin // Only process selected nodes for "Copy as ..." actions SelectionOnly := S is TMemoryStream; @@ -987,9 +897,9 @@ begin // Skip hidden key columns if not (coVisible in Grid.Header.Columns[i].Options) then Continue; - Data := GridData.Columns[i].Name; + Data := Grid.Header.Columns[i].Text; // Alter column name in header if data is not raw. - if (GridData.Columns[i].DatatypeCat in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then + if (GridData.DataType(i).Category in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then Data := 'HEX(' + Data + ')'; // Add header item. if tmp <> '' then tmp := tmp + Separator; @@ -998,13 +908,13 @@ begin tmp := tmp + Terminator; StreamWrite(S, tmp); - Grid.Visible := false; - // Data: if SelectionOnly then Node := Grid.GetFirstSelected else Node := Grid.GetFirst; while Assigned(Node) do begin if (Node.Index+1) mod 100 = 0 then ExportStatusMsg(Node, NodeCount, S.Size); + RowNum := Grid.GetNodeData(Node); + GridData.RecNo := RowNum^; tmp := ''; for i:=0 to Grid.Header.Columns.Count-1 do begin // Skip hidden key columns @@ -1012,15 +922,15 @@ begin Continue; Data := Grid.Text[Node, i]; // Remove 0x. - if (GridData.Columns[i].DatatypeCat in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then + if (GridData.DataType(i).Category in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then Delete(Data, 1, 2); // Unformat numeric values - if (GridData.Columns[i].DatatypeCat in [dtcInteger, dtcReal]) and (not Mainform.prefExportLocaleNumbers) then + if (GridData.DataType(i).Category in [dtcInteger, dtcReal]) and (not Mainform.prefExportLocaleNumbers) then Data := UnformatNumber(Data); // Escape encloser characters inside data per de-facto CSV. Data := StringReplace(Data, Encloser, Encloser + Encloser, [rfReplaceAll]); // Special handling for NULL (MySQL-ism, not de-facto CSV: unquote value) - if GridData.Rows[Node.Index].Cells[i].IsNull then Data := 'NULL' + if GridData.IsNull(i) then Data := 'NULL' else Data := Encloser + Data + Encloser; // Add cell. if tmp <> '' then tmp := tmp + Separator; @@ -1036,7 +946,6 @@ begin break; end; end; - Grid.Visible := true; Mainform.ProgressBarStatus.Visible := False; Mainform.ShowStatusMsg(STATUS_MSG_READY); end; @@ -1053,9 +962,10 @@ var i, MaxSize: Integer; tmp, Data: String; Node: PVirtualNode; - GridData: TGridResult; + GridData: TMySQLQuery; SelectionOnly: Boolean; NodeCount: Cardinal; + RowNum: PCardinal; begin // Only process selected nodes for "Copy as ..." actions SelectionOnly := S is TMemoryStream; @@ -1074,12 +984,12 @@ begin '
' + Data + '' + Grid.Header.Columns[i].Text + '
' + CRLF; StreamWrite(S, tmp); - // Avoid reloading discarded data before the end. - Grid.Visible := false; if SelectionOnly then Node := Grid.GetFirstSelected else Node := Grid.GetFirst; while Assigned(Node) do begin if (Node.Index+1) mod 100 = 0 then ExportStatusMsg(Node, NodeCount, S.Size); + RowNum := Grid.GetNodeData(Node); + GridData.RecNo := RowNum^; tmp := #9'' + CRLF; for i:=0 to Grid.Header.Columns.Count-1 do begin // Skip hidden key columns @@ -1087,17 +997,17 @@ begin Continue; // Print cell start tag. tmp := tmp + #9#9'<' + Grid.Header.Columns[i].Text; - if GridData.Rows[Node.Index].Cells[i].IsNull then tmp := tmp + ' isnull="true" />' + CRLF + if GridData.IsNull(i) then tmp := tmp + ' isnull="true" />' + CRLF else begin - if (GridData.Columns[i].DatatypeCat in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then + if (GridData.DataType(i).Category in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then tmp := tmp + ' format="hex"'; tmp := tmp + '>'; Data := Grid.Text[Node, i]; // Remove 0x. - if (GridData.Columns[i].DatatypeCat in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then + if (GridData.DataType(i).Category in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then Delete(Data, 1, 2); // Unformat numeric values - if (GridData.Columns[i].DatatypeCat in [dtcInteger, dtcReal]) and (not Mainform.prefExportLocaleNumbers) then + if (GridData.DataType(i).Category in [dtcInteger, dtcReal]) and (not Mainform.prefExportLocaleNumbers) then Data := UnformatNumber(Data); // Escape XML control characters in data. Data := htmlentities(Data); @@ -1118,7 +1028,6 @@ begin // footer: tmp := '
' + CRLF; StreamWrite(S, tmp); - Grid.Visible := true; Mainform.ProgressBarStatus.Visible := False; Mainform.ShowStatusMsg(STATUS_MSG_READY); end; @@ -1134,9 +1043,10 @@ var i, MaxSize: Integer; tmp, Data: String; Node: PVirtualNode; - GridData: TGridResult; + GridData: TMySQLQuery; SelectionOnly: Boolean; NodeCount: Cardinal; + RowNum: PCardinal; begin // Only process selected nodes for "Copy as ..." actions SelectionOnly := S is TMemoryStream; @@ -1151,12 +1061,13 @@ begin else NodeCount := Grid.RootNodeCount; EnableProgressBar(NodeCount); - // Avoid reloading discarded data before the end. - Grid.Visible := false; + if SelectionOnly then Node := Grid.GetFirstSelected else Node := Grid.GetFirst; while Assigned(Node) do begin if (Node.Index+1) mod 100 = 0 then - ExportStatusMsg(Node, NodeCount, S.Size); + ExportStatusMsg(Node, NodeCount, S.Size); + RowNum := Grid.GetNodeData(Node); + GridData.RecNo := RowNum^; tmp := 'INSERT INTO '+Mainform.Mask(Tablename)+' ('; for i:=0 to Grid.Header.Columns.Count-1 do begin // Skip hidden key columns @@ -1170,15 +1081,15 @@ begin // Skip hidden key columns if not (coVisible in Grid.Header.Columns[i].Options) then Continue; - if GridData.Rows[Node.Index].Cells[i].IsNull then + if GridData.IsNull(i) then tmp := tmp + 'NULL' else begin Data := Grid.Text[Node, i]; // Remove 0x. - if (GridData.Columns[i].DatatypeCat in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then + if (GridData.DataType(i).Category in [dtcBinary, dtcSpatial]) and (not Mainform.actBlobAsText.Checked) then Delete(Data, 1, 2); // Unformat numeric values - if GridData.Columns[i].DatatypeCat in [dtcInteger, dtcReal] then + if GridData.DataType(i).Category in [dtcInteger, dtcReal] then tmp := tmp + UnformatNumber(Data) else tmp := tmp + esc(Data); @@ -1199,7 +1110,6 @@ begin // footer: tmp := CRLF; StreamWrite(S, tmp); - Grid.Visible := true; Mainform.ProgressBarStatus.Visible := False; Mainform.ShowStatusMsg(STATUS_MSG_READY); end; @@ -3194,74 +3104,6 @@ end; -{ *** TTableColumn } - -constructor TTableColumn.Create; -begin - inherited Create; -end; - -destructor TTableColumn.Destroy; -begin - inherited Destroy; -end; - -procedure TTableColumn.SetStatus(Value: TEditingStatus); -begin - // Set editing flag and enable "Save" button - if (FStatus in [esAddedUntouched, esAddedModified]) and (Value = esModified) then - Value := esAddedModified - else if (FStatus in [esAddedUntouched, esAddedModified]) and (Value = esDeleted) then - Value := esAddedDeleted; - FStatus := Value; - if Value <> esUntouched then - TfrmTableEditor(Mainform.ActiveObjectEditor).Modification(Self); -end; - - - -{ *** TTableKey } - -constructor TTableKey.Create; -begin - inherited Create; - Columns := TStringList.Create; - SubParts := TStringList.Create; - Columns.OnChange := Modification; - Subparts.OnChange := Modification; -end; - -destructor TTableKey.Destroy; -begin - FreeAndNil(Columns); - FreeAndNil(SubParts); - inherited Destroy; -end; - -procedure TTableKey.Modification(Sender: TObject); -begin - if not Added then - Modified := True; -end; - - -{ *** TForeignKey } - -constructor TForeignKey.Create; -begin - inherited Create; - Columns := TStringList.Create; - ForeignColumns := TStringList.Create; -end; - -destructor TForeignKey.Destroy; -begin - FreeAndNil(Columns); - FreeAndNil(ForeignColumns); - inherited Destroy; -end; - - { *** TDBObjectEditor } constructor TDBObjectEditor.Create(AOwner: TComponent); diff --git a/source/main.dfm b/source/main.dfm index 5c2251ce..c251d88d 100644 --- a/source/main.dfm +++ b/source/main.dfm @@ -1307,25 +1307,25 @@ object MainForm: TMainForm TreeOptions.PaintOptions = [toShowButtons, toShowDropmark, toShowHorzGridLines, toShowVertGridLines, toThemeAware, toUseBlendedImages, toAlwaysHideSelection] TreeOptions.SelectionOptions = [toExtendedFocus, toFullRowSelect, toMultiSelect, toRightClickSelect] WantTabs = True - OnAfterCellPaint = DataGridAfterCellPaint + OnAfterCellPaint = AnyGridAfterCellPaint OnBeforeCellPaint = AnyGridBeforeCellPaint OnBeforePaint = DataGridBeforePaint - OnChange = DataGridChange - OnCreateEditor = DataGridCreateEditor - OnEditCancelled = DataGridEditCancelled - OnEdited = DataGridEdited - OnEditing = DataGridEditing + OnCreateEditor = AnyGridCreateEditor + OnEditCancelled = AnyGridEditCancelled + OnEdited = AnyGridEdited + OnEditing = AnyGridEditing OnEnter = ValidateControls OnExit = ValidateControls - OnFocusChanged = DataGridFocusChanged - OnFocusChanging = DataGridFocusChanging + OnFocusChanged = AnyGridFocusChanged + OnFocusChanging = AnyGridFocusChanging OnGetText = AnyGridGetText OnPaintText = AnyGridPaintText + OnGetNodeDataSize = AnyGridGetNodeDataSize OnHeaderClick = DataGridHeaderClick OnInitNode = AnyGridInitNode OnKeyDown = AnyGridKeyDown - OnMouseUp = DataGridMouseUp - OnNewText = DataGridNewText + OnMouseUp = AnyGridMouseUp + OnNewText = AnyGridNewText Columns = <> end end @@ -1505,8 +1505,9 @@ object MainForm: TMainForm Header.AutoSizeIndex = -1 Header.DefaultHeight = 17 Header.Height = 20 + Header.Images = ImageListMain Header.MainColumn = -1 - Header.Options = [hoColumnResize, hoDblClickResize, hoDrag, hoShowHint] + Header.Options = [hoColumnResize, hoDblClickResize, hoDrag, hoHotTrack, hoShowHint, hoShowImages, hoShowSortGlyphs] Header.ParentFont = True IncrementalSearch = isAll LineStyle = lsSolid @@ -1517,12 +1518,24 @@ object MainForm: TMainForm TreeOptions.PaintOptions = [toShowButtons, toShowDropmark, toShowHorzGridLines, toShowVertGridLines, toThemeAware, toUseBlendedImages, toAlwaysHideSelection] TreeOptions.SelectionOptions = [toExtendedFocus, toMultiSelect, toRightClickSelect] WantTabs = True + OnAfterCellPaint = AnyGridAfterCellPaint + OnAfterPaint = vstAfterPaint OnBeforeCellPaint = AnyGridBeforeCellPaint - OnFocusChanged = QueryGridFocusChanged + OnCompareNodes = vstCompareNodes + OnCreateEditor = AnyGridCreateEditor + OnEditCancelled = AnyGridEditCancelled + OnEdited = AnyGridEdited + OnEditing = AnyGridEditing + OnFocusChanged = AnyGridFocusChanged + OnFocusChanging = AnyGridFocusChanging OnGetText = AnyGridGetText OnPaintText = AnyGridPaintText + OnGetNodeDataSize = AnyGridGetNodeDataSize + OnHeaderClick = vstHeaderClick OnInitNode = AnyGridInitNode OnKeyDown = AnyGridKeyDown + OnMouseUp = AnyGridMouseUp + OnNewText = AnyGridNewText Columns = <> end end @@ -2018,7 +2031,7 @@ object MainForm: TMainForm Enabled = False ImageIndex = 45 ShortCut = 16429 - OnExecute = actDataDuplicateRowExecute + OnExecute = actDataInsertExecute end object actDataDelete: TAction Category = 'Data' @@ -7944,6 +7957,24 @@ object MainForm: TMainForm object HTMLview1: TMenuItem Action = actHTMLview end + object N2: TMenuItem + Caption = '-' + end + object Insertrow1: TMenuItem + Action = actDataInsert + end + object Duplicaterow2: TMenuItem + Action = actDataDuplicateRow + end + object Cancelediting2: TMenuItem + Action = actDataCancelChanges + end + object Post1: TMenuItem + Action = actDataPostChanges + end + object Deleteselectedrows1: TMenuItem + Action = actDataDelete + end object N14: TMenuItem Caption = '-' end diff --git a/source/main.pas b/source/main.pas index 83564d60..12e27988 100644 --- a/source/main.pas +++ b/source/main.pas @@ -38,7 +38,7 @@ type LabelResultInfo: TLabel; Grid: TVirtualStringTree; TabSheet: TTabSheet; - GridResult: TGridResult; + Results: TMySQLQuery; FilterText: String; end; @@ -473,6 +473,12 @@ type Runroutines1: TMenuItem; actCreateEvent: TAction; Event1: TMenuItem; + Deleteselectedrows1: TMenuItem; + Insertrow1: TMenuItem; + Duplicaterow2: TMenuItem; + Post1: TMenuItem; + Cancelediting2: TMenuItem; + N2: TMenuItem; procedure actCreateDBObjectExecute(Sender: TObject); procedure menuConnectionsPopup(Sender: TObject); procedure actExitApplicationExecute(Sender: TObject); @@ -580,21 +586,20 @@ type procedure InsertDate(Sender: TObject); procedure setNULL1Click(Sender: TObject); function QueryLoad( filename: String; ReplaceContent: Boolean = true ): Boolean; - procedure DataGridChange(Sender: TBaseVirtualTree; Node: PVirtualNode); - procedure DataGridCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; + procedure AnyGridCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink); - procedure DataGridEditCancelled(Sender: TBaseVirtualTree; Column: TColumnIndex); - procedure DataGridEdited(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: + procedure AnyGridEditCancelled(Sender: TBaseVirtualTree; Column: TColumnIndex); + procedure AnyGridEdited(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); - procedure DataGridEditing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: + procedure AnyGridEditing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean); - procedure DataGridFocusChanging(Sender: TBaseVirtualTree; OldNode, NewNode: + procedure AnyGridFocusChanging(Sender: TBaseVirtualTree; OldNode, NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex; var Allowed: Boolean); procedure AnyGridGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: String); procedure DataGridHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo); procedure AnyGridKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); - procedure DataGridNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: + procedure AnyGridNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; NewText: String); procedure AnyGridPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType); @@ -659,19 +664,18 @@ type procedure menuTreeCollapseAllClick(Sender: TObject); procedure menuTreeExpandAllClick(Sender: TObject); procedure SynMemoFilterStatusChange(Sender: TObject; Changes: TSynStatusChanges); - procedure DataGridAfterCellPaint(Sender: TBaseVirtualTree; + procedure AnyGridAfterCellPaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; CellRect: TRect); procedure menuShowSizeColumnClick(Sender: TObject); procedure AnyGridBeforeCellPaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; CellPaintMode: TVTCellPaintMode; CellRect: TRect; var ContentRect: TRect); - procedure QueryGridFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); procedure pnlQueryHelpersCanResize(Sender: TObject; var NewWidth, NewHeight: Integer; var Resize: Boolean); procedure pnlQueryMemoCanResize(Sender: TObject; var NewWidth, NewHeight: Integer; var Resize: Boolean); - procedure DataGridMouseUp(Sender: TObject; Button: TMouseButton; + procedure AnyGridMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure File1Click(Sender: TObject); procedure ListVariablesBeforePaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas); @@ -719,7 +723,6 @@ type procedure TimerFilterVTTimer(Sender: TObject); procedure PageControlMainContextPopup(Sender: TObject; MousePos: TPoint; var Handled: Boolean); procedure menuQueryHelpersGenerateStatementClick(Sender: TObject); - procedure actDataDuplicateRowExecute(Sender: TObject); procedure actSelectInverseExecute(Sender: TObject); procedure FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean); @@ -741,7 +744,7 @@ type procedure AnyGridInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); procedure editFilterVTRightButtonClick(Sender: TObject); - procedure DataGridFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; + procedure AnyGridFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); procedure ListTablesKeyPress(Sender: TObject; var Key: Char); procedure DBtreeFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode); @@ -759,6 +762,7 @@ type Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: Integer); procedure ListDatabasesDblClick(Sender: TObject); procedure actRunRoutinesExecute(Sender: TObject); + procedure AnyGridGetNodeDataSize(Sender: TBaseVirtualTree; var NodeDataSize: Integer); private FDelimiter: String; FileNameSessionLog: String; @@ -796,7 +800,6 @@ type procedure UpdateFilterPanel(Sender: TObject); procedure DatabaseChanged(Database: String); procedure DBObjectsCleared(Database: String); - function GetBlobContent(Results: TMySQLQuery; Column: Integer): String; procedure DoSearchReplace; procedure UpdateLineCharPanel; procedure PaintColorBar(Value, Max: Extended; TargetCanvas: TCanvas; CellRect: TRect); @@ -871,8 +874,7 @@ type DataGridFocusedCell: TStringList; DataGridFocusedNodeIndex: Int64; DataGridFocusedColumnName: String; - DataGridHasChanges: Boolean; - DataGridResult: TGridResult; + DataGridResult: TMySQLQuery; DataGridFullRowMode: Boolean; SelectedTableCreateStatement: String; SelectedTableColumns: TTableColumnList; @@ -903,8 +905,8 @@ type procedure SessionConnect(Sender: TObject); function InitConnection(Params: TConnectionParameters; Session: String): Boolean; function ActiveGrid: TVirtualStringTree; - function GridResult(Grid: TBaseVirtualTree): TGridResult; overload; - function GridResult(PageIndex: Integer): TGridResult; overload; + function GridResult(Grid: TBaseVirtualTree): TMySQLQuery; overload; + function GridResult(PageIndex: Integer): TMySQLQuery; overload; property ActiveDatabase : String read GetActiveDatabase write SetSelectedDatabase; property SelectedTable : TDBObject read GetSelectedTable; procedure TestVTreeDataArray( P: PVTreeDataArray ); @@ -917,15 +919,6 @@ type procedure RefreshTree(DoResetTableCache: Boolean; SelectDatabase: String = ''); procedure RefreshActiveTreeDB(FocusObject: TDBObject); function FindDBNode(db: String): PVirtualNode; - function GridPostUpdate(Sender: TBaseVirtualTree): Boolean; - function GridPostInsert(Sender: TBaseVirtualTree): Boolean; - function GridPostDelete(Sender: TBaseVirtualTree): Boolean; - function DataGridPostUpdateOrInsert(Node: PVirtualNode): Boolean; - procedure GridFinalizeEditing(Sender: TBaseVirtualTree); - function GetWhereClause(Row: PGridRow; Columns: PGridColumns): String; - function GetKeyColumns: TStringList; - procedure DataGridInsertRow(CopyValuesFromNode: PVirtualNode); - procedure DataGridCancel(Sender: TObject); procedure CalcNullColors; procedure HandleDataGridAttributes(RefreshingData: Boolean); function GetRegKeyTable: String; @@ -939,9 +932,8 @@ type procedure SelectDBObject(Text: String; NodeType: TListNodeType); procedure SetupSynEditors; procedure ParseSelectedTableStructure; - function DataGridEnsureFullRow(Grid: TVirtualStringTree; Node: PVirtualNode): Boolean; + function AnyGridEnsureFullRow(Grid: TVirtualStringTree; Node: PVirtualNode): Boolean; procedure DataGridEnsureFullRows(Grid: TVirtualStringTree; SelectedOnly: Boolean); - function DataGridRowHasFullData(Node: PVirtualNode): Boolean; end; @@ -1039,8 +1031,7 @@ begin DoDisconnect; // Clearing query and browse data. - SetLength(DataGridResult.Rows, 0); - SetLength(DataGridResult.Columns, 0); + FreeAndNil(DataGridResult); // Save various settings OpenRegistry; @@ -1322,7 +1313,6 @@ begin QueryTab.spltQuery := spltQuery; QueryTab.LabelResultInfo := LabelResultInfo; QueryTab.Grid := QueryGrid; - QueryTab.GridResult := TGridResult.Create; QueryTabs := TObjectList.Create; QueryTabs.Add(QueryTab); @@ -1331,7 +1321,6 @@ begin SetupSynEditors; AllDatabases := TStringList.Create; - DataGridResult := TGridResult.Create; btnAddTab := TSpeedButton.Create(PageControlMain); btnAddTab.Parent := PageControlMain; @@ -1580,8 +1569,6 @@ var miFunction, miFilterFunction: TMenuItem; begin - DataGridHasChanges := False; - // Activate logging if GetRegValue(REGNAME_LOGTOFILE, DEFAULT_LOGTOFILE) then ActivateFileLogging; @@ -1685,6 +1672,8 @@ end; procedure TMainForm.DoDisconnect; +var + Results: TMySQLQuery; begin // Do nothing in case user clicked Cancel on session manager if (not Assigned(Connection)) or (not Connection.Active) then @@ -1696,8 +1685,9 @@ begin MainReg.WriteString( REGNAME_LASTUSEDDB, Connection.Database ); // Post pending UPDATE - if DataGridHasChanges then - actDataPostChangesExecute(Self); + Results := GridResult(DataGrid); + if Assigned(Results) and Results.Modified then + actDataPostChangesExecute(DataGrid); // Clear database and table lists DBtree.ClearSelection; @@ -2156,9 +2146,9 @@ begin if g = nil then begin messagebeep(MB_ICONASTERISK); exit; end; Screen.Cursor := crHourGlass; ShowStatusMsg('Saving contents to file...'); - IsBinary := GridResult(ActiveGrid).Columns[g.FocusedColumn].DatatypeCat = dtcBinary; + IsBinary := GridResult(ActiveGrid).DataType(g.FocusedColumn).Category = dtcBinary; - DataGridEnsureFullRow(g, g.FocusedNode); + AnyGridEnsureFullRow(g, g.FocusedNode); Header := WideHexToBin(Copy(g.Text[g.FocusedNode, g.FocusedColumn], 3, 20)); SaveBinary := false; filename := GetTempDir+'\'+APPNAME+'-preview.'; @@ -2365,21 +2355,50 @@ end; procedure TMainForm.actDataDeleteExecute(Sender: TObject); +var + Grid: TVirtualStringTree; + Node, FocusAfterDelete: PVirtualNode; + RowNum: PCardinal; + Results: TMySQLQuery; + Nodes: TNodeArray; + i: Integer; begin // Delete row(s) - if (DataGrid.SelectedCount = 1) and - (DataGridResult.Rows[DataGrid.GetFirstSelected.Index].State = grsInserted) - then begin - // Deleting the virtual row which is only in memory by stopping edit mode - actDataCancelChanges.Execute; - end else begin - // The "normal" case: Delete existing rows - if DataGrid.SelectedCount = 0 then - MessageDLG('Please select one or more rows to delete them.', mtError, [mbOK], 0) - else if MessageDLG('Delete '+inttostr(DataGrid.SelectedCount)+' row(s)?', - mtConfirmation, [mbOK, mbCancel], 0) = mrOK then begin - GridPostDelete(DataGrid); + Grid := ActiveGrid; + Results := GridResult(Grid); + if Grid.SelectedCount = 0 then + MessageDLG('Please select one or more rows to delete them.', mtError, [mbOK], 0) + else try + Results.CheckEditable; + if MessageDLG('Delete '+IntToStr(Grid.SelectedCount)+' row(s)?', + mtConfirmation, [mbOK, mbCancel], 0) = mrOK then begin + FocusAfterDelete := nil; + Node := Grid.GetFirstSelected; + while Assigned(Node) do begin + RowNum := Grid.GetNodeData(Node); + Results.RecNo := RowNum^; + if Results.DeleteRow then begin + SetLength(Nodes, Length(Nodes)+1); + Nodes[Length(Nodes)-1] := Node; + FocusAfterDelete := Node; + end; + Node := Grid.GetNextSelected(Node); + end; + if Assigned(FocusAfterDelete) then + FocusAfterDelete := Grid.GetNext(FocusAfterDelete); + // Remove nodes and select some nearby node + Grid.BeginUpdate; + for i:=Low(Nodes) to High(Nodes) do + Grid.DeleteNode(Nodes[i]); + Grid.EndUpdate; + if not Assigned(FocusAfterDelete) then + FocusAfterDelete := Grid.GetLast; + if Assigned(FocusAfterDelete) then + SelectNode(Grid, FocusAfterDelete); + ValidateControls(Sender); end; + except on E:EDatabaseError do + MessageDlg('Grid editing error: '+E.Message, mtError, [mbOK], 0); end; end; @@ -3048,42 +3067,83 @@ procedure TMainForm.actDataFirstExecute(Sender: TObject); var Node: PVirtualNode; begin - Node := DataGrid.GetFirst; - if Assigned(Node) then begin - DataGrid.ClearSelection; - DataGrid.FocusedNode := Node; - DataGrid.Selected[Node] := True; - end; + Node := ActiveGrid.GetFirst; + if Assigned(Node) then + SelectNode(ActiveGrid, Node); end; + procedure TMainForm.actDataInsertExecute(Sender: TObject); +var + DupeNode, NewNode: PVirtualNode; + Grid: TVirtualStringTree; + Results: TMySQLQuery; + RowNum: Cardinal; + DupeNum: PCardinal; + i: Integer; + Value: String; + IsNull: Boolean; begin - DataGridInsertRow(nil); -end; - - -procedure TMainForm.actDataDuplicateRowExecute(Sender: TObject); -begin - DataGridEnsureFullRow(DataGrid, DataGrid.FocusedNode); - DataGridInsertRow(DataGrid.FocusedNode); + Grid := ActiveGrid; + Results := GridResult(Grid); + try + Results.CheckEditable; + DupeNode := nil; + if Sender = actDataDuplicateRow then + DupeNode := Grid.FocusedNode; + RowNum := Results.InsertRow; + NewNode := Grid.InsertNode(Grid.FocusedNode, amInsertAfter, PCardinal(RowNum)); + SelectNode(Grid, NewNode); + if Assigned(DupeNode) then begin + // Copy values from source row, ensure we have whole cell data + DupeNum := Grid.GetNodeData(DupeNode); + AnyGridEnsureFullRow(Grid, DupeNode); + for i:=0 to Grid.Header.Columns.Count-1 do begin + if not (coVisible in DataGrid.Header.Columns[i].Options) then + continue; // Ignore invisible key column + if Results.ColIsPrimaryKeyPart(i) then + continue; // Empty value for primary key column + Results.RecNo := DupeNum^; + Value := Results.Col(i); + IsNull := Results.IsNull(i); + Results.RecNo := RowNum; + Results.SetCol(i, Value, IsNull); + end; + end; + except on E:EDatabaseError do + MessageDlg('Grid editing error: '+E.Message, mtError, [mbOk], 0); + end; end; procedure TMainForm.actDataLastExecute(Sender: TObject); var Node: PVirtualNode; + Grid: TVirtualStringTree; begin + Grid := ActiveGrid; // Be sure to have all rows - if DatagridWantedRowCount < prefGridRowcountMax then + if (Grid = DataGrid) and (DatagridWantedRowCount < prefGridRowcountMax) then actDataShowAll.Execute; - Node := DataGrid.GetLast; + Node := Grid.GetLast; if Assigned(Node) then - SelectNode(DataGrid, Node); + SelectNode(Grid, Node); end; procedure TMainForm.actDataPostChangesExecute(Sender: TObject); +var + Grid: TVirtualStringTree; + Results: TMySQLQuery; begin - DataGridPostUpdateOrInsert(Datagrid.FocusedNode); + if Sender is TVirtualStringTree then + Grid := Sender as TVirtualStringTree + else + Grid := ActiveGrid; + Results := GridResult(Grid); + Results.SaveModifications; + // Node needs a repaint to remove red triangles + if Assigned(Grid.FocusedNode) then + Grid.InvalidateNode(Grid.FocusedNode); end; procedure TMainForm.actRemoveFilterExecute(Sender: TObject); @@ -3094,8 +3154,28 @@ end; procedure TMainForm.actDataCancelChangesExecute(Sender: TObject); +var + Grid: TVirtualStringTree; + Results: TMySQLQuery; + RowNum: PCardinal; + Node, FocNode: PVirtualNode; begin - DataGridCancel(Sender); + // Cancel INSERT or UPDATE mode + Grid := ActiveGrid; + Node := Grid.FocusedNode; + if Assigned(Node) then begin + Results := GridResult(Grid); + RowNum := Grid.GetNodeData(Node); + Results.RecNo := RowNum^; + Results.DiscardModifications; + if Results.Inserted then begin + FocNode := Grid.GetPreviousSibling(Node); + Grid.DeleteNode(Node); + SelectNode(Grid, FocNode); + end else + Grid.InvalidateNode(Node); + ValidateControls(Sender); + end; end; @@ -3210,62 +3290,35 @@ begin end; -function TMainForm.DataGridEnsureFullRow(Grid: TVirtualStringTree; Node: PVirtualNode): Boolean; +function TMainForm.AnyGridEnsureFullRow(Grid: TVirtualStringTree; Node: PVirtualNode): Boolean; var - i: Integer; - Row: PGridRow; - Select: String; + RowNum: PCardinal; Data: TMySQLQuery; begin // Load remaining data on a partially loaded row in data grid - if Grid <> DataGrid then begin - Result := True; - Exit; - end; - Row := @DataGridResult.Rows[Node.Index]; - if not DataGridRowHasFullData(Node) then begin - Select := 'SELECT '; - for i:=0 to Length(DataGridResult.Columns)-1 do - Select := Select + mask(DataGridResult.Columns[i].Name) + ', '; - Delete(Select, Length(Select)-1, 2); - Select := Select + - ' FROM '+mask(SelectedTable.Name) + - ' WHERE '+GetWhereClause(Row, @DataGridResult.Columns) + - ' LIMIT 1'; - try - Data := Connection.GetResults(Select); - if Data.RecordCount = 0 then - raise Exception.Create('Unable to find row.'); - for i:=0 to Length(DataGridResult.Columns)-1 do begin - case DataGridResult.Columns[i].DatatypeCat of - dtcInteger, dtcReal: Row.Cells[i].Text := FormatNumber(Data.Col(i), False); - dtcBinary, dtcSpatial: Row.Cells[i].Text := GetBlobContent(Data, i); - else Row.Cells[i].Text := Data.Col(i); - end; - Row.Cells[i].IsNull := Data.IsNull(i); - end; - Row.HasFullData := True; - except On E:EDatabaseError do - MessageDlg(E.Message, mtError, [mbOK], 0); - end; - end; - Result := Row.HasFullData; + RowNum := Grid.GetNodeData(Node); + Data := GridResult(Grid); + Data.RecNo := RowNum^; + Result := Data.EnsureFullRow; end; procedure TMainForm.DataGridEnsureFullRows(Grid: TVirtualStringTree; SelectedOnly: Boolean); var Node: PVirtualNode; + Results: TMySQLQuery; + RowNum: PCardinal; begin // Load remaining data of all grid rows - if Grid <> DataGrid then - Exit; + Results := GridResult(Grid); if SelectedOnly then Node := Grid.GetFirstSelected else Node := Grid.GetFirst; while Assigned(Node) do begin - if not DataGridRowHasFullData(Node) then begin + RowNum := Grid.GetNodeData(Node); + Results.RecNo := RowNum^; + if not Results.HasFullData then begin DataGridFullRowMode := True; InvalidateVT(Grid, VTREE_NOTLOADED_PURGECACHE, True); break; @@ -3278,36 +3331,14 @@ begin end; -function TMainForm.DataGridRowHasFullData(Node: PVirtualNode): Boolean; -var - i: Integer; - HasFullData: Boolean; - Row: PGridRow; -begin - Row := @DataGridResult.Rows[Node.Index]; - if not Row.HasFullData then begin - HasFullData := True; - for i:=0 to Length(DataGridResult.Columns)-1 do begin - HasFullData := Length(Row.Cells[i].Text) < GRIDMAXDATA; - if not HasFullData then - break; - end; - Row.HasFullData := HasFullData; - end; - Result := Row.HasFullData; -end; - - procedure TMainForm.DataGridBeforePaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas); var vt: TVirtualStringTree; - Data: TMySQLQuery; Select: String; RefreshingData, IsKeyColumn: Boolean; - i, j, Offset, ColLen, ColWidth: Integer; - KeyCols, ColWidths: TStringList; + i, Offset, ColLen, ColWidth: Integer; + KeyCols, ColWidths, WantedColumnOrgnames: TStringList; WantedColumns: TTableColumnList; - Cell: PGridCell; c: TTableColumn; procedure InitColumn(idx: Integer; TblCol: TTableColumn); @@ -3315,8 +3346,6 @@ var k: Integer; Col: TVirtualTreeColumn; begin - SetLength(DataGridResult.Columns, idx+1); - DataGridResult.Columns[idx].Name := TblCol.Name; col := vt.Header.Columns.Add; col.Text := TblCol.Name; col.Hint := TblCol.Comment; @@ -3343,38 +3372,9 @@ var end; // Data type - DataGridResult.Columns[idx].DatatypeCat := TblCol.DataType.Category; - DataGridResult.Columns[idx].Datatype := TblCol.DataType.Index; col.Alignment := taLeftJustify; - case DataGridResult.Columns[idx].DatatypeCat of - dtcInteger, dtcReal: col.Alignment := taRightJustify; - dtcText: begin - if TblCol.LengthSet <> '' then - DataGridResult.Columns[idx].MaxLength := MakeInt(TblCol.LengthSet) - else case TblCol.DataType.Index of - // 255 is the width in bytes. If characters that use multiple bytes are - // contained, the width in characters is decreased below this number. - dtTinyText: DataGridResult.Columns[idx].MaxLength := 255; - dtText: DataGridResult.Columns[idx].MaxLength := 65535; - dtMediumText: DataGridResult.Columns[idx].MaxLength := 16777215; - dtLongText: DataGridResult.Columns[idx].MaxLength := 4294967295; - end; - end; - dtcIntegerNamed: begin - DataGridResult.Columns[idx].ValueList := TStringList.Create; - DataGridResult.Columns[idx].ValueList.QuoteChar := ''''; - DataGridResult.Columns[idx].ValueList.Delimiter := ','; - DataGridResult.Columns[idx].ValueList.DelimitedText := TblCol.LengthSet; - end; - dtcSetNamed: begin - DataGridResult.Columns[idx].ValueList := TStringList.Create; - DataGridResult.Columns[idx].ValueList.QuoteChar := ''''; - DataGridResult.Columns[idx].ValueList.Delimiter := ','; - DataGridResult.Columns[idx].ValueList.DelimitedText := TblCol.LengthSet; - end; - else DataGridResult.Columns[idx].MaxLength := MaxInt; // Fallback for unknown column types - end; - DataGridResult.Columns[idx].IsPriPart := Data.ColIsPrimaryKeyPart(idx); + if DataGridResult.DataType(idx).Category in [dtcInteger, dtcReal] then + col.Alignment := taRightJustify; end; begin @@ -3407,8 +3407,9 @@ begin Select := 'SELECT '; // Ensure key columns are included to enable editing - KeyCols := GetKeyColumns; + KeyCols := Connection.GetKeyColumns(SelectedTableColumns, SelectedTableKeys); WantedColumns := TTableColumnList.Create(False); + WantedColumnOrgnames := TStringList.Create; for i:=0 to SelectedTableColumns.Count-1 do begin c := SelectedTableColumns[i]; IsKeyColumn := KeyCols.IndexOf(c.Name) > -1; @@ -3427,6 +3428,7 @@ begin else Select := Select + ' ' + Mask(c.Name) + ', '; WantedColumns.Add(c); + WantedColumnOrgnames.Add(c.Name); end; end; // Cut last comma @@ -3456,57 +3458,38 @@ begin // Append LIMIT clause if RefreshingData and (vt.Tag <> VTREE_NOTLOADED_PURGECACHE) then - Offset := Length(DataGridResult.Rows) + Offset := DataGridResult.RecordCount else Offset := 0; Select := Select + ' LIMIT '+IntToStr(Offset)+', '+IntToStr(DatagridWantedRowCount-Offset); try ShowStatusMsg('Fetching rows ...'); - Data := Connection.GetResults(Select); - except - // Wrong WHERE clause in most cases - On E:EDatabaseError do - MessageDlg(E.Message, mtError, [mbOK], 0); - end; + if not Assigned(DataGridResult) then + DataGridResult := TMySQLQuery.Create(Self); + DataGridResult.Connection := Connection; + DataGridResult.SQL := Select; + DataGridResult.Execute(Offset > 0); + DataGridResult.ColumnOrgNames := WantedColumnOrgnames; - if Assigned(Data) then begin editFilterVT.Clear; TimerFilterVT.OnTimer(Sender); + // Assign new data + vt.BeginUpdate; + vt.Clear; + vt.RootNodeCount := DataGridResult.RecordCount; + // Set up grid column headers ShowStatusMsg('Setting up columns ...'); ColWidths := TStringList.Create; for i:=0 to vt.Header.Columns.Count-1 do ColWidths.Values[vt.Header.Columns[i].Text] := IntToStr(vt.Header.Columns[i].Width); - SetLength(DataGridResult.Columns, Data.ColumnCount); vt.Header.Columns.BeginUpdate; vt.Header.Columns.Clear; for i:=0 to WantedColumns.Count-1 do InitColumn(i, WantedColumns[i]); - // Set up grid rows and data array - ShowStatusMsg('Copying rows to internal structure ...'); - vt.BeginUpdate; - SetLength(DataGridResult.Rows, Offset+Data.RecordCount); - for i:=Offset to Offset+Data.RecordCount-1 do begin - DataGridResult.Rows[i].HasFullData := DataGridFullRowMode or (KeyCols.Count = 0); - for j:=0 to Length(DataGridResult.Columns)-1 do begin - SetLength(DataGridResult.Rows[i].Cells, Data.ColumnCount); - Cell := @DataGridResult.Rows[i].Cells[j]; - case DataGridResult.Columns[j].DatatypeCat of - dtcInteger, dtcReal: Cell.Text := FormatNumber(Data.Col(j), False); - dtcBinary, dtcSpatial: Cell.Text := GetBlobContent(Data, j); - else Cell.Text := Data.Col(j); - end; - Cell.IsNull := Data.IsNull(j); - end; - Data.Next; - end; - ShowStatusMsg('Freeing memory ...'); - FreeAndNil(Data); - vt.RootNodeCount := Length(DataGridResult.Rows); - // Autoset or restore column width for i:=0 to vt.Header.Columns.Count-1 do begin ColWidth := 0; @@ -3521,6 +3504,7 @@ begin vt.Header.Columns.EndUpdate; vt.EndUpdate; + // Do not steel filter while writing filters if not SynMemoFilter.Focused then vt.SetFocus; @@ -3545,7 +3529,13 @@ begin EnumerateRecentFilters; if Integer(vt.RootNodeCount) = prefGridRowcountMax then LogSQL('Browsing is currently limited to a maximum of '+FormatNumber(prefGridRowcountMax)+' rows. To see more rows, increase this maximum in Tools > Preferences > Data .', lcInfo); + + except + // Wrong WHERE clause in most cases + on E:EDatabaseError do + MessageDlg(E.Message, mtError, [mbOK], 0); end; + end; vt.Tag := VTREE_LOADED; DataGridFullRowMode := False; @@ -3593,14 +3583,27 @@ begin end; -procedure TMainForm.AnyGridInitNode(Sender: TBaseVirtualTree; ParentNode, - Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); +procedure TMainForm.AnyGridInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; + var InitialStates: TVirtualNodeInitStates); +var + Idx: PCardinal; begin // Display multiline grid rows if prefGridRowsLineCount = DEFAULT_GRIDROWSLINECOUNT then Exclude(Node.States, vsMultiLine) else Include(Node.States, vsMultiLine); + // Node may have data already, if added via InsertRow + if not (vsInitialUserData in Node.States) then begin + Idx := Sender.GetNodeData(Node); + Idx^ := Node.Index; + end; +end; + + +procedure TMainForm.AnyGridGetNodeDataSize(Sender: TBaseVirtualTree; var NodeDataSize: Integer); +begin + NodeDataSize := SizeOf(Cardinal); end; @@ -3610,6 +3613,7 @@ end; procedure TMainForm.PageControlMainChange(Sender: TObject); var tab: TTabSheet; + Grid: TVirtualStringTree; begin tab := PageControlMain.ActivePage; @@ -3634,6 +3638,11 @@ begin // Ensure controls are in a valid state ValidateControls(Sender); FixQueryTabCloseButtons; + + // Leave editing mode on tab changes so the editor does not stay somewhere + Grid := ActiveGrid; + if Assigned(Grid) and Grid.IsEditing then + Grid.CancelEditNode; end; @@ -3803,30 +3812,37 @@ end; } procedure TMainForm.ValidateControls(Sender: TObject); var - inDataGrid, inDataTab, inDataOrQueryTab, inDataOrQueryTabNotEmpty: Boolean; - SelectedNodes: TNodeArray; + inDataTab, inDataOrQueryTab, inDataOrQueryTabNotEmpty, inGrid, GridHasChanges: Boolean; + Grid: TVirtualStringTree; + Results: TMySQLQuery; + RowNum: PCardinal; begin + Grid := ActiveGrid; + Results := nil; + GridHasChanges := False; + if Assigned(Grid) then begin + Results := GridResult(Grid); + if Assigned(Grid.FocusedNode) then begin + RowNum := Grid.GetNodeData(Grid.FocusedNode); + Results.RecNo := RowNum^; + GridHasChanges := Results.Modified or Results.Inserted; + end; + end; inDataTab := PageControlMain.ActivePage = tabData; - inDataGrid := ActiveControl = DataGrid; - inDataOrQueryTab := (PageControlMain.ActivePage = tabData) or QueryTabActive; - inDataOrQueryTabNotEmpty := inDataOrQueryTab and (ActiveGrid.RootNodeCount > 0); - - SelectedNodes := ListTables.GetSortedSelection(False); + inDataOrQueryTab := inDataTab or QueryTabActive; + inDataOrQueryTabNotEmpty := inDataOrQueryTab and (Grid.RootNodeCount > 0); + inGrid := inDataOrQueryTab and (ActiveControl = Grid); actSQLhelp.Enabled := Connection.ServerVersionInt >= 40100; actImportCSV.Enabled := Connection.ServerVersionInt >= 32206; - // Data tab - if query results are made editable, these will need - // to be changed to look at which tab is focused. - actDataInsert.Enabled := inDataGrid; - actDataDuplicateRow.Enabled := inDataGrid and Assigned(ActiveGrid.FocusedNode); - actDataDelete.Enabled := inDataGrid and (DataGrid.SelectedCount > 0); - actDataFirst.Enabled := inDataGrid; - actDataLast.Enabled := inDataGrid; - actDataPostChanges.Enabled := inDataGrid and DataGridHasChanges; - actDataCancelChanges.Enabled := inDataGrid and DataGridHasChanges; - if (not inDataTab) and DataGrid.IsEditing then - DataGrid.EndEditNode; + actDataInsert.Enabled := inGrid and Assigned(Results); + actDataDuplicateRow.Enabled := inGrid and inDataOrQueryTabNotEmpty and Assigned(Grid.FocusedNode); + actDataDelete.Enabled := inGrid and (Grid.SelectedCount > 0); + actDataFirst.Enabled := inDataOrQueryTabNotEmpty and inGrid; + actDataLast.Enabled := inDataOrQueryTabNotEmpty and inGrid; + actDataPostChanges.Enabled := GridHasChanges; + actDataCancelChanges.Enabled := GridHasChanges; // Activate export-options if we're on Data- or Query-tab actCopyAsCSV.Enabled := inDataOrQueryTabNotEmpty; @@ -3834,10 +3850,9 @@ begin actCopyAsXML.Enabled := inDataOrQueryTabNotEmpty; actCopyAsSQL.Enabled := inDataOrQueryTabNotEmpty; actExportData.Enabled := inDataOrQueryTabNotEmpty; - actHTMLView.Enabled := inDataOrQueryTabNotEmpty and Assigned(ActiveGrid.FocusedNode); - setNull1.Enabled := inDataGrid and Assigned(DataGrid.FocusedNode); + actHTMLView.Enabled := inDataOrQueryTabNotEmpty and Assigned(Grid.FocusedNode); + setNull1.Enabled := inDataOrQueryTab and Assigned(Results) and Assigned(Grid.FocusedNode); - // Query tab // Manually invoke OnChange event of tabset to fill helper list with data if QueryTabActive then RefreshQueryHelpers; @@ -3925,13 +3940,12 @@ end; procedure TMainForm.ExecSQLClick(Sender: TObject; Selection: Boolean=false; CurrentLine: Boolean=false); var SQL: TStringList; - i, j, QueryCount: Integer; + i, QueryCount: Integer; SQLTime, SQLNetTime: Cardinal; Results: TMySQLQuery; ColName, Text, LB: String; col: TVirtualTreeColumn; ResultLabel: TLabel; - ActiveGridResult: TGridResult; cap: String; begin ResultLabel := ActiveQueryTab.LabelResultInfo; @@ -4018,44 +4032,25 @@ begin ActiveGrid.Header.Options := ActiveGrid.Header.Options + [hoVisible]; ActiveGrid.Header.Columns.BeginUpdate; ActiveGrid.Header.Columns.Clear; - debug('mem: clearing and initializing query columns.'); - ActiveGridResult := GridResult(ActiveGrid); - SetLength(ActiveGridResult.Columns, 0); - SetLength(ActiveGridResult.Columns, Results.ColumnCount); + ActiveQueryTab.Results := Results; + for i:=0 to Results.ColumnCount-1 do begin ColName := Results.ColumnNames[i]; col := ActiveGrid.Header.Columns.Add; col.Text := ColName; - col.Options := col.Options - [coAllowClick]; - ActiveGridResult.Columns[i].Name := ColName; - ActiveGridResult.Columns[i].DatatypeCat := Results.DataType(i).Category; - if ActiveGridResult.Columns[i].DatatypeCat in [dtcInteger, dtcReal] then + if Results.DataType(i).Category in [dtcInteger, dtcReal] then col.Alignment := taRightJustify; - ActiveGridResult.Columns[i].IsPriPart := Results.ColIsPrimaryKeyPart(i); + if Results.ColIsPrimaryKeyPart(i) then + col.ImageIndex := ICONINDEX_PRIMARYKEY + else if Results.ColIsUniqueKeyPart(i) then + col.ImageIndex := ICONINDEX_UNIQUEKEY + else if Results.ColIsKeyPart(i) then + col.ImageIndex := ICONINDEX_INDEXKEY; end; - debug('mem: query column initialization complete.'); - debug('mem: clearing and initializing query rows (internal data).'); - SetLength(ActiveGridResult.Rows, 0); - SetLength(ActiveGridResult.Rows, Results.RecordCount); - Results.First; - for i:=0 to Results.RecordCount-1 do begin - SetLength(ActiveGridResult.Rows[i].Cells, Results.ColumnCount); - for j:=0 to Results.ColumnCount-1 do begin - case ActiveGridResult.Columns[j].DatatypeCat of - dtcInteger, dtcReal: ActiveGridResult.Rows[i].Cells[j].Text := FormatNumber(Results.Col(j), False); - dtcBinary, dtcSpatial: ActiveGridResult.Rows[i].Cells[j].Text := GetBlobContent(Results, j); - else ActiveGridResult.Rows[i].Cells[j].Text := Results.Col(j); - end; - ActiveGridResult.Rows[i].Cells[j].IsNull := Results.IsNull(j); - end; - Results.Next; - end; - Results.Free; - debug('mem: initializing query rows (grid).'); - ActiveGrid.RootNodeCount := Length(ActiveGridResult.Rows); - debug('mem: query row initialization complete.'); + // Remove all nodes + ActiveGrid.Clear; + ActiveGrid.RootNodeCount := Results.RecordCount; ActiveGrid.Header.Columns.EndUpdate; - ActiveGrid.ClearSelection; ActiveGrid.OffsetXY := Point(0, 0); for i:=0 to ActiveGrid.Header.Columns.Count-1 do AutoCalcColWidth(ActiveGrid, i); @@ -4854,6 +4849,7 @@ var y,m,d,h,i,s,ms : Word; cpText, Col, value : String; CellFocused: Boolean; + RowNumber: PCardinal; const CLPBRD : String = 'CLIPBOARD'; begin @@ -4872,10 +4868,12 @@ begin DataYear.Caption := Format('%.4d', [y]); // Manipulate the Quick-filter menuitems - DataGridEnsureFullRow(DataGrid, DataGrid.FocusedNode); - Col := mask(DataGrid.Header.Columns[DataGrid.FocusedColumn].Text); + AnyGridEnsureFullRow(DataGrid, DataGrid.FocusedNode); + RowNumber := DataGrid.GetNodeData(DataGrid.FocusedNode); + DatagridResult.RecNo := RowNumber^; + Col := mask(DatagridResult.ColumnNames[DataGrid.FocusedColumn]); // 1. block: include selected columnname and value from datagrid in caption - if DataGridResult.Rows[DataGrid.FocusedNode.Index].Cells[DataGrid.FocusedColumn].IsNull then begin + if DatagridResult.IsNull(DataGrid.FocusedColumn) then begin QF1.Hint := Col + ' IS NULL'; QF2.Hint := Col + ' IS NOT NULL'; QF3.Visible := False; @@ -4950,7 +4948,7 @@ begin QFvalues[0].OnClick := nil; if DataGrid.FocusedColumn = NoColumn then Exit; - Col := DataGridResult.Columns[DataGrid.FocusedColumn].Name; + Col := DataGridResult.ColumnNames[DataGrid.FocusedColumn]; ShowStatusMsg('Fetching distinct values ...'); Data := Connection.GetResults('SELECT '+mask(Col)+', COUNT(*) AS c FROM '+mask(SelectedTable.Name)+ ' GROUP BY '+mask(Col)+' ORDER BY c DESC, '+mask(Col)+' LIMIT 30'); @@ -5507,7 +5505,9 @@ begin Sender.SortDirection := sdDescending else Sender.SortDirection := sdAscending; + Screen.Cursor := crHourglass; Sender.Treeview.SortTree( HitInfo.Column, Sender.SortDirection ); + Screen.Cursor := crDefault; end; @@ -6262,8 +6262,8 @@ begin if not FProcessDBtreeFocusChanges then Exit; // Post pending UPDATE - if DataGridHasChanges then - actDataPostChangesExecute(Sender); + if Assigned(DataGridResult) and DataGridResult.Modified then + actDataPostChangesExecute(DataGrid); case Sender.GetNodeLevel(Node) of 0: begin if (not DBtree.Dragging) and (not QueryTabActive) then begin @@ -6629,32 +6629,35 @@ end; {** A grid cell fetches its text content } -procedure TMainForm.AnyGridGetText(Sender: TBaseVirtualTree; Node: - PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: String); +procedure TMainForm.AnyGridGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; + Column: TColumnIndex; TextType: TVSTTextType; var CellText: string); var - c: PGridCell; - gr: TGridResult; - EditingCell: Boolean; + EditingAndFocused: Boolean; + RowNumber: PCardinal; + Results: TMySQLQuery; begin if Column = -1 then Exit; - gr := GridResult(Sender); - - c := @gr.Rows[Node.Index].Cells[Column]; - EditingCell := Sender.IsEditing and (Node = Sender.FocusedNode) and (Column = Sender.FocusedColumn); - if c.Modified then begin - if c.NewIsNull then begin - if EditingCell then CellText := '' - else CellText := TEXT_NULL; - end else CellText := c.NewText; - end else begin - if c.IsNull then begin - if EditingCell then CellText := '' - else CellText := TEXT_NULL; - end else begin - CellText := c.Text; - if (Sender = DataGrid) and (not DataGridRowHasFullData(Node)) and (Length(c.Text) = GRIDMAXDATA) then - CellText := CellText + ' [...]'; + EditingAndFocused := Sender.IsEditing and (Node = Sender.FocusedNode) and (Column = Sender.FocusedColumn); + Results := GridResult(Sender); + RowNumber := Sender.GetNodeData(Node); + Results.RecNo := RowNumber^; + if Results.IsNull(Column) and (not EditingAndFocused) then + CellText := TEXT_NULL + else begin + case Results.DataType(Column).Category of + dtcInteger, dtcReal: CellText := FormatNumber(Results.Col(Column), False); + dtcBinary, dtcSpatial: begin + if actBlobAsText.Checked then + CellText := Results.Col(Column) + else + CellText := '0x' + Results.BinColAsHex(Column); + end; + else begin + CellText := Results.Col(Column); + if (Length(CellText) = GRIDMAXDATA) and (not Results.HasFullData) then + CellText := CellText + ' [...]'; + end; end; end; end; @@ -6678,20 +6681,22 @@ end; Cell in data- or query grid gets painted. Colorize font. This procedure is called extremely often for repainting the grid cells. Keep it highly optimized. } -procedure TMainForm.AnyGridPaintText(Sender: TBaseVirtualTree; const - TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; TextType: - TVSTTextType); +procedure TMainForm.AnyGridPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; + Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType); var cl: TColor; - r: TGridResult; + r: TMySQLQuery; + RowNumber: PCardinal; begin - if Column = -1 then + if Column = NoColumn then Exit; r := GridResult(Sender); + RowNumber := Sender.GetNodeData(Node); + r.RecNo := RowNumber^; // Make primary key columns bold - if r.Columns[Column].IsPriPart then + if r.ColIsPrimaryKeyPart(Column) then TargetCanvas.Font.Style := TargetCanvas.Font.Style + [fsBold]; // Do not apply any color on a selected, highlighted cell to keep readability @@ -6699,22 +6704,27 @@ begin cl := clHighlightText else if vsSelected in Node.States then cl := clBlack - else if r.Rows[Node.Index].Cells[Column].IsNull then - cl := DatatypeCategories[Integer(r.Columns[Column].DatatypeCat)].NullColor + else if r.IsNull(Column) then + cl := DatatypeCategories[Integer(r.DataType(Column).Category)].NullColor else - cl := DatatypeCategories[Integer(r.Columns[Column].DatatypeCat)].Color; + cl := DatatypeCategories[Integer(r.DataType(Column).Category)].Color; TargetCanvas.Font.Color := cl; end; -procedure TMainForm.DataGridAfterCellPaint(Sender: TBaseVirtualTree; - TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; - CellRect: TRect); +procedure TMainForm.AnyGridAfterCellPaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas; + Node: PVirtualNode; Column: TColumnIndex; CellRect: TRect); +var + Results: TMySQLQuery; + RowNum: PCardinal; begin // Don't waist time if Column = NoColumn then Exit; // Paint a red triangle at the top left corner of the cell - if DataGridResult.Rows[Node.Index].Cells[Column].Modified then + Results := GridResult(Sender); + RowNum := Sender.GetNodeData(Node); + Results.RecNo := RowNum^; + if Results.Modified(Column) then ImageListMain.Draw(TargetCanvas, CellRect.Left, CellRect.Top, 111); end; @@ -6782,41 +6792,41 @@ end; Only allow grid editing if there is a good key available } procedure TMainForm.setNULL1Click(Sender: TObject); +var + RowNum: PCardinal; + Grid: TVirtualStringTree; + Results: TMySQLQuery; begin // Internally calls OnNewText event: - DataGrid.Text[DataGrid.FocusedNode, DataGrid.FocusedColumn] := ''; - DataGridResult.Rows[DataGrid.FocusedNode.Index].Cells[DataGrid.FocusedColumn].NewIsNull := True; - DataGrid.RepaintNode(DataGrid.FocusedNode); + Grid := ActiveGrid; + RowNum := Grid.GetNodeData(Grid.FocusedNode); + Results := GridResult(Grid); + Results.RecNo := RowNum^; + try + Results.SetCol(Grid.FocusedColumn, '', True); + except + on E:EDatabaseError do MessageDlg(E.Message, mtError, [mbOK], 0); + end; + Grid.RepaintNode(Grid.FocusedNode); end; {** Content of a grid cell was modified } -procedure TMainForm.DataGridNewText(Sender: TBaseVirtualTree; Node: - PVirtualNode; Column: TColumnIndex; NewText: String); +procedure TMainForm.AnyGridNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; NewText: String); var - Row: PGridRow; -begin - Row := @DataGridResult.Rows[Node.Index]; - // Remember new value - Row.Cells[Column].NewText := NewText; - Row.Cells[Column].NewIsNull := False; - Row.Cells[Column].Modified := True; - // Set state of row for UPDATE mode, don't touch grsInserted - if Row.State = grsDefault then - DataGridResult.Rows[Node.Index].State := grsModified; - DataGridHasChanges := True; - ValidateControls(Sender); -end; - - -{** - DataGrid: node focus has changed -} -procedure TMainForm.DataGridChange(Sender: TBaseVirtualTree; Node: - PVirtualNode); + Results: TMySQLQuery; + RowNum: PCardinal; begin + Results := GridResult(Sender); + RowNum := Sender.GetNodeData(Node); + Results.RecNo := RowNum^; + try + Results.SetCol(Column, NewText, False); + except + on E:EDatabaseError do MessageDlg(E.Message, mtError, [mbOK], 0); + end; ValidateControls(Sender); end; @@ -6824,426 +6834,37 @@ end; {** DataGrid: node and/or column focus is about to change. See if we allow that. } -procedure TMainForm.DataGridFocusChanging(Sender: TBaseVirtualTree; OldNode, +procedure TMainForm.AnyGridFocusChanging(Sender: TBaseVirtualTree; OldNode, NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex; var Allowed: Boolean); +var + Results: TMySQLQuery; + RowNum: PCardinal; begin // Detect changed focus and update row - if Assigned(OldNode) and (OldNode <> NewNode) then - Allowed := DataGridPostUpdateOrInsert(OldNode) - else - Allowed := True; + Allowed := True; + Results := GridResult(Sender); + if Assigned(OldNode) and (OldNode <> NewNode) then begin + RowNum := Sender.GetNodeData(OldNode); + Results.RecNo := RowNum^; + if Results.Modified then + Allowed := Results.SaveModifications + else if Results.Inserted then begin + Results.DiscardModifications; + Sender.DeleteNode(OldNode); + end; + end; end; -procedure TMainForm.DataGridFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; +procedure TMainForm.AnyGridFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); begin + ValidateControls(Sender); UpdateLineCharPanel; end; -{** - DataGrid: invoke update or insert routine -} -function TMainForm.DataGridPostUpdateOrInsert(Node: PVirtualNode): Boolean; -begin - Result := True; - if not Assigned(Node) then - Exit; - if Cardinal(High(DataGridResult.Rows)) >= Node.Index then - case DataGridResult.Rows[Node.Index].State of - grsModified: Result := GridPostUpdate(DataGrid); - grsInserted: Result := GridPostInsert(DataGrid); - end; -end; - - -{** - DataGrid: compose and fire UPDATE query -} -function TMainForm.GridPostUpdate(Sender: TBaseVirtualTree): Boolean; -var - i: Integer; - sql, Val: String; - Row: PGridRow; -begin - sql := 'UPDATE '+mask(DataGridTable)+' SET'; - Row := @DataGridResult.Rows[Sender.FocusedNode.Index]; - for i := 0 to Length(DataGridResult.Columns) - 1 do begin - if Row.Cells[i].Modified then begin - Val := Row.Cells[i].NewText; - case DataGridResult.Columns[i].DatatypeCat of - dtcInteger, dtcReal: Val := UnformatNumber(Val); - dtcBinary, dtcSpatial: begin - if actBlobAsText.Checked then - Val := esc(Val) - else begin - CheckHex(Copy(Val, 3), 'Invalid hexadecimal string given in field "' + DataGridResult.Columns[i].Name + '".'); - if Val = '0x' then - Val := esc(''); - end; - end; - else Val := esc(Val); - end; - if Row.Cells[i].NewIsNull then Val := 'NULL'; - sql := sql + ' ' + mask(DataGridResult.Columns[i].Name) + '=' + Val + ', '; - end; - end; - // Cut trailing comma - sql := Copy(sql, 1, Length(sql)-2); - sql := sql + ' WHERE ' + GetWhereClause(Row, @DataGridResult.Columns) + ' LIMIT 1'; - try - // Send UPDATE query - Connection.Query(sql); - if Connection.RowsAffected = 0 then begin - MessageDlg('Your change did not affect any row! This can have several causes:' + CRLF + CRLF + - 'a) Your changes were silently converted by the server. For instance, if you tried to ' + - 'update an unsigned TINYINT field from its maximum value 255 to a higher value.' + CRLF + CRLF + - 'b) The server could not find the source row because it was deleted ' + - 'from outside.' + CRLF + CRLF + - 'c) The server could not find the source row because its primary key fields were modified ' + - 'from outside.', - mtInformation, [mbOK], 0); - end; - Result := True; - except - on E:EDatabaseError do begin - MessageDlg(E.Message, mtError, [mbOK], 0); - Result := False; - end; - end; - - if Result then begin - // Reselect just updated row in grid from server to ensure displaying - // correct values which were silently converted by the server - for i := 0 to Length(DataGridResult.Columns) - 1 do begin - if not Row.Cells[i].Modified then - Continue; - Row.Cells[i].Text := Row.Cells[i].NewText; - Row.Cells[i].IsNull := Row.Cells[i].NewIsNull; - end; - GridFinalizeEditing(Sender); - end; -end; - - -{** - Repaint edited node and reset state of grid row -} -procedure TMainForm.GridFinalizeEditing(Sender: TBaseVirtualTree); -var - i, c: Integer; -begin - c := Sender.FocusedNode.Index; - DataGridResult.Rows[c].State := grsDefault; - for i := 0 to Length(DataGridResult.Rows[c].Cells) - 1 do begin - DataGridResult.Rows[c].Cells[i].NewText := ''; - DataGridResult.Rows[c].Cells[i].Modified := False; - end; - Sender.RepaintNode(Sender.FocusedNode); - DataGridHasChanges := False; - ValidateControls(Sender); -end; - - -{** - Compose a WHERE clause used for UPDATEs and DELETEs -} -function TMainForm.GetWhereClause(Row: PGridRow; Columns: PGridColumns): String; -var - i, j: Integer; - KeyVal: String; - KeyCols: TStringList; -begin - Result := ''; - KeyCols := GetKeyColumns; - - if KeyCols.Count = 0 then begin - // No key present - use all columns in that case - for i:=0 to SelectedTableColumns.Count-1 do - KeyCols.Add(SelectedTableColumns[i].Name); - end; - - for i := 0 to KeyCols.Count - 1 do begin - for j := 0 to Length(Columns^) - 1 do begin - if Columns^[j].Name = KeyCols[i] then - break; - end; - // Find old value of key column - KeyVal := Row.Cells[j].Text; - // Quote if needed - case DataGridResult.Columns[j].DatatypeCat of - dtcInteger, dtcReal: KeyVal := UnformatNumber(KeyVal); - dtcBinary, dtcSpatial: begin - if actBlobAsText.Checked then - KeyVal := esc(KeyVal) - else if KeyVal = '0x' then - KeyVal := esc(''); - end - else KeyVal := esc(KeyVal); - end; - - if Row.Cells[j].IsNull then KeyVal := ' IS NULL' - else KeyVal := '=' + KeyVal; - Result := Result + mask(KeyCols[i]) + KeyVal + ' AND '; - end; - // Cut trailing AND - Result := Copy(Result, 1, Length(Result)-5); -end; - - -{** - Find key columns for a WHERE clause by analysing a SHOW KEYS FROM ... resultset -} -function TMainForm.GetKeyColumns: TStringList; -var - i, j, k: Integer; - AllowsNull: Boolean; - Key: TTableKey; - Col: TTableColumn; -begin - Result := TStringList.Create; - // Find best key for updates - // 1. round: find a primary key - for i:=0 to SelectedTableKeys.Count-1 do begin - Key := TTableKey(SelectedTableKeys[i]); - if Key.Name = 'PRIMARY' then begin - Result := Key.Columns; - Exit; - end; - end; - // no primary key available -> 2. round: find a unique key - for i:=0 to SelectedTableKeys.Count-1 do begin - Key := TTableKey(SelectedTableKeys[i]); - if Key.IndexType = UKEY then begin - // We found a UNIQUE key - better than nothing. Check if one of the key - // columns allows NULLs which makes it dangerous to use in UPDATES + DELETES. - AllowsNull := False; - for j:=0 to Key.Columns.Count-1 do begin - for k:=0 to SelectedTableColumns.Count-1 do begin - Col := TTableColumn(SelectedTableColumns[k]); - if Col.Name = Key.Columns[j] then - AllowsNull := Col.AllowNull; - if AllowsNull then break; - end; - if AllowsNull then break; - end; - if not AllowsNull then begin - Result := Key.Columns; - break; - end; - end; - end; -end; - - -{** - DataGrid: compose and fire UPDATE query -} -procedure TMainForm.DataGridInsertRow(CopyValuesFromNode: PVirtualNode); -var - i, j: Integer; - OldRow: TGridRow; -begin - // Scroll to the bottom to ensure we append the new row at the very last DataGridResult chunk - DataGrid.FocusedNode := DataGrid.GetLast; - DataGrid.Repaint; - // Steeling focus now to invoke posting a pending row update - DataGrid.FocusedNode := nil; - i := Length(DataGridResult.Rows); - SetLength(DataGridResult.Rows, i+1); - SetLength(DataGridResult.Rows[i].Cells, Length(DataGridResult.Columns)); - DataGridResult.Rows[i].State := grsInserted; - for j:=0 to Length(DataGridResult.Rows[i].Cells)-1 do begin - DataGridResult.Rows[i].Cells[j].Text := ''; - end; - if Assigned(CopyValuesFromNode) then begin - // Copy values from source row, ensure we have whole cell data - OldRow := DataGridResult.Rows[CopyValuesFromNode.Index]; - for j:=0 to DataGrid.Header.Columns.Count-1 do begin - if not (coVisible in DataGrid.Header.Columns[j].Options) then - continue; // Ignore invisible key column - if SelectedTableColumns[j].DefaultType = cdtAutoInc then - continue; // Empty value for auto-increment column - DataGridResult.Rows[i].Cells[j].NewText := OldRow.Cells[j].Text; - DataGridResult.Rows[i].Cells[j].NewIsNull := OldRow.Cells[j].IsNull; - DataGridResult.Rows[i].Cells[j].Modified := (DataGridResult.Rows[i].Cells[j].NewText <> DataGridResult.Rows[i].Cells[j].Text) - or (DataGridResult.Rows[i].Cells[j].NewIsNull <> DataGridResult.Rows[i].Cells[j].IsNull); - end; - end; - DataGrid.FocusedNode := DataGrid.AddChild(nil); - DataGrid.ClearSelection; - DataGrid.Selected[DataGrid.FocusedNode] := True; - DataGridHasChanges := True; - ValidateControls(DataGrid); -end; - - -{** - DataGrid: compose and fire INSERT query -} -function TMainForm.GridPostInsert(Sender: TBaseVirtualTree): Boolean; -var - Row: PGridRow; - sql, Cols, Val, Vals: String; - i: Integer; - Node: PVirtualNode; -begin - Node := Sender.FocusedNode; - Row := @DataGridResult.Rows[Node.Index]; - Cols := ''; - Vals := ''; - for i := 0 to Length(DataGridResult.Columns) - 1 do begin - if Row.Cells[i].Modified then begin - Cols := Cols + mask(DataGridResult.Columns[i].Name) + ', '; - Val := Row.Cells[i].NewText; - case DataGridResult.Columns[i].DatatypeCat of - dtcInteger, dtcReal: Val := UnformatNumber(Val); - dtcBinary, dtcSpatial: begin - if actBlobAsText.Checked then - Val := esc(Val) - else begin - CheckHex(Copy(Val, 3), 'Invalid hexadecimal string given in field "' + DataGridResult.Columns[i].Name + '".'); - if Val = '0x' then - Val := esc(''); - end; - end; - else Val := esc(Val); - end; - if Row.Cells[i].NewIsNull then Val := 'NULL'; - Vals := Vals + Val + ', '; - end; - end; - if Length(Cols) = 0 then begin - // No field was manually modified, cancel the INSERT in that case - Sender.BeginUpdate; - Sender.DeleteNode(Node); - SetLength(DataGridResult.Rows, Length(DataGridResult.Rows) - 1); - Sender.EndUpdate; - DataGridHasChanges := False; - ValidateControls(Sender); - Result := True; // Important for DataGridFocusChanging to allow moving focus - end else begin - // At least one field was modified, assume this INSERT should be posted - Vals := Copy(Vals, 1, Length(Vals)-2); - Cols := Copy(Cols, 1, Length(Cols)-2); - sql := 'INSERT INTO '+mask(DataGridTable)+' ('+Cols+') VALUES ('+Vals+')'; - // Send INSERT query - try - Connection.Query(sql); - if Connection.RowsAffected = 0 then - Raise Exception.Create('Server failed to insert row.'); - Result := True; - GridFinalizeEditing(Sender); - InvalidateVT(DataGrid, VTREE_NOTLOADED_PURGECACHE, False); - except - on E:EDatabaseError do begin - MessageDlg(E.Message, mtError, [mbOK], 0); - Result := False; - end; - end; - end; -end; - - -{** - DataGrid: compose and fire DELETE query -} -function TMainForm.GridPostDelete(Sender: TBaseVirtualTree): Boolean; -var - Node, FocusAfterDelete: PVirtualNode; - Nodes: TNodeArray; - sql: String; - Affected: Int64; - i, j: Integer; - msg: String; -begin - Node := Sender.GetFirstSelected; - FocusAfterDelete := nil; - sql := 'DELETE FROM '+mask(SelectedTable.Name)+' WHERE'; - while Assigned(Node) do begin - sql := sql + ' (' + - GetWhereClause(@DataGridResult.Rows[Node.Index], @DataGridResult.Columns) + - ') OR'; - FocusAfterDelete := Node; - Node := Sender.GetNextSelected(Node); - end; - if Assigned(FocusAfterDelete) then - FocusAfterDelete := Sender.GetNext(FocusAfterDelete); - sql := Copy(sql, 1, Length(sql)-3); - sql := sql + ' LIMIT ' + IntToStr(Sender.SelectedCount); - - try - // Send DELETE query - Connection.Query(sql); - Result := True; - except - on E:EDatabaseError do begin - MessageDlg(E.Message, mtError, [mbOK], 0); - Result := False; - end; - end; - - if Result then begin - // Remove deleted row nodes out of the grid - Affected := Connection.RowsAffected; - if Affected = Sender.SelectedCount then begin - // Fine. Number of deleted rows equals the selected node count. - // In this case, just remove the selected nodes, avoid a full reload - Sender.BeginUpdate; - Nodes := Sender.GetSortedSelection(True); - for i:=High(Nodes) downto Low(Nodes) do begin - for j := Nodes[i].Index to High(DataGridResult.Rows)-1 do begin - // Move upper rows by one so the selected row gets overwritten - DataGridResult.Rows[j] := DataGridResult.Rows[j+1]; - end; - end; - SetLength(DataGridResult.Rows, Length(DataGridResult.Rows) - Sender.SelectedCount); - Sender.DeleteSelectedNodes; - if not Assigned(FocusAfterDelete) then - FocusAfterDelete := Sender.GetLast; - if Assigned(FocusAfterDelete) then - SelectNode(Sender as TVirtualStringTree, FocusAfterDelete); - Sender.EndUpdate; - end else begin - // Should never get called as we block DELETEs on tables without a unique key - InvalidateVT(DataGrid, VTREE_NOTLOADED_PURGECACHE, False); - msg := 'Warning: Consistency problem detected.' + CRLF + CRLF - + 'The last DELETE query affected ' + FormatNumber(Affected) + ' rows, when it should have touched '+FormatNumber(Sender.SelectedCount)+' row(s)!' - + CRLF + CRLF - + 'This is most likely caused by not having a primary key in the table''s definition.'; - LogSQL( msg ); - MessageDlg( msg, mtWarning, [mbOK], 0); - end; - DisplayRowCountStats; - end; -end; - - -{** - DataGrid: cancel INSERT or UPDATE mode, reset modified node data -} -procedure TMainForm.DataGridCancel(Sender: TObject); -var - i: Integer; -begin - case DataGridResult.Rows[DataGrid.FocusedNode.Index].State of - grsModified: GridFinalizeEditing(DataGrid); - grsInserted: begin - i := Length(DataGridResult.Rows); - DataGrid.DeleteNode(DataGrid.FocusedNode, False); - SetLength(DataGridResult.Rows, i-1); - // Focus+select last node if possible - actDataLastExecute(Sender); - end; - end; - DataGridHasChanges := False; - ValidateControls(Sender); -end; - - - procedure TMainForm.AnyGridKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); var @@ -7254,26 +6875,32 @@ begin VK_HOME: g.FocusedColumn := 0; VK_END: g.FocusedColumn := g.Header.Columns.Count-1; VK_RETURN: if Assigned(g.FocusedNode) then g.EditNode(g.FocusedNode, g.FocusedColumn); - VK_DOWN: if (g = DataGrid) and Assigned(g.FocusedNode) and (g.FocusedNode.Index = g.RootNodeCount-1) then - actDataInsertExecute(Sender); - VK_NEXT: if (g = DataGrid) and Assigned(g.FocusedNode) and (g.FocusedNode.Index = g.RootNodeCount-1) then - actDataShowNext.Execute; + VK_DOWN: if g.FocusedNode = g.GetLast then actDataInsertExecute(Sender); + VK_NEXT: if (g = DataGrid) and (g.FocusedNode = g.GetLast) then actDataShowNext.Execute; end; end; -procedure TMainForm.DataGridEditing(Sender: TBaseVirtualTree; Node: +procedure TMainForm.AnyGridEditing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean); begin - Allowed := DataGridEnsureFullRow(Sender as TVirtualStringTree, Node); - if Allowed then begin - // Move Esc shortcut from "Cancel row editing" to "Cancel cell editing" - actDataCancelChanges.ShortCut := 0; - actDataPostChanges.ShortCut := 0; + Allowed := False; + try + GridResult(Sender).CheckEditable; + if not AnyGridEnsureFullRow(Sender as TVirtualStringTree, Node) then + MessageDlg('Could not load full row data.', mtError, [mbOk], 0) + else begin + Allowed := True; + // Move Esc shortcut from "Cancel row editing" to "Cancel cell editing" + actDataCancelChanges.ShortCut := 0; + actDataPostChanges.ShortCut := 0; + end; + except on E:EDatabaseError do + MessageDlg('Grid editing error: '+E.Message, mtError, [mbOk], 0); end; end; -procedure TMainForm.DataGridEdited(Sender: TBaseVirtualTree; Node: +procedure TMainForm.AnyGridEdited(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); begin // Reassign Esc to "Cancel row editing" action @@ -7283,7 +6910,7 @@ begin end; end; -procedure TMainForm.DataGridEditCancelled(Sender: TBaseVirtualTree; Column: +procedure TMainForm.AnyGridEditCancelled(Sender: TBaseVirtualTree; Column: TColumnIndex); begin // Reassign Esc to "Cancel row editing" action @@ -7291,7 +6918,7 @@ begin actDataPostChanges.ShortCut := TextToShortcut('Ctrl+Enter'); end; -procedure TMainForm.DataGridCreateEditor(Sender: TBaseVirtualTree; Node: +procedure TMainForm.AnyGridCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink); var VT: TVirtualStringTree; @@ -7308,12 +6935,13 @@ var Columns: TTableColumnList; Keys: TTableKeyList; ForeignKeys: TForeignKeyList; - ForeignResults: TMySQLQuery; + ForeignResults, Results: TMySQLQuery; begin VT := Sender as TVirtualStringTree; + Results := GridResult(VT); // Find foreign key values on InnoDB table cells - for ForeignKey in SelectedTableForeignKeys do begin + if Sender = DataGrid then for ForeignKey in SelectedTableForeignKeys do begin idx := ForeignKey.Columns.IndexOf(DataGrid.Header.Columns[Column].Text); if idx > -1 then begin // Find the first text column if available and use that for displaying in the pulldown instead of using meaningless id numbers @@ -7338,7 +6966,7 @@ begin SQL := SQL + ' LIMIT 1000'; EnumEditor := TEnumEditorLink.Create(VT); - EnumEditor.DataType := DataGridResult.Columns[Column].Datatype; + EnumEditor.DataType := DataGridResult.DataType(Column).Index; EditLink := EnumEditor; if TextCol = '' then EnumEditor.ValueList := Connection.GetCol(SQL) @@ -7354,37 +6982,37 @@ begin end; end; - TypeCat := DataGridResult.Columns[Column].DatatypeCat; + TypeCat := Results.DataType(Column).Category; if Assigned(EditLink) then // Editor was created above, do nothing now else if (TypeCat = dtcText) or ((TypeCat in [dtcBinary, dtcSpatial]) and actBlobAsText.Checked) then begin InplaceEditor := TInplaceEditorLink.Create(VT); - InplaceEditor.DataType := DataGridResult.Columns[Column].Datatype; - InplaceEditor.MaxLength := DataGridResult.Columns[Column].MaxLength; + InplaceEditor.DataType := Results.DataType(Column).Index; + InplaceEditor.MaxLength := Results.MaxLength(Column); InplaceEditor.ButtonVisible := True; EditLink := InplaceEditor; end else if (TypeCat in [dtcBinary, dtcSpatial]) and prefEnableBinaryEditor then begin HexEditor := THexEditorLink.Create(VT); - HexEditor.DataType := DataGridResult.Columns[Column].Datatype; - HexEditor.MaxLength := DataGridResult.Columns[Column].MaxLength; + HexEditor.DataType := Results.DataType(Column).Index; + HexEditor.MaxLength := Results.MaxLength(Column); EditLink := HexEditor; end else if (TypeCat = dtcTemporal) and prefEnableDatetimeEditor then begin DateTimeEditor := TDateTimeEditorLink.Create(VT); - DateTimeEditor.DataType := DataGridResult.Columns[Column].Datatype; + DateTimeEditor.DataType := Results.DataType(Column).Index; EditLink := DateTimeEditor; end else if (TypeCat = dtcIntegerNamed) and prefEnableEnumEditor then begin EnumEditor := TEnumEditorLink.Create(VT); - EnumEditor.DataType := DataGridResult.Columns[Column].Datatype; - EnumEditor.ValueList := DataGridResult.Columns[Column].ValueList; + EnumEditor.DataType := Results.DataType(Column).Index; + EnumEditor.ValueList := Results.ValueList(Column); EditLink := EnumEditor; end else if (TypeCat = dtcSetNamed) and prefEnableSetEditor then begin SetEditor := TSetEditorLink.Create(VT); - SetEditor.DataType := DataGridResult.Columns[Column].Datatype; - SetEditor.ValueList := DataGridResult.Columns[Column].ValueList; + SetEditor.DataType := Results.DataType(Column).Index; + SetEditor.ValueList := Results.ValueList(Column); EditLink := SetEditor; end else begin InplaceEditor := TInplaceEditorLink.Create(VT); - InplaceEditor.DataType := DataGridResult.Columns[Column].Datatype; + InplaceEditor.DataType := Results.DataType(Column).Index; InplaceEditor.ButtonVisible := False; EditLink := InplaceEditor; end; @@ -7463,18 +7091,21 @@ procedure TMainForm.AnyGridBeforeCellPaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; CellPaintMode: TVTCellPaintMode; CellRect: TRect; var ContentRect: TRect); var - gr: TGridResult; + r: TMySQLQuery; cl: TColor; + RowNumber: PCardinal; begin if Column = -1 then Exit; - gr := GridResult(Sender); + r := GridResult(Sender); + RowNumber := Sender.GetNodeData(Node); + r.RecNo := RowNumber^; cl := clNone; if (vsSelected in Node.States) and (Node = Sender.FocusedNode) and (Column = Sender.FocusedColumn) then cl := clHighlight else if vsSelected in Node.States then cl := $00DDDDDD - else if prefEnableNullBG and gr.Rows[Node.Index].Cells[Column].IsNull then + else if prefEnableNullBG and r.IsNull(Column) then cl := prefNullBG; if cl <> clNone then begin TargetCanvas.Brush.Color := cl; @@ -7601,12 +7232,6 @@ begin end; -procedure TMainForm.QueryGridFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); -begin - ValidateControls(Sender); -end; - - procedure TMainForm.pnlQueryHelpersCanResize(Sender: TObject; var NewWidth, NewHeight: Integer; var Resize: Boolean); begin @@ -7623,19 +7248,23 @@ begin end; -procedure TMainForm.DataGridMouseUp(Sender: TObject; Button: TMouseButton; +procedure TMainForm.AnyGridMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var Grid: TVirtualStringTree; Hit: THitInfo; + Results: TMySQLQuery; begin // Detect mouse hit in grid whitespace and apply changes. Grid := Sender as TVirtualStringTree; if not Assigned(Grid.FocusedNode) then Exit; Grid.GetHitTestInfoAt(X, Y, False, Hit); - if (Hit.HitNode = nil) or (Hit.HitColumn = NoColumn) or (Hit.HitColumn = InvalidColumn) then - DataGridPostUpdateOrInsert(Grid.FocusedNode); + if (Hit.HitNode = nil) or (Hit.HitColumn = NoColumn) or (Hit.HitColumn = InvalidColumn) then begin + Results := GridResult(Grid); + if Results.Modified then + Results.SaveModifications; + end; end; @@ -8092,7 +7721,7 @@ begin end else if Control is TVirtualStringTree then begin Grid := Control as TVirtualStringTree; if Assigned(Grid.FocusedNode) then begin - DataGridEnsureFullRow(Grid, Grid.FocusedNode); + AnyGridEnsureFullRow(Grid, Grid.FocusedNode); Clipboard.AsText := Grid.Text[Grid.FocusedNode, Grid.FocusedColumn]; if (Grid = ActiveGrid) and DoCut then Grid.Text[Grid.FocusedNode, Grid.FocusedColumn] := ''; @@ -8387,7 +8016,6 @@ begin QueryTabs.Add(TQueryTab.Create); QueryTab := QueryTabs[QueryTabs.Count-1]; QueryTab.Number := i; - QueryTab.GridResult := TGridResult.Create; QueryTab.TabSheet := TTabSheet.Create(PageControlMain); QueryTab.TabSheet.PageControl := PageControlMain; @@ -8490,14 +8118,28 @@ begin QueryTab.Grid.PopupMenu := QueryGrid.PopupMenu; QueryTab.Grid.LineStyle := QueryGrid.LineStyle; QueryTab.Grid.Font.Assign(QueryGrid.Font); + QueryTab.Grid.Header.Options := QueryGrid.Header.Options; QueryTab.Grid.Header.ParentFont := QueryGrid.Header.ParentFont; + QueryTab.Grid.Header.Images := QueryGrid.Header.Images; QueryTab.Grid.WantTabs := QueryGrid.WantTabs; QueryTab.Grid.AutoScrollDelay := QueryGrid.AutoScrollDelay; + QueryTab.Grid.OnAfterCellPaint := QueryGrid.OnAfterCellPaint; + QueryTab.Grid.OnAfterPaint := QueryGrid.OnAfterPaint; QueryTab.Grid.OnBeforeCellPaint := QueryGrid.OnBeforeCellPaint; + QueryTab.Grid.OnCreateEditor := QueryGrid.OnCreateEditor; + QueryTab.Grid.OnCompareNodes := QueryGrid.OnCompareNodes; + QueryTab.Grid.OnEditCancelled := QueryGrid.OnEditCancelled; + QueryTab.Grid.OnEdited := QueryGrid.OnEdited; + QueryTab.Grid.OnEditing := QueryGrid.OnEditing; QueryTab.Grid.OnFocusChanged := QueryGrid.OnFocusChanged; + QueryTab.Grid.OnFocusChanging := QueryGrid.OnFocusChanging; + QueryTab.Grid.OnGetNodeDataSize := QueryGrid.OnGetNodeDataSize; QueryTab.Grid.OnGetText := QueryGrid.OnGetText; + QueryTab.Grid.OnHeaderClick := QueryGrid.OnHeaderClick; QueryTab.Grid.OnInitNode := QueryGrid.OnInitNode; QueryTab.Grid.OnKeyDown := QueryGrid.OnKeyDown; + QueryTab.Grid.OnMouseUp := QueryGrid.OnMouseUp; + QueryTab.Grid.OnNewText := QueryGrid.OnNewText; QueryTab.Grid.OnPaintText := QueryGrid.OnPaintText; FixVT(QueryTab.Grid, prefGridRowsLineCount); SetupSynEditors; @@ -8708,14 +8350,14 @@ begin end; -function TMainForm.GridResult(Grid: TBaseVirtualTree): TGridResult; +function TMainForm.GridResult(Grid: TBaseVirtualTree): TMySQLQuery; begin // All grids (data- and query-grids) are placed directly on a TTabSheet Result := GridResult((Grid.Parent as TTabSheet).PageIndex) end; -function TMainForm.GridResult(PageIndex: Integer): TGridResult; +function TMainForm.GridResult(PageIndex: Integer): TMySQLQuery; begin // Return the grid result for "Data" or one of the "Query" tabs. // Results are enumerated like the tabs on which they get displayed, starting at tabData @@ -8723,7 +8365,7 @@ begin if PageIndex < 0 then Result := DataGridResult else if PageIndex < QueryTabs.Count then - Result := QueryTabs[PageIndex].GridResult + Result := QueryTabs[PageIndex].Results else Result := nil; end; @@ -9068,17 +8710,16 @@ begin DefaultValues.Add(Val); end; end; - KeyColumns := GetKeyColumns; - if KeyColumns.Count > 0 then begin - WhereClause := ''; - for i:=0 to KeyColumns.Count-1 do begin - idx := ColumnNames.IndexOf(mask(KeyColumns[i])); - if idx > -1 then - WhereClause := WhereClause + mask(KeyColumns[i])+'='+DefaultValues[idx] + ' AND '; + KeyColumns := Connection.GetKeyColumns(SelectedTableColumns, SelectedTableKeys); + WhereClause := ''; + for i:=0 to KeyColumns.Count-1 do begin + idx := ColumnNames.IndexOf(mask(KeyColumns[i])); + if idx > -1 then begin + if WhereClause <> '' then + WhereClause := WhereClause + ' AND '; + WhereClause := WhereClause + mask(KeyColumns[i])+'='+DefaultValues[idx]; end; - Delete(WhereClause, Length(sql)-3, 4); - end else - WhereClause := '??? # No primary or unique key available!'; + end; if MenuItem = menuQueryHelpersGenerateInsert then begin sql := 'INSERT INTO '+mask(SelectedTable.Name)+CRLF+ @@ -9183,15 +8824,6 @@ begin end; -function TMainForm.GetBlobContent(Results: TMySQLQuery; Column: Integer): String; -begin - if actBlobAsText.Checked then - Result := Results.Col(Column) - else - Result := '0x' + Results.BinColAsHex(Column); -end; - - procedure TMainForm.vstScroll(Sender: TBaseVirtualTree; DeltaX, DeltaY: Integer); begin // A tree gets scrolled only when the mouse is over it - see FormMouseWheel diff --git a/source/mysql_connection.pas b/source/mysql_connection.pas index d244b5fa..afb25de0 100644 --- a/source/mysql_connection.pas +++ b/source/mysql_connection.pas @@ -4,7 +4,7 @@ interface uses Classes, SysUtils, windows, mysql_api, mysql_structures, SynRegExpr, Contnrs, Generics.Collections, Generics.Defaults, - DateUtils, Types, ShellApi, Math; + DateUtils, Types, ShellApi, Math, Dialogs; type { TDBObjectList and friends } @@ -41,6 +41,72 @@ type function Compare(const Left, Right: TDBObject): Integer; override; end; + // General purpose editing status flag + TEditingStatus = (esUntouched, esModified, esDeleted, esAddedUntouched, esAddedModified, esAddedDeleted); + + TColumnDefaultType = (cdtNothing, cdtText, cdtTextUpdateTS, cdtNull, cdtNullUpdateTS, cdtCurTS, cdtCurTSUpdateTS, cdtAutoInc); + + // Column object, many of them in a TObjectList + TTableColumn = class(TObject) + private + procedure SetStatus(Value: TEditingStatus); + public + Name, OldName: String; + DataType: TDatatype; + LengthSet: String; + Unsigned, AllowNull: Boolean; + DefaultType: TColumnDefaultType; + DefaultText: String; + Comment, Collation: String; + FStatus: TEditingStatus; + constructor Create; + destructor Destroy; override; + property Status: TEditingStatus read FStatus write SetStatus; + end; + PTableColumn = ^TTableColumn; + TTableColumnList = TObjectList; + + TTableKey = class(TObject) + public + Name, OldName: String; + IndexType, Algorithm: String; + Columns, SubParts: TStringList; + Modified, Added: Boolean; + constructor Create; + destructor Destroy; override; + procedure Modification(Sender: TObject); + end; + TTableKeyList = TObjectList; + + // Helper object to manage foreign keys in a TObjectList + TForeignKey = class(TObject) + public + KeyName, ReferenceTable, OnUpdate, OnDelete: String; + Columns, ForeignColumns: TStringList; + Modified, Added, KeyNameWasCustomized: Boolean; + constructor Create; + destructor Destroy; override; + end; + TForeignKeyList = TObjectList; + + TRoutineParam = class(TObject) + public + Name, Context, Datatype: String; + end; + TRoutineParamList = TObjectList; + + // Structures for in-memory changes of a TMySQLQuery + TCellData = class(TObject) + NewText, OldText: String; + NewIsNull, OldIsNull: Boolean; + Modified: Boolean; + end; + TRowData = class(TObjectList) + OldRecNo, RecNo: Int64; + Inserted: Boolean; + end; + TUpdateData = TObjectList; + // Custom exception class for any connection or database related error EDatabaseError = class(Exception); @@ -175,6 +241,7 @@ type function GetDBObjects(db: String; Refresh: Boolean=False): TDBObjectList; function DbObjectsCached(db: String): Boolean; function ParseDateTime(Str: String): TDateTime; + function GetKeyColumns(Columns: TTableColumnList; Keys: TTableKeyList): TStringList; procedure ClearDbObjects(db: String); procedure ClearAllDbObjects; property Parameters: TConnectionParameters read FParameters write FParameters; @@ -220,19 +287,37 @@ type FRecNo, FRecordCount: Int64; FColumnNames: TStringList; + FColumnOrgNames: TStringList; + FColumnTypes: Array of TDatatype; FColumnLengths: TIntegerDynArray; - FLastResult: PMYSQL_RES; + FColumnFlags: TCardinalDynArray; + FResultList: Array of PMYSQL_RES; + FCurrentResults: PMYSQL_RES; FCurrentRow: PMYSQL_ROW; + FCurrentUpdateRow: TRowData; FEof: Boolean; - FDatatypes: Array of TDatatype; FLogCategory: TMySQLLogCategory; FStoreResult: Boolean; + FColumns: TTableColumnList; + FKeys: TTableKeyList; + FForeignKeys: TForeignKeyList; + FEditingPrepared: Boolean; + FUpdateData: TUpdateData; procedure SetSQL(Value: String); procedure SetRecNo(Value: Int64); + procedure SetColumnOrgNames(Value: TStringList); + procedure PrepareEditing; + procedure CreateUpdateRow; + function DatabaseName: String; + function TableName: String; + function QuotedDbAndTableName: String; + function GetKeyColumns: TStringList; + function GetWhereClause: String; + function ColAttributes(Column: Integer): TTableColumn; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; - procedure Execute; + procedure Execute(AddResult: Boolean=False); procedure First; procedure Next; function ColumnCount: Integer; @@ -240,17 +325,33 @@ type function Col(ColumnName: String; IgnoreErrors: Boolean=False): String; overload; function BinColAsHex(Column: Integer; IgnoreErrors: Boolean=False): String; function DataType(Column: Integer): TDataType; + function MaxLength(Column: Integer): Int64; + function ValueList(Column: Integer): TStringList; function ColExists(Column: String): Boolean; function ColIsPrimaryKeyPart(Column: Integer): Boolean; + function ColIsUniqueKeyPart(Column: Integer): Boolean; + function ColIsKeyPart(Column: Integer): Boolean; function IsNull(Column: Integer): Boolean; overload; function IsNull(Column: String): Boolean; overload; function HasResult: Boolean; + procedure CheckEditable; + function DeleteRow: Boolean; + function InsertRow: Cardinal; + procedure SetCol(Column: Integer; NewText: String; Null: Boolean); + function EnsureFullRow: Boolean; + function HasFullData: Boolean; + function Modified(Column: Integer): Boolean; overload; + function Modified: Boolean; overload; + function Inserted: Boolean; + function SaveModifications: Boolean; + procedure DiscardModifications; property RecNo: Int64 read FRecNo write SetRecNo; property Eof: Boolean read FEof; property RecordCount: Int64 read FRecordCount; property ColumnNames: TStringList read FColumnNames; property LogCategory: TMySQLLogCategory read FLogCategory write FLogCategory; property StoreResult: Boolean read FStoreResult write FStoreResult; + property ColumnOrgNames: TStringList read FColumnOrgNames write SetColumnOrgNames; published property SQL: String read FSQL write SetSQL; property Connection: TMySQLConnection read FConnection write FConnection; @@ -259,6 +360,9 @@ type implementation +uses helpers; + + { TConnectionParameters } @@ -1346,6 +1450,45 @@ begin end; +function TMySQLConnection.GetKeyColumns(Columns: TTableColumnList; Keys: TTableKeyList): TStringList; +var + i: Integer; + AllowsNull: Boolean; + Key: TTableKey; + Col: TTableColumn; +begin + Result := TStringList.Create; + // Find best key for updates + // 1. round: find a primary key + for Key in Keys do begin + if Key.Name = 'PRIMARY' then + Result.Assign(Key.Columns); + end; + if Result.Count = 0 then begin + // no primary key available -> 2. round: find a unique key + for Key in Keys do begin + if Key.IndexType = UKEY then begin + // We found a UNIQUE key - better than nothing. Check if one of the key + // columns allows NULLs which makes it dangerous to use in UPDATES + DELETES. + AllowsNull := False; + for i:=0 to Key.Columns.Count-1 do begin + for Col in Columns do begin + if Col.Name = Key.Columns[i] then + AllowsNull := Col.AllowNull; + if AllowsNull then break; + end; + if AllowsNull then break; + end; + if not AllowsNull then begin + Result.Assign(Key.Columns); + break; + end; + end; + end; + end; +end; + + { TMySQLQuery } @@ -1356,16 +1499,26 @@ begin FRecordCount := 0; FColumnNames := TStringList.Create; FColumnNames.CaseSensitive := True; + FColumnOrgNames := TStringList.Create; + FColumnOrgNames.CaseSensitive := True; FStoreResult := True; FLogCategory := lcSQL; end; destructor TMySQLQuery.Destroy; +var + i: Integer; begin FreeAndNil(FColumnNames); - if HasResult then - mysql_free_result(FLastResult); + FreeAndNil(FColumnOrgNames); + FreeAndNil(FColumns); + FreeAndNil(FKeys); + SetLength(FColumnFlags, 0); + SetLength(FColumnLengths, 0); + if HasResult then for i:=Low(FResultList) to High(FResultList) do + mysql_free_result(FResultList[i]); + SetLength(FResultList, 0); inherited Destroy; end; @@ -1376,55 +1529,86 @@ begin end; -procedure TMySQLQuery.Execute; +procedure TMySQLQuery.Execute(AddResult: Boolean=False); var i, j, NumFields: Integer; + NumResults: Int64; Field: PMYSQL_FIELD; IsBinary: Boolean; + FLastResult: PMYSQL_RES; begin FLastResult := Connection.Query(FSQL, FStoreResult, FLogCategory); - FRecordCount := Connection.RowsFound; - if HasResult then begin - NumFields := mysql_num_fields(FLastResult); - SetLength(FDatatypes, NumFields); - SetLength(FColumnLengths, NumFields); - FColumnNames.Clear; - for i:=0 to NumFields-1 do begin - Field := mysql_fetch_field_direct(FLastResult, i); - FColumnNames.Add(Utf8ToString(Field.name)); - - FDatatypes[i] := Datatypes[Low(Datatypes)]; - if (Field.flags and ENUM_FLAG) = ENUM_FLAG then - FDatatypes[i] := Datatypes[Integer(dtEnum)] - else if (Field.flags and SET_FLAG) = SET_FLAG then - FDatatypes[i] := Datatypes[Integer(dtSet)] - else for j:=Low(Datatypes) to High(Datatypes) do begin - if Field._type = Datatypes[j].NativeType then begin - if Datatypes[j].Index in [dtTinytext, dtText, dtMediumtext, dtLongtext] then begin - // Text and Blob types share the same constants (see FIELD_TYPEs in mysql_api) - // Some function results return binary collation up to the latest versions. Work around - // that by checking if this field is a real table field - // See http://bugs.mysql.com/bug.php?id=10201 - if Connection.IsUnicode then - IsBinary := (Field.charsetnr = COLLATION_BINARY) and (Field.org_table <> '') - else - IsBinary := (Field.flags and BINARY_FLAG) = BINARY_FLAG; - if IsBinary then - continue; + if AddResult and (Length(FResultList) = 0) then + AddResult := False; + if AddResult then + NumResults := Length(FResultList)+1 + else begin + for i:=Low(FResultList) to High(FResultList) do + mysql_free_result(FResultList[i]); + NumResults := 1; + FRecordCount := 0; + FEditingPrepared := False; + end; + if FLastResult <> nil then begin + Connection.Log(lcDebug, 'Result #'+IntToStr(NumResults)+' fetched.'); + SetLength(FResultList, NumResults); + FResultList[NumResults-1] := FLastResult; + FRecordCount := FRecordCount + FLastResult.row_count; + end; + if not AddResult then begin + if HasResult then begin + NumFields := mysql_num_fields(FLastResult); + SetLength(FColumnTypes, NumFields); + SetLength(FColumnLengths, NumFields); + SetLength(FColumnFlags, NumFields); + FColumnNames.Clear; + FColumnOrgNames.Clear; + for i:=0 to NumFields-1 do begin + Field := mysql_fetch_field_direct(FLastResult, i); + FColumnNames.Add(Utf8ToString(Field.name)); + FColumnOrgNames.Add(Utf8ToString(Field.org_name)); + FColumnFlags[i] := Field.flags; + FColumnTypes[i] := Datatypes[Low(Datatypes)]; + if (Field.flags and ENUM_FLAG) = ENUM_FLAG then + FColumnTypes[i] := Datatypes[Integer(dtEnum)] + else if (Field.flags and SET_FLAG) = SET_FLAG then + FColumnTypes[i] := Datatypes[Integer(dtSet)] + else for j:=Low(Datatypes) to High(Datatypes) do begin + if Field._type = Datatypes[j].NativeType then begin + if Datatypes[j].Index in [dtTinytext, dtText, dtMediumtext, dtLongtext] then begin + // Text and Blob types share the same constants (see FIELD_TYPEs in mysql_api) + // Some function results return binary collation up to the latest versions. Work around + // that by checking if this field is a real table field + // See http://bugs.mysql.com/bug.php?id=10201 + if Connection.IsUnicode then + IsBinary := (Field.charsetnr = COLLATION_BINARY) and (Field.org_table <> '') + else + IsBinary := (Field.flags and BINARY_FLAG) = BINARY_FLAG; + if IsBinary then + continue; + end; + FColumnTypes[i] := Datatypes[j]; + break; end; - FDatatypes[i] := Datatypes[j]; - break; end; end; + RecNo := 0; + end else begin + SetLength(FColumnTypes, 0); + SetLength(FColumnLengths, 0); + SetLength(FColumnFlags, 0); end; - RecNo := 0; - end else begin - SetLength(FDatatypes, 0); - SetLength(FColumnLengths, 0); end; end; +procedure TMySQLQuery.SetColumnOrgNames(Value: TStringList); +begin + // Retrieve original column names from caller + FColumnOrgNames.Text := Value.Text; +end; + + procedure TMySQLQuery.First; begin RecNo := 0; @@ -1440,21 +1624,51 @@ end; procedure TMySQLQuery.SetRecNo(Value: Int64); var LengthPointer: PLongInt; - i: Integer; + i, j: Integer; + NumRows: Int64; + Row: TRowData; + RowFound: Boolean; begin - if Value >= RecordCount then begin + if (not FEditingPrepared) and (Value >= RecordCount) then begin FRecNo := RecordCount; FEof := True; end else begin - if FRecNo+1 <> Value then - mysql_data_seek(FLastResult, Value); + + // Find row in edited data + RowFound := False; + if FEditingPrepared then begin + for Row in FUpdateData do begin + if Row.RecNo = Value then begin + FCurrentRow := nil; + FCurrentUpdateRow := Row; + RowFound := True; + break; + end; + end; + end; + + // Row not edited data - find it in normal result + if not RowFound then begin + NumRows := 0; + for i:=Low(FResultList) to High(FResultList) do begin + Inc(NumRows, FResultList[i].row_count); + if NumRows > Value then begin + FCurrentResults := FResultList[i]; + if FRecNo+1 <> Value then + mysql_data_seek(FCurrentResults, FCurrentResults.row_count-(NumRows-Value)); + FCurrentRow := mysql_fetch_row(FCurrentResults); + FCurrentUpdateRow := nil; + // Remember length of column contents. Important for Col() so contents of cells with #0 chars are not cut off + LengthPointer := mysql_fetch_lengths(FCurrentResults); + for j:=Low(FColumnLengths) to High(FColumnLengths) do + FColumnLengths[j] := PInteger(Integer(LengthPointer) + j * SizeOf(Integer))^; + break; + end; + end; + end; + FRecNo := Value; FEof := False; - FCurrentRow := mysql_fetch_row(FLastResult); - // Remember length of column contents. Important for Col() so contents of cells with #0 chars are not cut off - LengthPointer := mysql_fetch_lengths(FLastResult); - for i:=Low(FColumnLengths) to High(FColumnLengths) do - FColumnLengths[i] := PInteger(Integer(LengthPointer) + i * SizeOf(Integer))^; end; end; @@ -1470,11 +1684,17 @@ var AnsiStr: AnsiString; begin if (Column > -1) and (Column < ColumnCount) then begin - SetString(AnsiStr, FCurrentRow[Column], FColumnLengths[Column]); - if Connection.IsUnicode then - Result := UTF8ToString(AnsiStr) - else - Result := String(AnsiStr); + if FEditingPrepared and Assigned(FCurrentUpdateRow) then begin + // Row was edited and only valid in a TRowData + Result := FCurrentUpdateRow[Column].NewText; + end else begin + // The normal case: Fetch cell from mysql result + SetString(AnsiStr, FCurrentRow[Column], FColumnLengths[Column]); + if Connection.IsUnicode then + Result := UTF8ToString(AnsiStr) + else + Result := String(AnsiStr); + end; end else if not IgnoreErrors then Raise EDatabaseError.CreateFmt('Column #%d not available. Query returned %d columns and %d rows.', [Column, ColumnCount, RecordCount]); end; @@ -1494,25 +1714,70 @@ end; function TMySQLQuery.BinColAsHex(Column: Integer; IgnoreErrors: Boolean=False): String; var - LengthPointer: PLongInt; BinLen: Integer; + Ansi: AnsiString; begin // Return a binary column value as hex AnsiString - if (Column > -1) and (Column < ColumnCount) then begin - LengthPointer := mysql_fetch_lengths(FLastResult); - if LengthPointer <> nil then begin - BinLen := PInteger(Integer(LengthPointer) + Column * SizeOf(Integer))^; - SetLength(Result, BinLen*2); - BinToHex(FCurrentRow[Column], PChar(Result), BinLen); - end; - end else if not IgnoreErrors then - Raise EDatabaseError.CreateFmt('Column #%d not available. Query returned %d columns and %d rows.', [Column, ColumnCount, RecordCount]); + Result := Col(Column, IgnoreErrors); + Ansi := AnsiString(Result); + BinLen := FColumnLengths[Column]; + SetLength(Result, BinLen*2); + BinToHex(PAnsiChar(Ansi), PChar(Result), BinLen); end; function TMySQLQuery.DataType(Column: Integer): TDataType; begin - Result := FDatatypes[Column]; + Result := FColumnTypes[Column]; +end; + + +function TMySQLQuery.MaxLength(Column: Integer): Int64; +var + ColAttr: TTableColumn; +begin + // Return maximum posible length of values in given columns + // Note: PMYSQL_FIELD.max_length holds the maximum existing value in that column, which is useless here + Result := MaxInt; + ColAttr := ColAttributes(Column); + if Assigned(ColAttr) then begin + case ColAttr.DataType.Index of + dtChar, dtVarchar, dtBinary, dtVarBinary: Result := MakeInt(ColAttr.LengthSet); + dtTinyText, dtTinyBlob: Result := 255; + dtText, dtBlob: Result := 65535; + dtMediumText, dtMediumBlob: Result := 16777215; + dtLongText, dtLongBlob: Result := 4294967295; + end; + end; +end; + + +function TMySQLQuery.ValueList(Column: Integer): TStringList; +var + ColAttr: TTableColumn; +begin + Result := TStringList.Create; + Result.QuoteChar := ''''; + Result.Delimiter := ','; + ColAttr := ColAttributes(Column); + if Assigned(ColAttr) and (ColAttr.DataType.Index in [dtEnum, dtSet]) then + Result.DelimitedText := ColAttr.LengthSet; +end; + + +function TMySQLQuery.ColAttributes(Column: Integer): TTableColumn; +var + i: Integer; +begin + Result := nil; + if FEditingPrepared then begin + for i:=0 to FColumns.Count do begin + if FColumns[i].Name = FColumnNames[Column] then begin + Result := FColumns[i]; + break; + end; + end; + end; end; @@ -1523,20 +1788,29 @@ end; function TMySQLQuery.ColIsPrimaryKeyPart(Column: Integer): Boolean; -var - Field: PMYSQL_FIELD; begin - if HasResult and (Column < ColumnCount) then begin - Field := mysql_fetch_field_direct(FLastResult, Column); - Result := (Field.flags and PRI_KEY_FLAG) = PRI_KEY_FLAG; - end else - Result := False; + Result := (FColumnFlags[Column] and PRI_KEY_FLAG) = PRI_KEY_FLAG; +end; + + +function TMySQLQuery.ColIsUniqueKeyPart(Column: Integer): Boolean; +begin + Result := (FColumnFlags[Column] and UNIQUE_KEY_FLAG) = UNIQUE_KEY_FLAG; +end; + + +function TMySQLQuery.ColIsKeyPart(Column: Integer): Boolean; +begin + Result := (FColumnFlags[Column] and MULTIPLE_KEY_FLAG) = MULTIPLE_KEY_FLAG; end; function TMySQLQuery.IsNull(Column: Integer): Boolean; begin - Result := FCurrentRow[Column] = nil; + if FEditingPrepared and Assigned(FCurrentUpdateRow) then + Result := FCurrentUpdateRow[Column].NewIsNull + else + Result := FCurrentRow[Column] = nil; end; @@ -1548,7 +1822,433 @@ end; function TMySQLQuery.HasResult: Boolean; begin - Result := FLastResult <> nil; + Result := Length(FResultList) > 0; +end; + + +procedure TMySQLQuery.PrepareEditing; +var + CreateTable: String; +begin + // Try to fetch column names and keys + if FEditingPrepared then + Exit; + CreateTable := Connection.GetVar('SHOW CREATE TABLE ' + QuotedDbAndTableName, 1); + FColumns := TTableColumnList.Create; + FKeys := TTableKeyList.Create; + FForeignKeys := TForeignKeyList.Create; + ParseTableStructure(CreateTable, FColumns, FKeys, FForeignKeys); + FUpdateData := TUpdateData.Create(True); + FEditingPrepared := True; +end; + + +function TMySQLQuery.DeleteRow: Boolean; +var + sql: String; + IsVirtual: Boolean; +begin + // Delete current row from result + PrepareEditing; + IsVirtual := Assigned(FCurrentUpdateRow) and FCurrentUpdateRow.Inserted; + if not IsVirtual then begin + sql := 'DELETE FROM ' + QuotedDbAndTableName + ' WHERE ' + GetWhereClause + ' LIMIT 1'; + Connection.Query(sql); + end; + if Assigned(FCurrentUpdateRow) then begin + FUpdateData.Remove(FCurrentUpdateRow); + FCurrentUpdateRow := nil; + end; + Result := True; +end; + + +function TMySQLQuery.InsertRow: Cardinal; +var + Row, OtherRow: TRowData; + c: TCellData; + i: Integer; + ColAttr: TTableColumn; + InUse: Boolean; +begin + // Add new row and return row number + PrepareEditing; + Row := TRowData.Create(True); + for i:=0 to ColumnCount-1 do begin + c := TCellData.Create; + Row.Add(c); + c.OldText := ''; + c.OldIsNull := False; + ColAttr := ColAttributes(i); + if Assigned(ColAttr) then begin + c.OldIsNull := ColAttr.DefaultType in [cdtNull, cdtNullUpdateTS, cdtAutoInc]; + if ColAttr.DefaultType in [cdtText, cdtTextUpdateTS] then + c.OldText := ColAttr.DefaultText; + end; + c.NewText := c.OldText; + c.NewIsNull := c.OldIsNull; + c.Modified := False; + end; + Row.Inserted := True; + // Find highest unused recno of inserted rows and use that for this row + Result := High(Cardinal); + while True do begin + InUse := False; + for OtherRow in FUpdateData do begin + InUse := OtherRow.RecNo = Result; + if InUse then break; + end; + if not InUse then break; + Dec(Result); + end; + Row.RecNo := Result; + FUpdateData.Add(Row); +end; + + +procedure TMySQLQuery.SetCol(Column: Integer; NewText: String; Null: Boolean); +begin + PrepareEditing; + if not Assigned(FCurrentUpdateRow) then begin + CreateUpdateRow; + EnsureFullRow; + end; + FCurrentUpdateRow[Column].NewText := NewText; + FCurrentUpdateRow[Column].NewIsNull := Null; + FCurrentUpdateRow[Column].Modified := True; +end; + + +procedure TMySQLQuery.CreateUpdateRow; +var + i: Integer; + c: TCellData; + Row: TRowData; +begin + Row := TRowData.Create(True); + for i:=0 to ColumnCount-1 do begin + c := TCellData.Create; + Row.Add(c); + c.OldText := Col(i); + c.NewText := c.OldText; + c.OldIsNull := IsNull(i); + c.NewIsNull := c.OldIsNull; + c.Modified := False; + end; + Row.Inserted := False; + Row.RecNo := RecNo; + FCurrentUpdateRow := Row; + FUpdateData.Add(FCurrentUpdateRow); +end; + + +function TMySQLQuery.EnsureFullRow: Boolean; +var + i: Integer; + sql: String; + Data: TMySQLQuery; +begin + // Load full column values + Result := True; + if not HasFullData then try + PrepareEditing; + for i:=0 to FColumnOrgNames.Count-1 do begin + if sql <> '' then + sql := sql + ', '; + sql := sql + Connection.QuoteIdent(FColumnOrgNames[i]); + end; + Data := Connection.GetResults('SELECT '+sql+' FROM '+QuotedDbAndTableName+' WHERE '+GetWhereClause); + if not Assigned(FCurrentUpdateRow) then + CreateUpdateRow; + for i:=0 to Data.ColumnCount-1 do begin + FCurrentUpdateRow[i].OldText := Data.Col(i); + FCurrentUpdateRow[i].NewText := FCurrentUpdateRow[i].OldText; + FCurrentUpdateRow[i].OldIsNull := Data.IsNull(i); + FCurrentUpdateRow[i].NewIsNull := FCurrentUpdateRow[i].OldIsNull; + end; + Data.Free; + except on E:EDatabaseError do + Result := False; + end; +end; + + +function TMySQLQuery.HasFullData: Boolean; +var + Val: String; + i: Integer; +begin + Result := True; + for i:=0 to ColumnCount-1 do begin + if not (Datatype(i).Category in [dtcText, dtcBinary]) then + continue; + Val := Col(i); + if Length(Val) = GRIDMAXDATA then begin + Result := False; + break; + end; + end; +end; + + +function TMySQLQuery.SaveModifications: Boolean; +var + i: Integer; + Row: TRowData; + Cell: TCellData; + sqlUpdate, sqlInsertColumns, sqlInsertValues, Val: String; + RowModified: Boolean; + ColAttr: TTableColumn; +begin + Result := True; + if not FEditingPrepared then + raise EDatabaseError.Create('Internal error: Cannot post modifications before editing was prepared.'); + + for Row in FUpdateData do begin + // Prepare update and insert queries + RecNo := Row.RecNo; + sqlUpdate := ''; + sqlInsertColumns := ''; + sqlInsertValues := ''; + RowModified := False; + for i:=0 to ColumnCount-1 do begin + Cell := Row[i]; + if not Cell.Modified then + continue; + RowModified := True; + if sqlUpdate <> '' then begin + sqlUpdate := sqlUpdate + ', '; + sqlInsertColumns := sqlInsertColumns + ', '; + sqlInsertValues := sqlInsertValues + ', '; + end; + if Cell.NewIsNull then + Val := 'NULL' + else case Datatype(i).Category of + dtcInteger, dtcReal: Val := UnformatNumber(Cell.NewText); + else Val := Connection.EscapeString(Cell.NewText); + end; + sqlUpdate := sqlUpdate + Connection.QuoteIdent(FColumnOrgNames[i]) + '=' + Val; + sqlInsertColumns := sqlInsertColumns + Connection.QuoteIdent(FColumnOrgNames[i]); + sqlInsertValues := sqlInsertValues + Val; + end; + + // Post query and fetch just inserted auto-increment id if applicable + if RowModified then try + if Row.Inserted then begin + Connection.Query('INSERT INTO '+QuotedDbAndTableName+' ('+sqlInsertColumns+') VALUES ('+sqlInsertValues+')'); + for i:=0 to ColumnCount-1 do begin + ColAttr := ColAttributes(i); + if Assigned(ColAttr) and (ColAttr.DefaultType = cdtAutoInc) then begin + Row[i].NewText := Connection.GetVar('SELECT LAST_INSERT_ID()'); + Row[i].NewIsNull := False; + break; + end; + end; + end else + Connection.Query('UPDATE '+QuotedDbAndTableName+' SET '+sqlUpdate+' WHERE '+GetWhereClause); + // TODO: Reload real row data from server if keys allow that??? + except + on E:EDatabaseError do begin + Result := False; + MessageDlg(E.Message, mtError, [mbOK], 0); + end; + end; + + // Reset modification flags + if Result then begin + for i:=0 to ColumnCount-1 do begin + Cell := Row[i]; + Cell.OldText := Cell.NewText; + Cell.OldIsNull := Cell.NewIsNull; + Cell.Modified := False; + end; + Row.Inserted := False; + end; + + end; +end; + + +procedure TMySQLQuery.DiscardModifications; +var + x: Integer; + c: TCellData; +begin + if FEditingPrepared and Assigned(FCurrentUpdateRow) then begin + if FCurrentUpdateRow.Inserted then + FUpdateData.Remove(FCurrentUpdateRow) + else for x:=0 to FCurrentUpdateRow.Count-1 do begin + c := FCurrentUpdateRow[x]; + c.NewText := c.OldText; + c.NewIsNull := c.OldIsNull; + c.Modified := False; + end; + end; +end; + + +function TMySQLQuery.Modified(Column: Integer): Boolean; +begin + Result := False; + if FEditingPrepared and Assigned(FCurrentUpdateRow) then try + Result := FCurrentUpdateRow[Column].Modified; + except + connection.Log(lcdebug, inttostr(column)); + raise; + end; +end; + + +function TMySQLQuery.Modified: Boolean; +var + x, y: Integer; +begin + Result := False; + if FEditingPrepared then for y:=0 to FUpdateData.Count-1 do begin + for x:=0 to FUpdateData[y].Count-1 do begin + Result := FUpdateData[y][x].Modified; + if Result then + break; + end; + if Result then + break; + end; +end; + + +function TMySQLQuery.Inserted: Boolean; +begin + // Check if current row was inserted and not yet posted to the server + Result := False; + if FEditingPrepared and Assigned(FCurrentUpdateRow) then + Result := FCurrentUpdateRow.Inserted; +end; + + +function TMySQLQuery.DatabaseName: String; +var + Field: PMYSQL_FIELD; + i: Integer; +begin + for i:=0 to ColumnCount-1 do begin + Field := mysql_fetch_field_direct(FCurrentResults, i); + if Field.db <> '' then begin + if Connection.IsUnicode then + Result := UTF8ToString(Field.db) + else + Result := String(Field.db); + break; + end; + end; +end; + + +function TMySQLQuery.TableName: String; +var + Field: PMYSQL_FIELD; + i: Integer; + tbl, db: AnsiString; +begin + for i:=0 to ColumnCount-1 do begin + Field := mysql_fetch_field_direct(FCurrentResults, i); + if (Field.org_table <> '') and (tbl <> '') and ((tbl <> Field.org_table) or (db <> Field.db)) then + raise EDatabaseError.Create('More than one table involved.'); + if Field.org_table <> '' then begin + tbl := Field.org_table; + db := Field.db; + end; + end; + if tbl = '' then + raise EDatabaseError.Create('Could not determine name of table.') + else begin + if Connection.IsUnicode then + Result := UTF8ToString(tbl) + else + Result := String(tbl); + end; +end; + + +function TMySQLQuery.QuotedDbAndTableName: String; +begin + // Return `db`.`table` if necessairy, otherwise `table` + if Connection.Database <> DatabaseName then + Result := Connection.QuoteIdent(DatabaseName)+'.'; + Result := Result + Connection.QuoteIdent(TableName); +end; + + +function TMySQLQuery.GetKeyColumns: TStringList; +var + NeededCols: TStringList; + i: Integer; +begin + PrepareEditing; + NeededCols := Connection.GetKeyColumns(FColumns, FKeys); + if NeededCols.Count = 0 then begin + // No good key found. Just expect all columns to be present. + for i:=0 to FColumns.Count-1 do + NeededCols.Add(FColumns[i].Name); + end; + + Result := TStringList.Create; + for i:=0 to NeededCols.Count-1 do begin + if FColumnOrgNames.IndexOf(NeededCols[i]) > -1 then begin + Result.Add(NeededCols[i]); + continue; + end; + end; +end; + + +procedure TMySQLQuery.CheckEditable; +var + i: Integer; +begin + if GetKeyColumns.Count = 0 then + raise EDatabaseError.Create(MSG_NOGRIDEDITING); + // All column names must be present in order to send valid INSERT/UPDATE/DELETE queries + for i:=0 to FColumnOrgNames.Count-1 do begin + if FColumnOrgNames[i] = '' then + raise EDatabaseError.Create('Column #'+IntToStr(i)+' has an undefined origin: '+ColumnNames[i]); + end; +end; + + +function TMySQLQuery.GetWhereClause: String; +var + i, j: Integer; + NeededCols: TStringList; + ColVal: String; + ColIsNull: Boolean; +begin + // Compose WHERE clause including values from best key for editing + NeededCols := GetKeyColumns; + + for i:=0 to NeededCols.Count-1 do begin + j := FColumnOrgNames.IndexOf(NeededCols[i]); + if j = -1 then + raise EDatabaseError.Create('Cannot compose WHERE clause - column missing: '+NeededCols[i]); + if Result <> '' then + Result := Result + ' AND'; + Result := Result + ' ' + Connection.QuoteIdent(FColumnOrgNames[j]); + if Modified(j) then begin + ColVal := FCurrentUpdateRow[j].OldText; + ColIsNull := FCurrentUpdateRow[j].OldIsNull; + end else begin + ColVal := Col(j); + ColIsNull := IsNull(j); + end; + + if ColIsNull then + Result := Result + ' IS NULL' + else begin + case DataType(j).Category of + dtcInteger, dtcReal: Result := Result + '=' + UnformatNumber(ColVal); + else Result := Result + '=' + Connection.EscapeString(ColVal); + end; + end; + end; end; @@ -1617,4 +2317,72 @@ begin end; + +{ *** TTableColumn } + +constructor TTableColumn.Create; +begin + inherited Create; +end; + +destructor TTableColumn.Destroy; +begin + inherited Destroy; +end; + +procedure TTableColumn.SetStatus(Value: TEditingStatus); +begin + // Set editing flag and enable "Save" button + if (FStatus in [esAddedUntouched, esAddedModified]) and (Value = esModified) then + Value := esAddedModified + else if (FStatus in [esAddedUntouched, esAddedModified]) and (Value = esDeleted) then + Value := esAddedDeleted; + FStatus := Value; +end; + + + +{ *** TTableKey } + +constructor TTableKey.Create; +begin + inherited Create; + Columns := TStringList.Create; + SubParts := TStringList.Create; + Columns.OnChange := Modification; + Subparts.OnChange := Modification; +end; + +destructor TTableKey.Destroy; +begin + FreeAndNil(Columns); + FreeAndNil(SubParts); + inherited Destroy; +end; + +procedure TTableKey.Modification(Sender: TObject); +begin + if not Added then + Modified := True; +end; + + +{ *** TForeignKey } + +constructor TForeignKey.Create; +begin + inherited Create; + Columns := TStringList.Create; + ForeignColumns := TStringList.Create; +end; + +destructor TForeignKey.Destroy; +begin + FreeAndNil(Columns); + FreeAndNil(ForeignColumns); + inherited Destroy; +end; + + + end. diff --git a/source/table_editor.pas b/source/table_editor.pas index ae5cce7b..fc941a19 100644 --- a/source/table_editor.pas +++ b/source/table_editor.pas @@ -800,6 +800,7 @@ begin NewNode := listColumns.InsertNode(fn, amInsertAfter, @NewCol); NewCol.Status := esAddedUntouched; SelectNode(listColumns, NewNode); + Modification(Sender); ValidateColumnControls; listColumns.EditNode(NewNode, 1); end; @@ -835,6 +836,7 @@ begin NodeFocus := listColumns.GetLast; if Assigned(NodeFocus) then SelectNode(listColumns, NodeFocus.Index); + Modification(Sender); ValidateColumnControls; end; @@ -1159,6 +1161,7 @@ begin 8: Col.Collation := NewText; end; Col.Status := esModified; + Modification(Sender); end; @@ -1168,6 +1171,7 @@ var begin Col := Sender.GetNodeData(Node); Col.Status := esModified; + Modification(Sender); end; @@ -1211,6 +1215,7 @@ begin 4: begin Col.Unsigned := not Col.Unsigned; Col.Status := esModified; + Modification(Sender); VT.InvalidateNode(Node); end; 5: begin @@ -1221,6 +1226,7 @@ begin Col.DefaultText := ''; end; Col.Status := esModified; + Modification(Sender); VT.InvalidateNode(Node); end; else begin