/// DB VCL dataset using TSQLTable/TSQLTableJSON data access
// - this unit is a part of the freeware Synopse framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit mORMotVCL;

{
    This file is part of Synopse mORmot framework.

    Synopse mORMot 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)


  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 *****

  Version 1.17
  - first public release, corresponding to Synopse mORMot Framework 1.17

  Version 1.18
  - renamed SQLite3VCL.pas to mORMotVCL.pas
  - fixed ticket [9de8be5d9e] with some types like TEnumeration or TTimeLog
  - fixed process with Unicode content
  - introduced new aForceWideString optional parameter for ticket [2970335e40]

}

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

interface

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


type
  /// read-only virtual TDataSet able to access a TSQLTable
  TSynSQLTableDataSet = class(TSynVirtualDataSet)
  protected
    fTable: TSQLTable;
    {$ifndef UNICODE}
    fForceWideString: boolean;
    {$endif}
    fTableShouldBeFreed: boolean;
    fTemp64: Int64;
    fTempBlob: TSQLRawBlob;
    procedure InternalInitFieldDefs; override;
    function GetRecordCount: Integer; override;
    function GetRowFieldData(Field: TField; RowIndex: integer;
      out ResultLen: Integer; OnlyCheckNull: boolean): Pointer; override;
    function SearchForField(const aLookupFieldName: RawUTF8; const aLookupValue: variant;
      aOptions: TLocateOptions): integer; override;
  public
    /// initialize the virtual TDataSet from a TSQLTable
    // - WARNING: the supplied TSQLTable instance shall remain available
    // all the time the returned TSynSQLTableDataSet instance is used, unless
    // the TableShouldBeFreed property is set to true or CreateOwnedTable()
    // constructor is used instead
    // - with non-Unicode version of Delphi, you can set ForceWideString to
    // force the use of WideString fields instead of AnsiString, if needed
    // - the TDataSet will be opened once created
    constructor Create(Owner: TComponent; Table: TSQLTable
      {$ifndef UNICODE}; ForceWideString: boolean=false{$endif}); reintroduce;
    /// initialize the virtual TDataSet owning a TSQLTable
    // - this constructor will set TableShouldBeFreed to TRUE
    // - with non-Unicode version of Delphi, you can set ForceWideString to
    // force the use of WideString fields instead of AnsiString, if needed
    // - the TDataSet will be opened once created
    constructor CreateOwnedTable(Owner: TComponent; Table: TSQLTable
      {$ifndef UNICODE}; ForceWideString: boolean=false{$endif}); reintroduce;
    /// initialize the virtual TDataSet from a supplied JSON result
    // - this constructor will parse the supplied JSON content and create
    // an internal TSQLTableJSON instance to process the data, guessing the
    // column types from the JSON content
    // - with non-Unicode version of Delphi, you can set ForceWideString to
    // force the use of WideString fields instead of AnsiString, if needed
    // - the TDataSet will be opened once created
    constructor CreateFromJSON(Owner: TComponent; const JSON: RawUTF8
      {$ifndef UNICODE}; ForceWideString: boolean=false{$endif}); reintroduce; overload;
    /// initialize the virtual TDataSet from a supplied JSON result
    // - you can set the expected column types matching the results column layout
    // - this constructor will parse the supplied JSON content and create
    // an internal TSQLTableJSON instance to process the data
    // - with non-Unicode version of Delphi, you can set ForceWideString to
    // force the use of WideString fields instead of AnsiString, if needed
    // - the TDataSet will be opened once created
    constructor CreateFromJSON(Owner: TComponent; const JSON: RawUTF8;
      const ColumnTypes: array of TSQLFieldType
      {$ifndef UNICODE}; ForceWideString: boolean=false{$endif}); reintroduce; overload;
    /// initialize the virtual TDataSet from a supplied JSON ORM result
    // - you can set the TSQLRecord classes to retrieve the expected column types
    // - this constructor will parse the supplied JSON content and create
    // an internal TSQLTableJSON instance to process the data
    // - with non-Unicode version of Delphi, you can set ForceWideString to
    // force the use of WideString fields instead of AnsiString, if needed
    // - the TDataSet will be opened once created
    constructor CreateFromJSON(Owner: TComponent; const JSON: RawUTF8;
      const Tables: array of TSQLRecordClass
      {$ifndef UNICODE}; ForceWideString: boolean=false{$endif}); reintroduce; overload;
    /// finalize the class instance
    destructor Destroy; override;

    /// if the supplied TSQLTable instance should be released with this class
    // - Create() will left to FALSE (meaning that the TSQLTable instance shall
    // remain available all the time the TSynSQLTableDataSet instance is used)
    // - CreateOwnedTable() will set to TRUE if you want the TSQLTable to be
    //  freed when this TSynSQLTableDataSet instance will be released
    // - you can also set it after Create(), on purpose
    property TableShouldBeFreed: boolean read fTableShouldBeFreed write fTableShouldBeFreed;
    /// access to the internal TSQLTable[JSON] data
    // - you can use e.g. the SortFields() methods
    // - you may change the table content on the fly, if the column remains the same
    property Table: TSQLTable read fTable write fTable;
  end;

  /// store low-level DB.pas field information
  // - as used by GetDBFieldDef and GetDBFieldValue
  TDBFieldDef = record
    FieldName: string;
    DBType: TFieldType;
    DBSize: Integer;
    SQLType: TSQLFieldType;
    SQLIndex: integer;
    FieldType: PSQLTableFieldType;
  end;

/// get low-level DB.pas field information
// - ready to be added to a TDataset as:
// !  aDataSet.FieldDefs.Add(FieldName,DBType,DBSize);
procedure GetDBFieldDef(aTable: TSQLTable; aField: integer;
  out DBFieldDef: TDBFieldDef{$ifndef UNICODE}; aForceWideString: boolean=false{$endif});

/// fill a DB.pas field content
// - used e.g. by mORMotMidasVCL.ToClientDataSet
procedure GetDBFieldValue(aTable: TSQLTable; aRow: integer; aField: TField;
  aDataSet: TDataSet; const DBFieldDef: TDBFieldDef);

/// convert a JSON result into a VCL DataSet, guessing the field types from the JSON
// - this function is just a wrapper around TSynSQLTableDataSet.CreateFromJSON()
// - with non-Unicode version of Delphi, you can set aForceWideString to
// force the use of WideString fields instead of AnsiString, if needed
// - with Unicode version of Delphi (2009+), string/UnicodeString will be used
function JSONToDataSet(aOwner: TComponent; const aJSON: RawUTF8
  {$ifndef UNICODE}; aForceWideString: boolean=false{$endif}): TSynSQLTableDataSet; overload;
 {$ifdef HASINLINE}inline;{$endif}

/// convert a JSON ORM result into a VCL DataSet, following TSQLRecord field types
// - this function is just a wrapper around TSynSQLTableDataSet.CreateFromJSON()
// - with non-Unicode version of Delphi, you can set aForceWideString to
// force the use of WideString fields instead of AnsiString, if needed
// - with Unicode version of Delphi (2009+), string/UnicodeString will be used
function JSONTableToDataSet(aOwner: TComponent; const aJSON: RawUTF8;
  const Tables: array of TSQLRecordClass
  {$ifndef UNICODE}; aForceWideString: boolean=false{$endif}): TSynSQLTableDataSet;

/// convert a JSON result into a VCL DataSet, with a given set of column types
// - this function is just a wrapper around TSynSQLTableDataSet.CreateFromJSON()
// - with non-Unicode version of Delphi, you can set aForceWideString to
// force the use of WideString fields instead of AnsiString, if needed
// - with Unicode version of Delphi (2009+), string/UnicodeString will be used
function JSONToDataSet(aOwner: TComponent; const aJSON: RawUTF8;
  const ColumnTypes: array of TSQLFieldType
  {$ifndef UNICODE}; aForceWideString: boolean=false{$endif}): TSynSQLTableDataSet; overload;


implementation

function JSONToDataSet(aOwner: TComponent; const aJSON: RawUTF8
  {$ifndef UNICODE}; aForceWideString: boolean{$endif}): TSynSQLTableDataSet;
begin
  result := TSynSQLTableDataSet.CreateFromJSON(
    aOwner,aJSON{$ifndef UNICODE},aForceWideString{$endif});
end;

function JSONToDataSet(aOwner: TComponent; const aJSON: RawUTF8;
  const ColumnTypes: array of TSQLFieldType
  {$ifndef UNICODE}; aForceWideString: boolean{$endif}): TSynSQLTableDataSet;
begin
  result := TSynSQLTableDataSet.CreateFromJSON(
    aOwner,aJSON,ColumnTypes{$ifndef UNICODE},aForceWideString{$endif});
end;

function JSONTableToDataSet(aOwner: TComponent; const aJSON: RawUTF8;
  const Tables: array of TSQLRecordClass
  {$ifndef UNICODE}; aForceWideString: boolean{$endif}): TSynSQLTableDataSet;
begin
  result := TSynSQLTableDataSet.CreateFromJSON(
    aOwner,aJSON,Tables{$ifndef UNICODE},aForceWideString{$endif});
end;



{ TSynSQLTableDataSet }

constructor TSynSQLTableDataSet.Create(Owner: TComponent; Table: TSQLTable
  {$ifndef UNICODE}; ForceWideString: boolean{$endif});
begin
  inherited Create(Owner);
  {$ifndef UNICODE}
  fForceWideString := ForceWideString;
  {$endif}
  if Table<>nil then
    fTable := Table;
  Open;
end;

constructor TSynSQLTableDataSet.CreateOwnedTable(Owner: TComponent; Table: TSQLTable
  {$ifndef UNICODE}; ForceWideString: boolean{$endif});
begin
  Create(Owner,Table{$ifndef UNICODE},ForceWideString{$endif});
  if Table<>nil then
    fTableShouldBeFreed := true;
end;

constructor TSynSQLTableDataSet.CreateFromJSON(Owner: TComponent; const JSON: RawUTF8
  {$ifndef UNICODE}; ForceWideString: boolean{$endif});
var T: TSQLTable;
begin
  T := TSQLTableJSON.Create('',JSON);
  try
    CreateOwnedTable(Owner,T{$ifndef UNICODE},ForceWideString{$endif});
    T := nil;
  finally
    T.Free; // release temporary instance in case of TSynSQLTableDataSet error
  end;
end;

constructor TSynSQLTableDataSet.CreateFromJSON(Owner: TComponent; const JSON: RawUTF8;
  const ColumnTypes: array of TSQLFieldType
  {$ifndef UNICODE}; ForceWideString: boolean{$endif});
var T: TSQLTable;
begin
  T := TSQLTableJSON.CreateWithColumnTypes(ColumnTypes,'',JSON);
  try
    CreateOwnedTable(Owner,T{$ifndef UNICODE},ForceWideString{$endif});
    T := nil;
  finally
    T.Free; // release temporary instance in case of TSynSQLTableDataSet error
  end;
end;

constructor TSynSQLTableDataSet.CreateFromJSON(Owner: TComponent; const JSON: RawUTF8;
  const Tables: array of TSQLRecordClass
  {$ifndef UNICODE}; ForceWideString: boolean{$endif});
var T: TSQLTable;
begin
  T := TSQLTableJSON.CreateFromTables(Tables,'',JSON);
  try
    CreateOwnedTable(Owner,T{$ifndef UNICODE},ForceWideString{$endif});
    T := nil;
  finally
    T.Free; // release temporary instance in case of TSynSQLTableDataSet error
  end;
end;


destructor TSynSQLTableDataSet.Destroy;
begin
  inherited;
  if fTableShouldBeFreed then
    FreeAndNil(fTable);
end;

function TSynSQLTableDataSet.GetRecordCount: Integer;
begin
  if fTable<>nil then
    result := fTable.RowCount else
    result := 0;
end;

function TSynSQLTableDataSet.GetRowFieldData(Field: TField; RowIndex: integer;
  out ResultLen: Integer; OnlyCheckNull: boolean): Pointer;
var info: PSQLTableFieldType;
    F: integer;
    P: PUTF8Char;
label Txt;
begin
  result := nil;
  F := Field.Index;
  inc(RowIndex); // first TSQLTable row are field names
  P := fTable.Get(RowIndex,F);
  if P=nil then // null field or out-of-range RowIndex/F -> result := nil
    exit;
  result := @fTemp64; // let result point to Int64, Double or TDatetime
  if OnlyCheckNull then
    exit;
  case fTable.FieldType(F,info) of
  sftBoolean, sftInteger, sftID, sftTID:
    SetInt64(P,fTemp64);
  sftFloat, sftCurrency:
    unaligned(PDouble(@fTemp64)^) := GetExtended(P);
  sftEnumerate, sftSet:
    if info^.ContentTypeInfo=nil then
      SetInt64(P,fTemp64) else
      goto Txt;
  sftDateTime, sftDateTimeMS:
    unaligned(PDouble(@fTemp64)^) := Iso8601ToDateTimePUTF8Char(P,0);
  sftTimeLog, sftModTime, sftCreateTime:
    unaligned(PDouble(@fTemp64)^) := TimeLogToDateTime(GetInt64(P));
  sftUnixTime:
    unaligned(PDouble(@fTemp64)^) := UnixTimeToDateTime(GetInt64(P));
  sftUnixMSTime:
    unaligned(PDouble(@fTemp64)^) := UnixMSTimeToDateTime(GetInt64(P));
  sftBlob: begin
    fTempBlob := BlobToTSQLRawBlob(P);
    result := pointer(fTempBlob);
    resultLen := length(fTempBlob);
  end;
  else begin // e.g. sftUTF8Text
Txt:result := P;
    resultLen := StrLen(P);
  end;
  end;
end;

procedure GetDBFieldValue(aTable: TSQLTable; aRow: integer; aField: TField;
  aDataSet: TDataSet; const DBFieldDef: TDBFieldDef);
var blob: TSQLRawBlob;
    sstream,dstream: TStream;
    P: PUTF8Char;
begin
  if (aField<>nil) and (aRow>0) then
  with DBFieldDef do begin
    P := aTable.Get(aRow,SQLIndex);
    if P=nil then
      aField.Clear else
    case SQLType of
    sftBoolean:
      aField.AsBoolean := GetInt64(P)<>0;
    sftInteger, sftID, sftTID, sftSessionUserID:
      if aField.DataType=ftLargeInt then // handle Int64 values directly
        TLargeintField(aField).Value := GetInt64(P) else
        aField.AsInteger := GetInteger(P);
    sftFloat, sftCurrency:
      aField.AsFloat := GetExtended(P);
    sftEnumerate, sftSet:
      if FieldType^.ContentTypeInfo=nil then
        aField.AsInteger := GetInteger(P) else
        aField.AsString := aTable.GetString(aRow,SQLIndex);
    sftDateTime, sftDateTimeMS:
      aField.AsDateTime := Iso8601ToDateTimePUTF8Char(P,0);
    sftUnixTime:
      aField.AsDateTime := UnixTimeToDateTime(GetInt64(P));
    sftUnixMSTime:
      aField.AsDateTime := UnixMSTimeToDateTime(GetInt64(P));
    sftTimeLog, sftModTime, sftCreateTime:
      aField.AsDateTime := TimeLogToDateTime(GetInt64(P));
    sftBlob: begin
      blob := BlobToTSQLRawBlob(P);
      if (blob='') or (aDataSet=nil) then
        aField.Clear else begin
        sstream := TRawByteStringStream.Create(blob);
        try
          dstream := aDataSet.CreateBlobStream(aField,bmWrite);
          try
            dstream.CopyFrom(sstream,0);
          finally
            dstream.Free;
          end;
        finally
          sstream.Free;
        end;
      end;
    end;
    sftUTF8Text:
      if aField.DataType=ftWideString then
        TWideStringField(aField).Value := aTable.GetSynUnicode(aRow,SQLIndex) else
        aField.AsString := aTable.GetString(aRow,SQLIndex);
    else
      aField.AsVariant := aTable.GetVariant(aRow,SQLIndex);
    end;
  end;
end;

procedure GetDBFieldDef(aTable: TSQLTable; aField: integer;
  out DBFieldDef: TDBFieldDef{$ifndef UNICODE}; aForceWideString: boolean{$endif});
begin
  with DBFieldDef do begin
    DBSize := 0;
    SQLIndex := aField;
    FieldName := aTable.GetString(0,aField);
    if FieldName='' then begin
      DBType := DB.ftUnknown;
      SQLType := sftUnknown;
    end else begin
      SQLType := aTable.FieldType(aField,FieldType);
      case SQLType of
      sftBoolean:
        DBType := ftBoolean;
      sftInteger, sftID, sftTID:
        DBType := ftLargeint; // LargeInt=Int64
      sftFloat, sftCurrency:
        DBType := ftFloat;
      sftEnumerate, sftSet:
        if FieldType^.ContentTypeInfo=nil then
          DBType := ftInteger else begin
          DBSize := 64;
          DBType := ftDefaultVCLString;
        end;
      sftRecord: begin
          DBSize := 64;
          DBType := ftDefaultVCLString;
        end;
      sftDateTime, sftDateTimeMS, sftUnixTime, sftUnixMSTime,
      sftTimeLog, sftModTime, sftCreateTime:
        DBType := ftDateTime;
      sftBlob: begin
          DBSize := (aTable.FieldLengthMax(aField,true)*3) shr 2;
          DBType := DB.ftBlob;
        end;
      sftUTF8Text: begin
        DBSize := aTable.FieldLengthMax(aField,true);
        {$ifndef UNICODE} // for Delphi 2009+ TWideStringField = UnicodeString!
        if aForceWideString then
          DBType := ftWideString else
        {$endif}
          DBType := ftDefaultVCLString;
      end;
      else begin
        DBType := ftDefaultVCLString;
        DBSize := aTable.FieldLengthMax(aField,true);
      end;
      end;
    end;
  end;
end;

procedure TSynSQLTableDataSet.InternalInitFieldDefs;
var F: Integer;
    Def: TDBFieldDef;
begin
  FieldDefs.Clear;
  for F := 0 to fTable.FieldCount-1 do begin
    GetDBFieldDef(fTable,F,Def{$ifndef UNICODE},fForceWideString{$endif});
    FieldDefs.Add(Def.FieldName,Def.DBType,Def.DBSize);
  end;
end;

function TSynSQLTableDataSet.SearchForField(const aLookupFieldName: RawUTF8;
  const aLookupValue: variant; aOptions: TLocateOptions): integer;
var f: integer;
    val: RawUTF8;
begin
  f := Table.FieldIndex(aLookupFieldName);
  if f<0 then
    result := 0 else begin
    VariantToUTF8(aLookupValue,val);
    if loPartialKey in aOptions then
      result := Table.SearchFieldIdemPChar(val,f) else
      result := Table.SearchFieldEquals(val,f,1,not (loCaseInsensitive in aOptions));
  end;
end;

end.

