/// DB VCL read/only dataset from SynDB data access
// - this unit is a part of the freeware Synopse framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit SynDBVCL;

{
    This file is part of Synopse framework.

    Synopse framework. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Original Code is Synopse mORMot framework.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s):
  - Alfred Glaenzer (alf)
  - Murat Ak

  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****

}

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

interface

uses
  {$ifdef ISDELPHIXE2}System.SysUtils,{$else}SysUtils,{$endif}
  Classes,
  Contnrs,
  SynCommons,
  SynTable,
  SynDB,
  DB,
  {$ifndef FPC}DBCommon,{$endif}
  SynVirtualDataSet;

type
  /// read-only virtual TDataSet able to access a binary buffer as returned
  // by TSQLStatement.FetchAllToBinary method or directly a TSQLStatement
  TSynBinaryDataSet = class(TSynVirtualDataSet)
  protected
    fData: RawByteString;
    fDataAccess: TSQLDBProxyStatementRandomAccess;
    fTemp64: Int64;
    procedure InternalInitFieldDefs; override;
    function GetRecordCount: Integer; override;
    function GetRowFieldData(Field: TField; RowIndex: integer;
      out ResultLen: Integer; OnlyCheckNull: boolean): Pointer; override;
  public
    /// initialize the virtual TDataSet from a FetchAllToBinary() buffer
    // - by default, ColumnDataSize would be computed from the supplied data,
    // unless you set IgnoreColumnDataSize=true to set the value to 0 (and
    // force e.g. SynDBVCL TSynBinaryDataSet.InternalInitFieldDefs define the
    // field as ftDefaultMemo) or you define some FieldDefs.Items[].Size values
    // for ftUTF8 column sizes before calling this From() method
    procedure From(const BinaryData: RawByteString;
      DataRowPosition: PCardinalDynArray=nil; IgnoreColumnDataSize: boolean=false); overload; virtual;
    /// initialize the virtual TDataSet from a SynDB TSQLDBStatement result set
    // - the supplied ISQLDBRows instance can safely be freed by the caller,
    // since a private binary copy will be owned by this instance (in Data)
    // - by default, ColumnDataSize would be computed from the supplied data,
    // unless you set IgnoreColumnDataSize=true to set the value to 0 (and
    // force e.g. SynDBVCL TSynBinaryDataSet.InternalInitFieldDefs define the
    // field as ftDefaultMemo) or you define some FieldDefs.Items[].Size values
    // for ftUTF8 column sizes before calling this From() method
    procedure From(Statement: TSQLDBStatement; MaxRowCount: cardinal=0;
      IgnoreColumnDataSize: boolean=false); overload; virtual;
    /// finalize the class instance
    destructor Destroy; override;
    /// read-only access to the internal binary buffer
    property Data: RawByteString read fData;
    /// read-only access to the internal SynDB data
    property DataAccess: TSQLDBProxyStatementRandomAccess read fDataAccess;
  end;

  /// TDataSet able to execute any SQL as SynDB's TSQLStatement result set
  // - this class is not meant to be used by itself, but via TSynDBDataSet,
  // defined in SynDBMidasVCL.pas, as a data provider able to apply updates to
  // the remote SynDB connection
  // - typical usage may be for instance over a SynDBRemote connection:
  // ! props := TSQLDBWinHTTPConnectionProperties.Create(....);
  // ! ds := TSynDBSQLDataSet.Create(MainForm);
  // ! ds.CommandText := 'select * from people';
  // ! ds.Open;
  // ! // ... use ds
  // ! ds.Close;
  // ! ds.CommandText := 'select * from customer where id=:(10):';
  // ! ds.Open;
  // ! // ... use ds
  TSynDBSQLDataSet = class(TSynBinaryDataSet)
  protected
    fConnection: TSQLDBConnectionProperties;
    fCommandText: string;
    procedure InternalOpen; override;
    procedure InternalClose; override;
    // IProvider implementation
    procedure PSSetCommandText(const ACommandText: string); override;
    function PSGetTableName: string; override;
    function PSUpdateRecord(UpdateKind: TUpdateKind; Delta: TDataSet): Boolean; override;
    function PSIsSQLBased: Boolean; override;
    function PSIsSQLSupported: Boolean; override;
    {$ifdef ISDELPHIXE3}
    function PSExecuteStatement(const ASQL: string; AParams: TParams): Integer; overload; override;
    function PSExecuteStatement(const ASQL: string; AParams: TParams; var ResultSet: TDataSet): Integer; overload; override;
    {$else}
    function PSExecuteStatement(const ASQL: string; AParams: TParams; ResultSet: Pointer=nil): Integer; overload; override;
    {$endif}
  public
    /// initialize the internal TDataSet from a SynDB TSQLDBStatement result set
    // - the supplied TSQLDBStatement can then be freed by the caller, since
    // a private binary copy will be owned by this instance (in fDataSet.Data)
    // - by default, ColumnDataSize would be computed from the supplied data,
    // unless you set IgnoreColumnDataSize=true to set the value to 0 (and
    // force e.g. SynDBVCL TSynBinaryDataSet.InternalInitFieldDefs define the
    // field as ftDefaultMemo) or you define some FieldDefs.Items[].Size values
    // for ftUTF8 column sizes before calling this From() method
    procedure From(Statement: TSQLDBStatement; MaxRowCount: cardinal=0;
      IgnoreColumnDataSize: boolean=false); override;
    /// the associated connection properties
    property Connection: TSQLDBConnectionProperties read fConnection write fConnection;
  published
    /// the SQL statement to be executed
    // - since this statement will be executed via Connection.ExecuteInlined,
    // you can specify optionally inlined parameters to this SQL text
    property CommandText: string read fCommandText write fCommandText;
  end;


/// fetch a SynDB's TQuery result into a VCL DataSet
// - if aMaxRowCount>0, will return up to the specified number of rows
// - current implementation will return a TSynSQLStatementDataSet instance,
// using an optimized internal binary buffer: the supplied TQuery can be released
// - if you need a writable TDataSet, you can use the slower ToClientDataSet()
// function as defined in SynDBMidasVCL.pas
function ToDataSet(aOwner: TComponent; aStatement: SynDB.TQuery;
  aMaxRowCount: integer=0): TSynBinaryDataSet; overload;

/// fetch a SynDB's TSQLDBStatement result into a VCL DataSet
// - just a wrapper around TSynSQLStatementDataSet.Create + Open
// - if aMaxRowCount>0, will return up to the specified number of rows
// - current implementation will return a TSynSQLStatementDataSet instance, using
// an optimized internal binary buffer: the supplied statement can be released
// - if you need a writable TDataSet, you can use the slower ToClientDataSet()
// function as defined in SynDBMidasVCL.pas
function ToDataSet(aOwner: TComponent; aStatement: TSQLDBStatement;
  aMaxRowCount: integer=0): TSynBinaryDataSet; overload;

/// fetch a SynDB ISQLDBRows result set into a VCL DataSet
// - this overloaded function can use directly a result of the
// TSQLDBConnectionProperties.Execute() method, as such:
// ! ds1.DataSet := ToDataSet(self,props.Execute('select * from table',[]));
function ToDataSet(aOwner: TComponent; aStatement: ISQLDBRows;
  aMaxRowCount: integer=0): TSynBinaryDataSet; overload;

/// fetch a SynDB's TSQLDBStatement.FetchAllToBinary buffer into a VCL DataSet
// - just a wrapper around TSynBinaryDataSet.Create + Open
// - if you need a writable TDataSet, you can use the slower ToClientDataSet()
// function as defined in SynDBMidasVCL.pas
function BinaryToDataSet(aOwner: TComponent;
  const aBinaryData: RawByteString): TSynBinaryDataSet;


implementation


function ToDataSet(aOwner: TComponent; aStatement: SynDB.TQuery;
  aMaxRowCount: integer): TSynBinaryDataSet;
begin
  if aStatement=nil then
    result := nil else
    result := ToDataSet(aOwner,
      aStatement.PreparedSQLDBStatement.Instance,aMaxRowCount);
end;

function ToDataSet(aOwner: TComponent; aStatement: TSQLDBStatement;
  aMaxRowCount: integer): TSynBinaryDataSet;
begin
  result := TSynBinaryDataSet.Create(aOwner);
  result.From(aStatement,aMaxRowCount);
  result.Open;
end;

function ToDataSet(aOwner: TComponent; aStatement: ISQLDBRows;
  aMaxRowCount: integer): TSynBinaryDataSet;
begin
  if aStatement=nil then
    result  := nil else
    result := ToDataSet(aOwner,aStatement.Instance,aMaxRowCount);
end;

function BinaryToDataSet(aOwner: TComponent; const aBinaryData: RawByteString): TSynBinaryDataSet;
begin
  result := TSynBinaryDataSet.Create(aOwner);
  result.From(aBinaryData);
  result.Open;
end;


{ TSynBinaryDataSet }

procedure TSynBinaryDataSet.From(const BinaryData: RawByteString;
  DataRowPosition: PCardinalDynArray; IgnoreColumnDataSize: boolean);
begin
  fData := BinaryData;
  fDataAccess := TSQLDBProxyStatementRandomAccess.Create(
    pointer(fData),length(fData),DataRowPosition,IgnoreColumnDataSize);
end;

procedure TSynBinaryDataSet.From(Statement: TSQLDBStatement; MaxRowCount: cardinal;
  IgnoreColumnDataSize: boolean);
var DataStream: TRawByteStringStream;
    DataRowPosition: TCardinalDynArray;
begin
  DataStream := TRawByteStringStream.Create;
  try
    Statement.FetchAllToBinary(DataStream,MaxRowCount,@DataRowPosition);
    From(DataStream.DataString,@DataRowPosition,IgnoreColumnDataSize);
  finally
    DataStream.Free;
  end;
end;

destructor TSynBinaryDataSet.Destroy;
begin
  inherited;
  FreeAndNil(fDataAccess);
end;

function TSynBinaryDataSet.GetRecordCount: Integer;
begin
  if fDataAccess=nil then
    result := 0 else
    result := fDataAccess.DataRowCount;
end;

procedure TSynBinaryDataSet.InternalInitFieldDefs;
var F,custom: integer;
    DBType: TFieldType;
    ExistingName: TRawUTF8DynArray; // FieldDefs.Items[].Name
    ExistingSize: TIntegerDynArray; // FieldDefs.Items[].Size
begin
  if FieldDefs.Count>0 then begin // custom column sizes
    SetLength(ExistingName,FieldDefs.Count);
    SetLength(ExistingSize,FieldDefs.Count);
    for F := 0 to FieldDefs.Count-1 do
      with FieldDefs.Items[F] do begin
        ExistingName[F] := StringToUtf8(Name);
        ExistingSize[F] := Size;
      end;
  end;
  FieldDefs.Clear;
  if fDataAccess=nil then
    exit;
  for F := 0 to fDataAccess.ColumnCount-1 do
    with fDataAccess.Columns[F] do begin
      if ExistingName<>nil then begin
        custom := FindRawUTF8(ExistingName,ColumnName);
        if custom>=0 then // retrieve custom max column length from FieldDefs
          ColumnDataSize := ExistingSize[custom];
      end;
      case ColumnType of
      SynTable.ftInt64:
        DBType := ftLargeint;
      SynTable.ftDate:
        DBType := ftDateTime;
      SynTable.ftUTF8:
        if ColumnDataSize=0 then
          DBType := ftDefaultMemo else // no size
          DBType := ftWideString; // means UnicodeString for Delphi 2009+
      SynTable.ftBlob:
        DBType := ftBlob;
      SynTable.ftDouble, SynTable.ftCurrency:
        DBType := ftFloat;                  
      else
        raise EDatabaseError.CreateFmt(
          'GetFieldData ColumnType=%s',[TSQLDBFieldTypeToString(ColumnType)]);
      end;
      FieldDefs.Add(UTF8ToString(ColumnName),DBType,ColumnDataSize);
    end;
end;

function TSynBinaryDataSet.GetRowFieldData(Field: TField;
  RowIndex: integer; out ResultLen: Integer; OnlyCheckNull: boolean): Pointer;
var F: integer;
begin
  result := nil;
  F := Field.Index;
  if (fDataAccess=nil) or not fDataAccess.GotoRow(RowIndex) then
    exit;
  result := fDataAccess.ColumnData(F);
  if (result<>nil) and not OnlyCheckNull then
    case fDataAccess.Columns[F].ColumnType of
    SynTable.ftInt64: begin
      fTemp64 := FromVarInt64(PByte(result));
      result := @fTemp64;
    end;
    SynTable.ftCurrency: begin // ftFloat expects a DOUBLE value
      unaligned(PDouble(@fTemp64)^) := PCurrency(result)^;
      result := @fTemp64;
    end;
    SynTable.ftUTF8, SynTable.ftBlob:
      resultLen := FromVarUInt32(PByte(result));
    end; // other ColumnTypes are already in the expected format
end;


{ TSynDBSQLDataSet }

procedure TSynDBSQLDataSet.From(Statement: TSQLDBStatement; MaxRowCount: cardinal;
  IgnoreColumnDataSize: boolean);
begin
  inherited From(Statement,MaxRowCount,IgnoreColumnDataSize);
  fConnection := Statement.Connection.Properties;
end;

procedure TSynDBSQLDataSet.InternalClose;
begin
  inherited InternalClose;
  FreeAndNil(fDataAccess);
  fData := '';
end;

procedure TSynDBSQLDataSet.InternalOpen;
var Rows: ISQLDBRows;
begin
  if fCommandText='' then begin
    if fData<>'' then // called e.g. after From() method
      inherited InternalOpen;
    exit;
  end;
  Rows := fConnection.ExecuteInlined(StringToUTF8(fCommandText),true);
  if Rows<>nil then begin
    From(Rows.Instance);
    inherited InternalOpen;
  end;
end;

{$ifdef ISDELPHIXE3}

function TSynDBSQLDataSet.PSExecuteStatement(const ASQL: string;
  AParams: TParams): Integer;
var DS: TDataSet;
begin
  DS := nil;
  result := PSExecuteStatement(ASQL,AParams,DS);
  DS.Free;
end;

function TSynDBSQLDataSet.PSExecuteStatement(const ASQL: string;
  AParams: TParams; var ResultSet: TDataSet): Integer;
{$else}
function TSynDBSQLDataSet.PSExecuteStatement(const ASQL: string;
  AParams: TParams; ResultSet: Pointer): Integer;
{$endif}
var Stmt: ISQLDBStatement;
    blob: TBlobData;
    p: integer;
begin // only execute writes in current implementation
  if fConnection=nil then
    raise ESQLQueryException.CreateUTF8('%.PSExecuteStatement with Connection=nil',[self]);
  Stmt := fConnection.NewThreadSafeStatementPrepared(StringToUTF8(ASQL),false);
  if Stmt<>nil then
    try
      if AParams<>nil then
        for p := 0 to AParams.Count-1 do
          if aParams[p].DataType = ftBlob then begin
            blob := aParams[p].AsBlob;
            Stmt.BindBlob(p+1,pointer(blob),length(blob));
          end else
            Stmt.BindVariant(p+1,AParams[p].Value,False);
      Stmt.ExecutePrepared;
      result := Stmt.UpdateCount;
      if result=0 then
        result := 1; // optimistic result, even if SynDB returned 0
    except
      result := 0;
    end else
      result := 0;
end;

function TSynDBSQLDataSet.PSGetTableName: string;
begin
  // ToDo We miss GetTableNameFromSQL in FPC, Delphi function from DBCommon
  result := {$ifdef FPC}''{$else}GetTableNameFromSQL(fCommandText){$endif};
end;

function TSynDBSQLDataSet.PSIsSQLBased: Boolean;
begin
  result := true;
end;

function TSynDBSQLDataSet.PSIsSQLSupported: Boolean;
begin
  result := true;
end;

procedure TSynDBSQLDataSet.PSSetCommandText(const ACommandText: string);
begin
  inherited;
  fCommandText := ACommandText;
end;

function TSynDBSQLDataSet.PSUpdateRecord(UpdateKind: TUpdateKind;
  Delta: TDataSet): Boolean;
begin
  result := false;
end;

end.
