unit SynDBExplorerFrame;

{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER

interface

uses
  Windows,
  Messages,
  SysUtils,
  Classes,
  Graphics,
  Controls,
  Forms,
  Dialogs,
  StdCtrls,
  Grids,
  ExtCtrls,
  ComCtrls,
  Types,
  ShellAPI,
  Menus,
  {$ifdef ISDELPHIXE3} System.UITypes, {$endif}
  SynCommons,
  SynDB,
  mORMot,
  mORMoti18n,
  mORMotUI,
  mORMotUILogin,
  mORMotToolBar,
  mORMotReport;

type
  TDBExplorerFrame = class(TFrame)
    Splitter1: TSplitter;
    PanelClient: TPanel;
    Splitter2: TSplitter;
    DrawGrid: TDrawGrid;
    PanelTop: TPanel;
    MemoSQL: TMemo;
    BtnExec: TButton;
    BtnResultToFile: TButton;
    PopupMenuSQL: TPopupMenu;
    MenuInsertFieldName: TMenuItem;
    MenuInsertFieldValues: TMenuItem;
    BtnExecToFile: TButton;
    PagesLeft: TPageControl;
    TabTables: TTabSheet;
    TabObjects: TTabSheet;
    EditTable: TEdit;
    ListTable: TListBox;
    ImageLogo: TImage;
    BtnQueryBuilder: TButton;
    BtnTablesExport: TButton;
    BtnExecLog: TButton;
    BtnExecToTab: TButton;
    btnRunServer: TButton;
    procedure EditTableChange(Sender: TObject);
    procedure ListTableDblClick(Sender: TObject);
    procedure BtnExecClick(Sender: TObject);
    procedure ListTableMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure BtnResultToFileClick(Sender: TObject);
    procedure PopupMenuSQLPopup(Sender: TObject);
    procedure ImageLogoClick(Sender: TObject);
    procedure BtnQueryBuilderClick(Sender: TObject);
    procedure ListTableClick(Sender: TObject);
    procedure BtnTablesExportClick(Sender: TObject);
    procedure BtnExecLogClick(Sender: TObject);
    procedure ListTableKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure ListTableKeyUp(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure btnRunServerClick(Sender: TObject);
  private
    fHint: THintWindowDelayed;
    fGrid: TSQLTableToGrid;
    fJSONBuffer: RawUTF8;
    fPreviousSQL: RawUTF8;
    fSQLLogFile: TFileName;
    fListTableShiftState: TShiftState;
    function OnText(Sender: TSQLTable; FieldIndex, RowIndex: Integer; var Text: string): boolean;
    function GetTableDescription(const TableName: string): string;
    procedure OnGridDblClick(Sender: TObject);
    procedure InsertMenu(Sender: TObject);
    function GridValue(Row,Col: integer): RawUTF8;
    function GetFileNameFromCurrentSelectedTable: TFileName;
    procedure AddSQL(SQL: string; AndExec, AndExecInNewTab: boolean);
    procedure LogClick(Sender: TObject);
    procedure LogDblClick(Sender: TObject);
    procedure LogSearch(Sender: TObject);
  public
    Tables: TStringList;
    Props: TSQLDBConnectionProperties;
    constructor Create(aOwner: TComponent); override;
    destructor Destroy; override;
  end;

resourcestring
  sCaptionStats = '%d rows (%s) in %s';
  sCaptionUpdated = '%d row(s) updated in %s';
  sNothingToExport = 'No result to export';
  sExportFilter = 'Excel file (*.csv)|*.csv|Text file (*.txt)|*.txt|'+
    'JSON file (*.json)|*.json|Acrobat PDF report (*.pdf)|*.pdf';
  sHugeFileExport = 'Do you want to export so much data?';
  sExportingN = 'Exporting %s...';
  sHugeFileConfirm = 'This could take a while to create the report';
  sExecExportFilter = 'Excel file (*.csv)|*.csv|Text file (*.txt)|*.txt|'+
    'Standard JSON file (*.json)|*.json|Smaller JSON file (*.json)|*.json|'+
    'SQLite3 database file (*.db3)|*.db3|'+
    'BigTable fixed sized record (*.rec)|*.rec|'+
    'BigTable variable length record (*.rec)|*.rec';
  sTableExportFilter = 'SQLite3 database file (*.db3)|*.db3';

implementation

uses
  SynDBOracle,
  SynDBExplorerMain,
  SynDBExplorerQueryBuilder,
  SynDBExplorerExportTables,
  SynTaskDialog,
  SynTable,
  SynBigTable,
  SynDBSQLite3,
  SynDBExplorerServer;


{$R *.dfm}

function RowsToSynBigTableRecord(const Dest: TFileName; Rows: TSQLDBStatement;
  VariableLength: boolean): integer;
const
  TOFIELD: array[Boolean,TSQLDBFieldType] of TSynTableFieldType = (
    (tftUnknown,tftUnknown,tftInt64,tftDouble,tftCurrency,tftDouble,tftUTF8,tftBlobInternal),
    (tftUnknown,tftUnknown,tftVarInt64,tftDouble,tftCurrency,tftDouble,tftUTF8,tftBlobInternal));
var F, FMax: integer;
    BT: TSynBigTableRecord;
    Blob: RawByteString;
    FT: TSynTableFieldType;
    ColName: RawUTF8;
    ColRowID: integer;
    FieldsMap: array of Integer; // column order vary depending on access speed
    FieldsValue: TRawByteStringDynArray;
begin
  result := 0;
  if (Dest='') or (Rows=nil) or (Rows.ColumnCount=0) then
    exit;
  FMax := Rows.ColumnCount-1;
  DeleteFile(Dest);
  SetLength(FieldsValue,FMax+1);
  BT := TSynBigTableRecord.Create(Dest,'SynDbExported');
  try
    while Rows.Step do begin
      if result=0 then begin
        // retrieve column layout (when first row of data is available)
        ColRowID := -1; 
        for F := 0 to FMax do begin
          ColName := Rows.ColumnName(F);
          FT := TOFIELD[VariableLength,Rows.ColumnType(F)];
          if FT=tftUnknown then
            raise Exception.CreateFmt('Invalid column type %s',[ColName]);
          if IsRowID(pointer(ColName)) then begin
            ColName := 'ID__'; // force accepted column name
            ColRowID := F;
          end;
          if not BT.AddField(ColName,FT) then
            raise Exception.CreateFmt('Impossible to add column %s',[ColName]);
        end;
        BT.AddFieldUpdate;
        if BT.Table.FieldCount<>FMax+1 then
          raise Exception.Create('Invalid column layout');
        SetLength(FieldsMap,FMax+1);
        for F := 0 to FMax do begin
          ColName := BT.Table.Field[F].Name;
          if (ColRowID>=0) and (ColName='ID__') then
            FieldsMap[F] := ColRowID else
            FieldsMap[F] := Rows.ColumnIndex(ColName);
          if FieldsMap[F]<0 then
            raise Exception.CreateFmt('Invalid column type %s',[ColName]);
        end;
      end;
      // recreate each record from scratch (fast in practice)
      for F := 0 to FMax do
      with BT.Table.Field[F] do begin
        case FieldType of
        tftInt64, tftVarInt64:
          FieldsValue[F] := SBF(Rows.ColumnInt(FieldsMap[F]));
        tftDouble:
          FieldsValue[F] := SBF(Rows.ColumnDouble(FieldsMap[F]));
        tftCurrency:
          FieldsValue[F] := SBFCurr(Rows.ColumnCurrency(FieldsMap[F]));
        tftUTF8:
          FieldsValue[F] := SBF(Rows.ColumnUTF8(FieldsMap[F]));
        tftBlobInternal: begin
          Blob := Rows.ColumnBlob(FieldsMap[F]);
          FieldsValue[F] := SBF(pointer(Blob),length(Blob));
        end;
        end;
      end;
      BT.Add(RawByteStringArrayConcat(FieldsValue));
      if BT.CurrentInMemoryDataSize>$5000000 then // write on disk every 80 MB
        BT.UpdateToFile;
      inc(result);
    end;
    assert(result=BT.Count);
  finally
    BT.Free;
  end;
end;

procedure TDBExplorerFrame.EditTableChange(Sender: TObject);
var s: string;
    i: integer;
begin
  s := SysUtils.UpperCase(SysUtils.trim(EditTable.Text));
  with ListTable.Items do
  try
    BeginUpdate;
    Clear;
    for i := 0 to Tables.Count-1 do
      if (s='') or (Pos(s,SysUtils.UpperCase(Tables[i]))>0) then
        Add(Tables[i]);
  finally
    EndUpdate;
  end;
  ListTableClick(nil);
end;

procedure TDBExplorerFrame.AddSQL(SQL: string; AndExec, AndExecInNewTab: boolean);
var len: integer;
    orig: string;
begin
  fHint.Hide;
  SQL := SysUtils.Trim(SQL);
  len := Length(SQL);
  if len=0 then
    exit;
  orig := MemoSQL.Lines.Text;
  if orig<>'' then
    SQL := #13#10#13#10+SQL;
  SQL := orig+SQL;
  MemoSQL.Lines.Text := SQL;
  MemoSQL.SelStart := length(SQL)-len;
  MemoSQL.SelLength := len;
  if AndExec then
    if AndExecInNewTab then
      BtnExecClick(BtnExecToTab) else
      BtnExecClick(BtnExec) else
    MemoSQL.SetFocus;
end;

procedure TDBExplorerFrame.ListTableKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  fListTableShiftState := Shift;
end;

procedure TDBExplorerFrame.ListTableKeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  fListTableShiftState := [];
end;

procedure TDBExplorerFrame.ListTableDblClick(Sender: TObject);
var i: integer;
begin
  i := ListTable.ItemIndex;
  if i>=0 then
    AddSQL(UTF8ToString(Props.SQLSelectAll(StringToUTF8(ListTable.Items[i]),nil,[])),
      true,ssShift in fListTableShiftState);
end;

procedure ShowException(E: Exception);
var Dlg: TTaskDialog;
    stack: string;
begin
  if E=nil then
    exit;
  Dlg.Content := E.Message;
  {$ifdef UNICODE}
  stack := sLineBreak+E.StackTrace;
  {$endif}
  Dlg.Info := Format('Exception class: %s%s',[E.ClassName,stack]);
  Dlg.Execute([],0,[],tiError);
end;

procedure TDBExplorerFrame.BtnExecClick(Sender: TObject);
var SQL, Stop: RawUTF8;
    Table: TSQLTable;
    Timer: TPrecisionTimer;
    SelStart, SelLength, RowsCount, Kind, i: integer;
    Rows: ISQLDBRows;
    RowsSize: Int64;
    FN: TFileName;
    FS: TFileStream;
    Frame: TDBExplorerFrame;
begin
  FreeAndNil(fGrid);
  DrawGrid.RowCount := 0;
  SelStart := MemoSQL.SelStart;
  SelLength := MemoSQL.SelLength;
  if SelLength>10 then
    SQL := Trim(S2U(MemoSQL.SelText)) else
    SQL := Trim(S2U(MemoSQL.Lines.Text));
  for i := 1 to length(SQL) do
    if SQL[i]<' ' then
      SQL[i] := ' '; // some engines (e.g. MSSQL) don't like line feeds
  Frame := self;
  Screen.Cursor := crSQLWait;
  Timer.Start;
  try
    try
      Caption := '';
      RowsCount := 0;
      RowsSize := 0;
      if SQL<>'' then
        if isSelect(Pointer(SQL)) then begin
          try
            Rows := Props.Execute(SQL,[],nil,(Sender<>BtnExecToFile));
          except
            on Exception do
            if Props.InheritsFrom(TSQLDBSQLite3ConnectionProperties) then
              // SQLite3 engine is local -> so any failure is fatal
              raise else begin
              // DB error (loose remote connection?) -> retry once
              Props.ClearConnectionPool;
              Rows := Props.Execute(SQL,[]);
            end;
          end;
          if (Sender=BtnExec) or (Sender=nil) or (Sender=BtnExecToTab) then begin
            if Sender=BtnExecToTab then begin
              Frame := ((Owner as TSynPage).Owner as TDbExplorerMain).CreateFrame;
              Frame.MemoSQL.Lines.Text := U2S(SQL);
            end;
            with Frame do begin
              fJSONBuffer := Rows.FetchAllAsJSON(false); 
              Stop := Timer.Stop;
              Table := TSQLTableJSON.Create('',pointer(fJSONBuffer),length(fJSONBuffer));
              fGrid := TSQLTableToGrid.Create(DrawGrid,Table,nil);
              fGrid.SetAlignedByType(sftCurrency,alRight);
              fGrid.OnValueText := OnText;
              fGrid.SetFieldFixedWidth(100);
              fGrid.FieldTitleTruncatedNotShownAsHint := true;
              DrawGrid.Options := DrawGrid.Options-[goRowSelect];
              DrawGrid.OnDblClick := self.OnGridDblClick;
              RowsCount := Table.RowCount;
              RowsSize := length(fJSONBuffer);
            end;
          end else
          if Sender=BtnExecToFile then begin
            Timer.Pause;
            with TSaveDialog.Create(self) do
            try
              InitialDir := GetShellFolderPath(CSIDL_DOCUMENTS);
              Filter := sExecExportFilter; // csv,txt,json,json,record,record
              DefaultExt := '.csv';
              FilterIndex := 0;
              Title := BtnExecToFile.Hint;
              Options := [ofOverwritePrompt,ofHideReadOnly,ofPathMustExist,ofEnableSizing];
              FileName := GetFileNameFromCurrentSelectedTable;
              if not Execute then
                exit;
              Kind := FilterIndex;
              FN := FileName;
            finally
              Free;
            end;
            Timer.Resume;
            with CreateTempForm(format(sExportingN,[GetFileNameFromCurrentSelectedTable]),nil,True) do
            try
              case Kind of
                5: Rowscount := RowsToSQLite3(FN,S2U(GetFileNameFromCurrentSelectedTable),Rows.Instance,false);
                6: RowsCount := RowsToSynBigTableRecord(FN,Rows.Instance,False); // fixed length
                7: RowsCount := RowsToSynBigTableRecord(FN,Rows.Instance,True);  // variable length
              else begin
                FS := TFileStream.Create(FN,fmCreate);
                try
                  case Kind of
                  1: RowsCount := Rows.Instance.FetchAllToCSVValues(FS,False,
                    AnsiChar({$ifdef ISDELPHIXE}FormatSettings.{$endif}ListSeparator),true);
                  2: RowsCount := Rows.Instance.FetchAllToCSVValues(FS,true,#9,true);
                  3: RowsCount := Rows.Instance.FetchAllToJSON(FS,true);  // expanded=true
                  4: RowsCount := Rows.Instance.FetchAllToJSON(FS,false); // expanded=false
                  end;
                finally
                  FS.Free;
                end;
              end;
              end;
              Stop := Timer.Stop;
            finally
              Free;
            end;
            RowsSize := FileSize(FN);
          end;
          Frame.Caption := format(sCaptionStats,
            [RowsCount,Ansi7ToString(KB(RowsSize)),Ansi7ToString(Stop)]);
        end else
          Caption := Format(sCaptionUpdated,[Props.ExecuteNoResult(SQL,[]),
            Ansi7ToString(Timer.Stop)]);
      with Frame do begin
        (Parent as TSynPage).Caption := Caption;
        MemoSQL.SelStart := SelStart;
        MemoSQL.SelLength := SelLength;
        MemoSQL.SetFocus;
        if SQL<>fPreviousSQL then begin
          AppendToTextFile(SQL,fSQLLogFile);
          fPreviousSQL := SQL;
        end;
      end;
    finally
      Screen.Cursor := crDefault;
    end;
  except
    on E: Exception do
      ShowException(E);
  end;
end;

function TDBExplorerFrame.GetTableDescription(const TableName: string): string;
var Fields: TRawUTF8DynArray;
begin
  Screen.Cursor := crSQLWait;
  try
    Props.GetFieldDefinitions(S2U(TableName),Fields,true);
    result := TableName+#13#10'  '+U2S(RawUTF8ArrayToCSV(Fields,#13#10'  '));
  finally
    Screen.Cursor := crDefault;
  end;
end;

procedure TDBExplorerFrame.ListTableMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var i: integer;
begin
  if Button=mbRight then begin
    i := ListTable.ItemIndex;
    if i>=0 then begin
      if (fHint.Tag=i) and fHint.Visible then begin
        fHint.Hide;
        exit;
      end;
      fHint.ShowDelayedString(GetTableDescription(ListTable.Items[i]),
        ListTable,X,Y,5000,clNavy,true);
      fHint.Tag := i;
    end;
  end;
end;

procedure TDBExplorerFrame.BtnQueryBuilderClick(Sender: TObject);
var SQL: string;
    O: TDBQueryObject;
begin
  case TDBQueryBuilderForm.BuildQuery(ListTable,Props,SQL) of
    mrOk:  AddSQL(SQL,false,false);  // Use SQL
    mrYes: AddSQL(SQL,true,false);   // Exec SQL
    mrRetry: begin // To Object (SQL=Object serialization)
      O.FromIniSection(StringToUTF8(SQL));
      assert(O.AsIniSection=SQL);
    end;
  end;
end;

constructor TDBExplorerFrame.Create(aOwner: TComponent);
begin
  inherited Create(aOwner);
  fHint := THintWindowDelayed.Create(self);
  fSQLLogFile := ChangeFileExt(ExeVersion.ProgramFileName,'.history');
  PagesLeft.ActivePageIndex := 0;
end;

destructor TDBExplorerFrame.Destroy;
begin
  FreeAndNil(fGrid);
  inherited;
end;

function TDBExplorerFrame.OnText(Sender: TSQLTable; FieldIndex,
  RowIndex: Integer; var Text: string): boolean;
begin
  if RowIndex=0 then begin
    Text := U2S(Sender.GetU(RowIndex,FieldIndex)); // display true column name
    result := true;
  end else
    result := false;
end;

procedure TDBExplorerFrame.BtnResultToFileClick(Sender: TObject);
var F: TStream;
    Rep: TGdiPages;
    Form: TCustomForm;
    TableName: string;
begin
  Form := Application.MainForm;
  if (fGrid=nil) or (fGrid.Table.RowCount=0) then
    ShowMessage(sNothingToExport,true) else
  with TSaveDialog.Create(Form) do
  try
    InitialDir := GetShellFolderPath(CSIDL_DOCUMENTS);
    DefaultExt := 'csv';
    Filter := sExportFilter; // csv,txt,json,pdf
    FilterIndex := 0;
    Title := BtnResultToFile.Hint;
    Options := [ofOverwritePrompt,ofHideReadOnly,ofPathMustExist,ofEnableSizing];
    TableName := GetFileNameFromCurrentSelectedTable;
    FileName := TableName;
    if not Execute then
      exit;
    with CreateTempForm(format(sExportingN,[TableName]),nil,True) do
    try
      if FilterIndex=4 then begin
        if fGrid.Table.RowCount>10000 then
          if YesNo(sHugeFileConfirm,sHugeFileExport,false,true)=mrNo then
            exit;
        Rep := TGDIPages.Create(Application);
        try
          Rep.BeginDoc;
          Rep.Font.Size := 11;
          Rep.DrawTitle(SysUtils.Trim(Form.Caption));
          Rep.NewLine;
          Rep.Font.Style := [fsBold];
          Rep.DrawText(MemoSQL.Text);
          Rep.Font.Style := [];
          Rep.Font.Size := 9;
          Rep.DrawText(self.Caption);
          Rep.NewLine;
          if ListTable.ItemIndex>=0 then begin
            Rep.DrawText(GetTableDescription(TableName));
            Rep.NewLine;
          end;
          Rep.WordWrapLeftCols := true;
          TSQLRibbon(nil).AddToReport(Rep,fGrid.Table,[]);
          Rep.EndDoc;
          Rep.Caption := GetFileNameWithoutExt(ExtractFileName(FileName));
          Rep.ExportPDFAuthor := U2S(ExeVersion.User);
          Rep.ExportPDFApplication := Form.Caption;
          Rep.ExportPDFSubject := BtnResultToFile.Hint;
          Rep.ExportPDFKeywords := MemoSQL.Text;
          Rep.ExportPDF(FileName,True,false)
        finally
          Rep.Free;
        end;
      end else begin
        F := TFileStream.Create(FileName,fmCreate);
        try
          case FilterIndex of
          1: fGrid.Table.GetCSVValues(F,false,
            AnsiChar({$ifdef ISDELPHIXE}FormatSettings.{$endif}ListSeparator),true);
          2: fGrid.Table.GetCSVValues(F,true,#9,true);
          3: fGrid.Table.GetJSONValues(F,true);
          end;
        finally
          F.Free;
        end;
      end;
    finally
      Screen.Cursor := crDefault;
      Free;
    end;
    ShellExecute(Form.Handle,nil,pointer(FileName),nil,nil,SW_SHOWNORMAL);
  finally
    Free;
  end;
end;

function TDBExplorerFrame.GridValue(Row, Col: integer): RawUTF8;
begin
  result := fGrid.Table.GetU(Row,Col);
  if Row>0 then
  case fGrid.Table.FieldType(Col) of
  sftAnsiText, sftUTF8Text, sftObject:
    result := QuotedStr(result);
  sftDateTime, sftDateTimeMS:
    result := Props.SQLIso8601ToDate(result);
  sftTimeLog, sftModTime, sftCreateTime, sftUnixTime, sftUnixMSTime:
    result := Props.SQLIso8601ToDate(DateTimeToIso8601Text(
      fGrid.Table.GetAsDateTime(Row,Col)));
  sftBlob, sftBlobDynArray:
    result := ''; // BLOB won't work in SQL without parameter binding
  end;
end;

procedure TDBExplorerFrame.OnGridDblClick(Sender: TObject);
var R,C: integer;
    sql: string;
    sel: boolean;
    selStart: integer;
begin
  R := DrawGrid.Row;
  C := DrawGrid.Col;
  if (R<=0) or (fGrid.Table=nil) then
    exit;
  sel := MemoSQL.SelLength>5;
  if sel then
    sql := MemoSQL.SelText else
    sql := MemoSQL.Text;
  if Pos(' WHERE ',SysUtils.UpperCase(sql))=0 then
    sql := sql+' where ' else
    sql := sql+' and ';
  sql := sql+fGrid.Table.GetString(0,C)+'='+U2S(GridValue(R,C));
  if sel then begin
    selStart := MemoSQL.SelStart;
    MemoSQL.SelText := sql;
    MemoSQL.SelStart := selStart;
    MemoSQL.SelLength := length(sql);
    MemoSQL.SetFocus;
  end else
    MemoSQL.Text := sql;
end;

procedure TDBExplorerFrame.PopupMenuSQLPopup(Sender: TObject);
  procedure Add(M: TMenuItem; Row: integer);
    function New(caption: string; Col: integer=0; csv: PString=nil): TMenuItem;
    begin
      caption := SysUtils.trim(caption);
      if caption<>'' then begin
        result := TMenuItem.Create(self);
        result.Caption := Caption;
        result.Hint := ' '+Caption;
        result.OnClick := InsertMenu;
        M.Add(result);
      end else begin
        result := nil;
        caption := 'null';
      end;
      if csv<>nil then begin
        if csv^<>'' then
          csv^ := csv^+',';
        csv^ := csv^+caption;
      end;
    end;
  var i: integer;
      csv: string;
  begin
    M.Clear;
    csv := '';
    if (Row>=0) and (fGrid<>nil) and (fGrid.Table<>nil) then
      with fGrid.Table do
        for i := 0 to FieldCount-1 do
          New(U2S(GridValue(Row,i)),i,@csv);
    M.Enabled := M.Count>0;
    if M.Enabled then begin
      New('-');
      New(csv).Caption := M.Hint;
    end;
  end;
begin
  Add(MenuInsertFieldName,0);
  Add(MenuInsertFieldValues,DrawGrid.Row);    
end;

procedure TDBExplorerFrame.InsertMenu(Sender: TObject);
var Ins: string;
begin
  if Sender.InheritsFrom(TMenuItem) then begin
    Ins := TMenuItem(Sender).Hint;
    MemoSQL.SelText := Ins;
    MemoSQL.SelLength := length(Ins);
  end;
end;

procedure TDBExplorerFrame.ImageLogoClick(Sender: TObject);
begin
{$WARNINGS OFF}
  if DebugHook=0 then
    ShellExecute(0,nil,'https://synopse.info',nil,nil,SW_SHOWNORMAL);
{$WARNINGS ON}
end;

function TDBExplorerFrame.GetFileNameFromCurrentSelectedTable: TFileName;
begin
  if (ListTable.Count=0) or (ListTable.ItemIndex<0) then
    result := 'Export' else
    result := ListTable.Items[ListTable.ItemIndex];
end;


procedure TDBExplorerFrame.ListTableClick(Sender: TObject);
var MultiSel: boolean;
begin
  MultiSel := ListTable.SelCount>0;
  BtnQueryBuilder.Enabled := MultiSel;
  BtnTablesExport.Enabled := MultiSel;
end;

procedure TDBExplorerFrame.BtnTablesExportClick(Sender: TObject);
var RowsCount: integer;
    Timer: TPrecisionTimer;
    aFileName: TFileName;
begin
  with TSaveDialog.Create(self) do
  try
    InitialDir := GetShellFolderPath(CSIDL_DOCUMENTS);
    Filter := sTableExportFilter;
    DefaultExt := '.db3';
    FilterIndex := 0;
    Title := BtnTablesExport.Hint;
    Options := [ofOverwritePrompt,ofHideReadOnly,ofPathMustExist,ofEnableSizing];
    FileName := U2S(Props.ServerName+'_'+NowToString(false,'_'));
    if not Execute then begin 
      FileName := U2S(
        TSQLDBOracleConnectionProperties.ExtractTnsName(Props.ServerName)+
        '_'+NowToString(false,'_'));
      if not Execute then
        exit;
    end;
    aFileName := FileName;
  finally
    Free;
  end;
  Timer.Start;
  RowsCount := TDBExportTablesForm.ExportTables(ListTable,Props,aFileName);
  (Parent as TTabSheet).Caption := format(sCaptionStats,
    [RowsCount,ExtractFileName(aFileName),Ansi7ToString(Timer.Stop)]);
end;

procedure TDBExplorerFrame.BtnExecLogClick(Sender: TObject);
var F: TForm;
    List: TListBox;
    Search: TEdit;
    Details: TMemo;
begin
  F := TForm.Create(Application);
  try
    F.Caption := ' '+BtnExecLog.Hint;
    F.Font := Font;
    F.Width := 800;
    F.Height := Screen.Height-80;
    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;
    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 TDBExplorerFrame.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 TDBExplorerFrame.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))),false);
    TForm(List.Owner).Close;
  end;
end;

procedure TDBExplorerFrame.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 TDBExplorerFrame.btnRunServerClick(Sender: TObject);
begin
  if (HTTPServerForm.Props=nil) or (HTTPServerForm.Server=nil) then
    HTTPServerForm.Props := Props;
  HTTPServerForm.Show;
end;

end.

