unit dddToolsAdminDB; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, Grids, StdCtrls, ExtCtrls, Menus, SynMemoEx, SynCommons, mORMot, mORMotDDD, mORMotHttpClient, mORMotUI, SynMustache; type TDBFrame = class; TOnExecute = function(Sender: TDBFrame; const SQL, Content: RawUTF8): boolean of object; TDBFrame = class(TFrame) pnlRight: TPanel; pnlTop: TPanel; mmoSQL: TMemo; btnExec: TButton; drwgrdResult: TDrawGrid; spl1: TSplitter; spl2: TSplitter; btnHistory: TButton; btnCmd: TButton; pmCmd: TPopupMenu; pnlLeft: TPanel; lstTables: TListBox; pnlLeftTop: TPanel; edtLabels: TEdit; chkTables: TCheckBox; procedure lstTablesDblClick(Sender: TObject); virtual; procedure btnExecClick(Sender: TObject); virtual; procedure drwgrdResultClick(Sender: TObject); virtual; procedure btnHistoryClick(Sender: TObject); virtual; procedure btnCmdClick(Sender: TObject); virtual; procedure edtLabelsChange(Sender: TObject); protected fGridToCellRow: integer; fGridToCellVariant: variant; fJson: RawJSON; fSQL, fPreviousSQL: RawUTF8; fSQLLogFile: TFileName; function ExecSQL(const SQL: RawUTF8): RawUTF8; function OnText(Sender: TSQLTable; FieldIndex, RowIndex: Integer; var Text: string): boolean; procedure OnCommandsToGridAdd(const Item: TSynNameValueItem; Index: PtrInt); function OnGridToCell(Sender: TSQLTable; Row, Field: integer; HumanFriendly: boolean): RawJSON; procedure LogClick(Sender: TObject); procedure LogDblClick(Sender: TObject); procedure LogSearch(Sender: TObject); public DatabaseName: RawUTF8; mmoResult: TMemoEx; // initialized by code from SynMemoEx.pas Grid: TSQLTableToGrid; GridLastTableName: RawUTF8; Client: TSQLHttpClientWebsockets; Admin: IAdministratedDaemon; Tables: TStringList; AssociatedModel: TSQLModel; AssociatedServices: TInterfaceFactoryObjArray; // Add(cmdline/table,nestedobject,-1=text/0..N=nestedarray#) CommandsToGrid: TSynNameValue; TableDblClickSelect: TSynNameValue; TableDblClickOrderByIdDesc: boolean; TableDblClickOrderByIdDescCSV: string; SavePrefix: TFileName; OnBeforeExecute: TOnExecute; OnAfterExecute: TOnExecute; constructor Create(AOwner: TComponent); override; procedure EnableChkTables(const aCaption: string); procedure Open; virtual; procedure FillTables(const customcode: string); virtual; procedure AddSQL(SQL: string; AndExec: boolean); procedure SetResult(const JSON: RawUTF8); virtual; function NewCmdPopup(const c: string; NoCmdTrim: boolean): TMenuItem; destructor Destroy; override; end; TDBFrameClass = class of TDBFrame; TDBFrameDynArray = array of TDBFrame; implementation {$R *.dfm} const WRAPPER_TEMPLATE = '{{#soa.services}}'#13#10'{{#methods}}'#13#10 + '#get {{uri}}/{{methodName}}{{#hasInParams}}?{{#args}}{{#dirInput}}{{argName}}={{typeSource}}' + '{{#commaInSingle}}&{{/commaInSingle}}{{/dirInput}}{{/args}}{{/hasInParams}}'#13#10 + '{{#hasOutParams}}'#13#10' { {{#args}}{{#dirOutput}}{{jsonQuote argName}}: {{typeSource}}' + '{{#commaOutResult}},{{/commaOutResult}} {{/dirOutput}}{{/args}} }'#13#10 + '{{/hasOutParams}}{{/methods}}'#13#10'{{/soa.services}}'#13#10'{{#enumerates}}{{name}}: ' + '{{#values}}{{EnumTrim .}}={{-index0}}{{^-last}}, {{/-last}}{{/values}}'#13#10'{{/enumerates}}'; { TDBFrame } constructor TDBFrame.Create(AOwner: TComponent); begin inherited; fSQLLogFile := ChangeFileExt(ExeVersion.ProgramFileName, '.history'); mmoResult := TMemoEx.Create(self); mmoResult.Name := 'mmoResult'; mmoResult.Parent := pnlRight; mmoResult.Align := alClient; mmoResult.Font.Height := -11; mmoResult.Font.Name := 'Consolas'; mmoResult.ReadOnly := true; mmoResult.ScrollBars := ssVertical; mmoResult.Text := ''; mmoResult.RightMargin := 130; mmoResult.RightMarginVisible := true; mmoResult.OnGetLineAttr := mmoResult.JSONLineAttr; pnlLeftTop.Height := 30; Tables := TStringList.Create; TableDblClickSelect.Init(false); CommandsToGrid.Init(false); CommandsToGrid.OnAfterAdd := OnCommandsToGridAdd; end; procedure TDBFrame.Open; begin FillTables(''); edtLabelsChange(nil); mmoSQL.Text := '#help'; btnExecClick(nil); mmoSQL.Text := ''; mmoResult.Text := ''; end; procedure TDBFrame.FillTables(const customcode: string); var i: integer; aTables: TRawUTF8DynArray; begin drwgrdResult.Align := alClient; aTables := Admin.DatabaseTables(DatabaseName); Tables.Clear; Tables.BeginUpdate; try for i := 0 to high(aTables) do Tables.Add(UTF8ToString(aTables[i])); finally Tables.EndUpdate; end; end; procedure TDBFrame.lstTablesDblClick(Sender: TObject); var i: integer; table, fields, sql, orderby: string; begin i := lstTables.ItemIndex; if i < 0 then exit; table := lstTables.Items[i]; fields := string(TableDblClickSelect.Value(RawUTF8(table))); if fields='' then fields := '*' else begin i := Pos(' order by ', fields); if i > 0 then begin orderby := copy(fields, i, maxInt); Setlength(fields, i - 1); end; end; sql := 'select '+fields+' from ' + table; if orderby <> '' then sql := sql + orderby else begin if TableDblClickOrderByIdDesc or ((TableDblClickOrderByIdDescCSV <> '') and (Pos(table + ',', TableDblClickOrderByIdDescCSV + ',') > 0)) then sql := sql + ' order by id desc'; sql := sql + ' limit 1000'; end; AddSQL(sql, true); end; procedure TDBFrame.SetResult(const JSON: RawUTF8); begin FreeAndNil(Grid); drwgrdResult.Hide; mmoResult.Align := alClient; mmoResult.WordWrap := false; mmoResult.ScrollBars := ssBoth; mmoResult.RightMarginVisible := false; if (JSON = '') or (JSON[1] in ['A'..'Z', '#']) then mmoResult.OnGetLineAttr := nil else mmoResult.OnGetLineAttr := mmoResult.JSONLineAttr; mmoResult.Text := UTF8ToString(StringReplaceTabs(JSON, ' ')); mmoResult.SetCaret(0, 0); mmoResult.TopRow := 0; fJson := ''; end; procedure TDBFrame.OnCommandsToGridAdd(const Item: TSynNameValueItem; Index: PtrInt); begin pmCmd.Items.Insert(0, NewCmdPopup(UTF8ToString(Item.Name), true)); end; function TDBFrame.NewCmdPopup(const c: string; NoCmdTrim: boolean): TMenuItem; var cmd, name, lastname: string; i, ext, num: integer; res: TDocVariantData; sub, subpar, subarch: TMenuItem; begin result := TMenuItem.Create(self); if length(c) > 40 then result.Caption := copy(c, 1, 37) + '...' else result.Caption := c; if NoCmdTrim then cmd := c else begin i := Pos(' ', c); if i > 0 then cmd := copy(c, 1, i) + '*' else begin i := Pos('(', c); if i > 0 then cmd := copy(c, 1, i) + '*)' else cmd := c; end; end; result.Hint := cmd; if (cmd = '#log *') or (cmd = '#db *') then begin // log/db files in sub-menus res.InitJSON(ExecSQL(StringToUTF8(cmd)), JSON_OPTIONS_FAST); SetLength(cmd, length(cmd) - 1); subpar := result; subarch := nil; if res.Kind = dvArray then for i := 0 to res.Count - 1 do begin name := res.Values[i].Name; if name = lastname then continue; // circumvent FindFiles() bug with *.dbs including *.dbsynlz lastname := name; case GetFileNameExtIndex(name, 'dbs,dbsynlz') of 0: begin // group sharded database files by 20 in sub-menus ext := Pos('.dbs', name); if (ext > 4) and TryStrToInt(Copy(name, ext - 4, 4), num) then if (subpar = result) or (num mod 20 = 0) then begin subpar := NewCmdPopup(cmd + name + ' ...', true); subpar.OnClick := nil; result.Add(subpar); end; end; 1: begin // group database backup files in a dedicated sub-menu if subarch = nil then begin subarch := NewCmdPopup(cmd + '*.dbsynlz ...', true); subarch.OnClick := nil; result.Add(subarch); end; subpar := subarch; end; else subpar := result; end; sub := NewCmdPopup(cmd + name, true); if cmd = '#log ' then sub.Caption := sub.Caption + ' ' + res.Values[i].TimeStamp else sub.Caption := FormatString('% %', [sub.Caption, KB(res.Values[i].Size)]); subpar.Add(sub); end; end else result.OnClick := btnExecClick; end; procedure TDBFrame.btnExecClick(Sender: TObject); var res, ctyp, execTime: RawUTF8; mmo, cmd, fn, local: string; SelStart, SelLength, cmdToGrid, i: integer; table: TSQLTable; tables: TSQLRecordClassDynArray; P: PUTF8Char; exec: TServiceCustomAnswer; ctxt: variant; timer: TPrecisionTimer; begin if (Sender <> nil) and Sender.InheritsFrom(TMenuItem) then begin mmo := TMenuItem(Sender).Hint; mmoSQL.Text := mmo; i := Pos('*', mmo); if (i > 0) and (mmo[1] = '#') then begin mmoSQL.SelStart := i - 1; mmoSQL.SelLength := 1; mmoSQL.SetFocus; exit; end; end; SelStart := mmoSQL.SelStart; SelLength := mmoSQL.SelLength; if SelLength > 10 then mmo := mmoSQL.SelText else mmo := mmoSQL.Lines.Text; fSQL := Trim(StringToUTF8(mmo)); if fSQL = '' then exit; if IdemPropNameU(fSQL, '#client') then begin fJson := ObjectToJSON(Client); end else if Assigned(OnBeforeExecute) and not OnBeforeExecute(self, fSQL, '') then fJson := '"You are not allowed to execute this command for security reasons"' else begin Screen.Cursor := crHourGlass; try try timer.Start; exec := Admin.DatabaseExecute(DatabaseName, fSQL); execTime := timer.Stop; ctyp := FindIniNameValue(pointer(exec.Header), HEADER_CONTENT_TYPE_UPPER); if IdemPChar(pointer(exec.Content), '') then begin // HTML in disguise i := PosI('', exec.content); if i = 0 then fJson := exec.Content else fJson := copy(exec.Content, i, maxInt); end else if (ctyp = '') or IdemPChar(pointer(ctyp), JSON_CONTENT_TYPE_UPPER) then fJson := exec.Content else if IdemPropNameU(ctyp, BINARY_CONTENT_TYPE) then begin fn := UTF8ToString(trim(FindIniNameValue(pointer(exec.Header), 'FILENAME:'))); if (fn <> '') and (exec.Content <> '') then with TSaveDialog.Create(self) do try Options := [ofOverwritePrompt, ofHideReadOnly, ofPathMustExist, ofEnableSizing]; InitialDir := GetShellFolderPath(CSIDL_DOCUMENTS); FileName := SavePrefix + fn; if Execute then begin local := FileName; FileFromString(exec.Content, local); end; finally Free; end; fJson := JSONEncode(['file', fn, 'size', length(exec.Content), 'type', ctyp, 'localfile', local]); end else fJson := FormatUTF8('"Unknown content-type: %"', [ctyp]); except on E: Exception do fJson := ObjectToJSON(E); end; finally Screen.Cursor := crDefault; end; end; FreeAndNil(Grid); GridLastTableName := ''; fGridToCellRow := 0; cmdToGrid := CommandsToGrid.Find(fSQL); if (fSQL[1] = '#') and ((cmdToGrid < 0) or (CommandsToGrid.List[cmdToGrid].Tag < 0)) then begin if fJson <> '' then if IdemPropNameU(fSQL, '#help') then begin fJson := Trim(UnQuoteSQLString(fJson)) + '|#client'#13#10; res := StringReplaceAll(fJson, '|', #13#10' '); if pmCmd.Items.Count = 0 then begin P := pointer(res); while P <> nil do begin cmd := UTF8ToString(Trim(GetNextLine(P, P))); if (cmd <> '') and (cmd[1] = '#') then pmCmd.Items.Add(NewCmdPopup(cmd, false)); end; end; end else if IdemPropNameU(fSQL, '#wrapper') then begin _Json(fJson,ctxt,JSON_OPTIONS_FAST); res := TSynMustache.Parse(WRAPPER_TEMPLATE).Render(ctxt, nil, TSynMustache.HelpersGetStandardList, nil, true); end else begin JSONBufferReformat(pointer(fJson), res, jsonUnquotedPropName); if (res = '') or (res = 'null') then res := fJson; end; if Assigned(OnAfterExecute) then OnAfterExecute(self,fSQL,res); SetResult(res); end else begin mmoResult.Text := ''; mmoResult.SetCaret(0, 0); mmoResult.TopRow := 0; mmoResult.Align := alBottom; mmoResult.WordWrap := true; mmoResult.ScrollBars := ssVertical; mmoResult.Height := 100; if AssociatedModel <> nil then tables := AssociatedModel.Tables; if cmdToGrid >= 0 then begin GridLastTableName := CommandsToGrid.List[cmdToGrid].Name; if isSelect(pointer(GridLastTableName)) then GridLastTableName := GetTableNameFromSQLSelect(GridLastTableName,false); if CommandsToGrid.List[cmdToGrid].Value <> '' then begin // display a nested object in the grid P := JsonObjectItem(pointer(fJson), CommandsToGrid.List[cmdToGrid].Value); if CommandsToGrid.List[cmdToGrid].Tag > 0 then P := JSONArrayItem(P, CommandsToGrid.List[cmdToGrid].Tag - 1); if P <> nil then GetJSONItemAsRawJSON(P, RawJSON(fJSON)); end; end else GridLastTableName := GetTableNameFromSQLSelect(fSQL, false); table := TSQLTableJSON.CreateFromTables(tables, fSQL, pointer(fJson), length(fJson)); Grid := TSQLTableToGrid.Create(drwgrdResult, table, nil); Grid.SetAlignedByType(sftCurrency, alRight); Grid.SetFieldFixedWidth(100); Grid.FieldTitleTruncatedNotShownAsHint := true; Grid.OnValueText := OnText; Grid.Table.OnExportValue := OnGridToCell; if Assigned(OnAfterExecute) then OnAfterExecute(self, fSQL, fJSON); drwgrdResult.Options := drwgrdResult.Options - [goRowSelect]; drwgrdResult.Show; mmoResult.OnGetLineAttr := mmoResult.JSONLineAttr; mmoResult.Text := FormatString(#13#10' Returned % row(s), as % in %', [table.RowCount, KB(fJson), execTime]); end; if Sender <> nil then begin mmoSQL.SelStart := SelStart; mmoSQL.SelLength := SelLength; mmoSQL.SetFocus; end; if ((fJson <> '') or ((fSQL[1] = '#') and (PosEx(' ', fSQL) > 0))) and (fSQL <> fPreviousSQL) then begin AppendToTextFile(fSQL, fSQLLogFile); fPreviousSQL := fSQL; end; end; destructor TDBFrame.Destroy; begin FreeAndNil(Grid); FreeAndNil(AssociatedModel); FreeAndNil(Tables); inherited; end; function TDBFrame.OnText(Sender: TSQLTable; FieldIndex, RowIndex: Integer; var Text: string): boolean; begin if Sender.FieldType(FieldIndex) in [sftBoolean] then result := false else begin Text := Sender.GetString(RowIndex, FieldIndex); // display the value as such result := true; end; end; function TDBFrame.OnGridToCell(Sender: TSQLTable; Row, Field: integer; HumanFriendly: boolean): RawJSON; var methodName: RawUTF8; serv, m: integer; begin if fGridToCellRow <> Row then begin Sender.ToDocVariant(Row, fGridToCellVariant, JSON_OPTIONS_FAST, true, true, true); fGridToCellRow := Row; if AssociatedServices <> nil then with _Safe(fGridToCellVariant)^ do if GetAsRawUTF8('Method', methodName) then for serv := 0 to high(AssociatedServices) do begin m := AssociatedServices[serv].FindFullMethodIndex(methodName, true); if m >= 0 then with AssociatedServices[serv].Methods[m] do begin ArgsAsDocVariantFix(GetAsDocVariantSafe('Input')^, true); ArgsAsDocVariantFix(GetAsDocVariantSafe('Output')^, false); break; end; end; end; with _Safe(fGridToCellVariant)^ do if cardinal(Field)>=cardinal(Count) then result := '' else if HumanFriendly and (_Safe(Values[Field])^.Kind = dvUndefined) then VariantToUTF8(Values[field], RawUTF8(result)) else result := VariantSaveJSON(Values[Field]); end; procedure TDBFrame.drwgrdResultClick(Sender: TObject); var R: integer; json: RawUTF8; begin R := drwgrdResult.Row; if (R > 0) and (R <> fGridToCellRow) and (Grid <> nil) then begin OnGridToCell(Grid.Table,R,0,false); JSONBufferReformat(pointer(VariantToUTF8(fGridToCellVariant)), json, jsonUnquotedPropNameCompact); mmoResult.OnGetLineAttr := mmoResult.JSONLineAttr; mmoResult.Text := UTF8ToString(json); mmoResult.SetCaret(0, 0); mmoResult.TopRow := 0; end; end; procedure TDBFrame.btnHistoryClick(Sender: TObject); var F: TForm; List: TListBox; Search: TEdit; Details: TMemo; begin F := TForm.Create(Application); try F.Caption := ' ' + btnHistory.Hint; F.Font := Font; F.Width := 800; F.Height := 600; F.Position := poMainFormCenter; Search := TEdit.Create(F); Search.Parent := F; Search.Align := alTop; Search.Height := 24; Search.OnChange := LogSearch; Details := TMemo.Create(F); Details.Parent := F; Details.Align := alBottom; Details.Height := 200; Details.readonly := true; Details.Font.Name := 'Consolas'; List := TListBox.Create(F); with List do begin Parent := F; Align := alClient; Tag := PtrInt(Details); OnClick := LogClick; OnDblClick := LogDblClick; end; Search.Tag := PtrInt(List); LogSearch(Search); F.ShowModal; finally F.Free; end; end; procedure TDBFrame.LogClick(Sender: TObject); var List: TListBox absolute Sender; ndx: integer; begin ndx := cardinal(List.ItemIndex); if ndx >= 0 then TMemo(List.Tag).Text := copy(List.Items[ndx], 21, maxInt) else TMemo(List.Tag).Clear; end; procedure TDBFrame.LogDblClick(Sender: TObject); var List: TListBox absolute Sender; SQL: string; ndx: integer; begin ndx := cardinal(List.ItemIndex); if ndx >= 0 then begin SQL := copy(List.Items[ndx], 21, maxInt); AddSQL(SQL, IsSelect(pointer(StringToAnsi7(SQL)))); TForm(List.Owner).Close; end; end; procedure TDBFrame.LogSearch(Sender: TObject); const MAX_LINES_IN_HISTORY = 500; var Edit: TEdit absolute Sender; List: TListBox; i: integer; s: RawUTF8; begin s := SynCommons.UpperCase(StringToUTF8(Edit.Text)); List := pointer(Edit.Tag); with TMemoryMapText.Create(fSQLLogFile) do try List.Items.BeginUpdate; List.Items.Clear; for i := Count - 1 downto 0 do if (s = '') or LineContains(s, i) then if List.Items.Add(Strings[i]) > MAX_LINES_IN_HISTORY then break; // read last 500 lines from UTF-8 file finally Free; List.Items.EndUpdate; end; List.ItemIndex := 0; LogClick(List); end; procedure TDBFrame.AddSQL(SQL: string; AndExec: boolean); var len: integer; orig: string; begin SQL := SysUtils.Trim(SQL); len := Length(SQL); if len = 0 then exit; orig := mmoSQL.Lines.Text; if orig <> '' then SQL := #13#10#13#10 + SQL; SQL := orig + SQL; mmoSQL.Lines.Text := SQL; mmoSQL.SelStart := length(SQL) - len; mmoSQL.SelLength := len; if AndExec then btnExecClick(btnExec) else mmoSQL.SetFocus; end; procedure TDBFrame.btnCmdClick(Sender: TObject); begin with ClientToScreen(btnCmd.BoundsRect.TopLeft) do pmCmd.Popup(X, Y + btnCmd.Height); end; function TDBFrame.ExecSQL(const SQL: RawUTF8): RawUTF8; var exec: TServiceCustomAnswer; begin exec := Admin.DatabaseExecute(DatabaseName, sql); result := exec.Content; end; procedure TDBFrame.EnableChkTables(const aCaption: string); begin pnlLeftTop.Height := 44; chkTables.Show; chkTables.Caption := aCaption; end; procedure TDBFrame.edtLabelsChange(Sender: TObject); var i, index: integer; match, previous: string; begin i := lstTables.ItemIndex; if i >= 0 then previous := lstTables.Items[i]; index := -1; match := SysUtils.Trim(SysUtils.UpperCase(edtLabels.Text)); if (length(match) > 5) and (match[1] = '%') then begin FillTables(match); match := ''; end; with lstTables.Items do try BeginUpdate; Clear; for i := 0 to Tables.Count - 1 do if (match = '') or (Pos(match, SysUtils.UpperCase(Tables[i])) > 0) then begin AddObject(Tables[i], Tables.Objects[i]); if previous = Tables[i] then index := Count - 1; end; finally EndUpdate; end; if index >= 0 then lstTables.ItemIndex := index; end; end.