/// NexusDB 3.x direct access classes (embedded engine only)
// - this unit is a part of the freeware Synopse framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit SynDBNexusDB;

{
  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):
  - Bas Schouten (initial uSGSynDBNexusDB.pas port)

  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
                                              

{.$define SYNDB_FULLNEXUSDB}
// by default, only the NexusDB Free Embedded version is interfaced
// - you can define this conditional in project options to use all units

uses
  Windows, SysUtils,
  {$IFNDEF DELPHI5OROLDER}
  Variants,
  {$ENDIF}
  Classes, Contnrs,
  {$ifdef ISDELPHIXE2}Data.DB,{$else}DB,{$endif}
  SynCommons,
  SynTable,
  SynLog,
  SynDB,
  SynDBDataset,
  nxDB,
  nxsdServerEngine,
  nxsrServerEngine,
  nxsqlEngine,
  nxlgEventLog,
  nxllTransport,
  {$ifdef SYNDB_FULLNEXUSDB}
  nxchCommandHandler,
  nxsiServerInfoPluginServer,
  nxtcCOMTransport,
  nxtmSharedMemoryTransport,
  nxtnNamedPipeTransport,
  nxtsBlowfishRC4SecuredTransport,
  nxtwWinsockTransport,
  {$endif}
  nxseAutoComponent;


{ -------------- NexusDB database engine native connection }

type
  /// Exception type associated to the direct NexusDB connection
  ESQLDBNexusDB = class(ESQLDBDataset);

  // available communication protocols used by NexusDB between client and server
  // - nxpFOLDER: default protocol, accessing NexusDB database in a Windows Folder
  // - nxpTCPIP: TCP/IP transport, indicated by nxtcp://
  // - nxpPIPE: Windows Named Pipe transport, indicated by nxpipe://
  // - nxpMEM: direct memory transport, indicated by nxmem://
  // - nxpBFISH: BlowFish transport, indicated by nxbfisch://
  TNXProtocol = (
    nxpUnknown,
    nxpFOLDER,
    nxpTCPIP,
    nxpPIPE,
    nxpCOM,
    nxpMEM,
    nxpBFISH);

  // implement properties shared by native NexusDB connections
  // - note that only the embedded engine is implemented by now - feedback needed!
  TSQLDBNexusDBConnectionProperties = class(TSQLDBDatasetConnectionProperties)
  private
    fProtocol: TNXProtocol;
  protected
    /// initialize fForeignKeys content with all foreign keys of this DB
    // - used by GetForeignKey method
    procedure GetForeignKeys; override;
  public
    /// initialize the properties to connect to the NexusDB engine
    // - this overridden method will initialize the protocol to be used as stated
    // by aServerName i.e. nxpTCIP://11.23.34.43
    // - Default protocol is nxpFolder
    // - if protocol is nxpFolder then aDatabaseName will contain the path to the
    // folder to be used
    // - if protocol is other then nxpFolder than aServerName will contain the server
    // to connect to and aDatabaseName will contains the alias of the database
    // - Possible aServerName formats:
    // $ <protocol>://<servername>/<alias>  (aDatabaseName will be overwritten by this alias)
    // $ <protocol>://servername            (aDatabaseName will contain alias)
    // $ ''                                 (aDatabaseName contains path to nxpFOLDER database)
    constructor Create(const aServerName, aDatabaseName, aUserID, aPassWord: RawUTF8); override;
    /// convert a textual column data type, as retrieved e.g. from SQLGetField,
    // into our internal primitive types
    function ColumnTypeNativeToDB(const aNativeType: RawUTF8; aScale: integer): TSQLDBFieldType; override;
    /// Determine if database exists
    // - just test if the corresponding folder exists
    function DatabaseExists: boolean; virtual;
    /// create the database folder (if not existing)
    function CreateDatabase: boolean; virtual;
    /// delete the database folder
    // - including all its files - so to be used carefully!
    function DeleteDatabase: boolean; virtual;
    /// create a new connection
    // - caller is responsible of freeing this instance
    // - this overridden method will create an TSQLDBNexusDBConnection instance
    function NewConnection: TSQLDBConnection; override;
  published
    /// the transport protocol used to connect to the NexusDB engine
    property Protocol: TNXProtocol read fProtocol;
  end;


  // implements a direct connection to the native NexusDB database
  TSQLDBNexusDBConnection = class(TSQLDBConnectionThreadSafe)
  protected
    fDatabase: TnxDatabase;
    fSession: TnxSession;
    fServerEngine: TnxBaseServerEngine;
    procedure SetServerEngine(aServerEngine: TnxBaseServerEngine);
  public
    /// prepare a connection to a specified NexusDB database server
    constructor Create(aProperties: TSQLDBConnectionProperties); override;
    /// release memory and connection
    destructor Destroy; override;
    /// connect to the specified NexusDB server
    // - should raise an ESQLDBNexusDB on error
    procedure Connect; override;
    /// stop connection to the specified NexusDB database server
    // - should raise an ESQLDBNexusDB on error
    procedure Disconnect; override;
    /// return TRUE if Connect has been already successfully called
    function IsConnected: boolean; override;
    /// create a new statement instance
    function NewStatement: TSQLDBStatement; override;
    /// begin a Transaction for this connection
    procedure StartTransaction; override;
    /// commit changes of a Transaction for this connection
    // - StartTransaction method must have been called before
    procedure Commit; override;
    /// discard changes of a Transaction for this connection
    // - StartTransaction method must have been called before
    procedure Rollback; override;
    /// access to the associated NexusDB connection instance
    property Database: TnxDatabase read fDatabase;
    /// associated NexusDB server engine
    property ServerEngine: TnxBaseServerEngine read fServerEngine write SetServerEngine;
  end;

  // implements a statement via the native NexusDB connection
  TSQLDBNexusDBStatement = class(TSQLDBDatasetStatement)
  protected
    /// initialize and set fQuery internal field as expected
    procedure DatasetCreate; override;
    /// set fQueryParams internal field as expected
    function DatasetPrepare(const aSQL: string): boolean; override;
    /// execute underlying TQuery.ExecSQL
    procedure DatasetExecSQL; override;
  public
  end;


const
  /// set aServerName to this value to create an in-memory table
  // - do not use this constant, since it was not working as expected yet
  NEXUSDB_INMEMORY = '#INMEM';


// determine NexusDB transport protocol (TNXProtocol) to be used, based on
// protocol indicator in connection string
// - if no protocol specifier is included in the connectionstring then nxpFOLDER
// is assumed.
// - aServerName will contain the URL to the Server if the protocol
// is not nxpFOLDER
function GetNXProtocol(const aConnectionString: RawUTF8; out aServerName: RawUTF8;
  out aAlias: RawUTF8): TNXProtocol;

/// return the internal NexusDB embedded engine
// - initialize it, if was not already the case
function NexusEmbeddedEngine: TnxServerEngine;

/// release any internal NexusDB embedded engine
// - returns nil on success, or PtrInt(-1) if was not initialized  
function DropNexusEmbeddedEngine: TnxServerEngine;


implementation

uses
  {$ifdef SYNDB_FULLNEXUSDB}
  nxreRemoteServerEngine,
  uMiscellaneous,
  {$endif}
  nxsdConst,
  nxsdTypes;


{ TSQLDBNexusDBConnectionProperties }

function TSQLDBNexusDBConnectionProperties.ColumnTypeNativeToDB(const aNativeType: RawUTF8;
  aScale: integer): TSQLDBFieldType;
const CONV_TABLE: array[TnxFieldType] of TSQLDBFieldType  = (
  SynTable.ftInt64, SynTable.ftUTF8, SynTable.ftUTF8, SynTable.ftInt64,
  SynTable.ftInt64, SynTable.ftInt64, SynTable.ftInt64, SynTable.ftInt64,
  SynTable.ftInt64, SynTable.ftInt64, SynTable.ftInt64, SynTable.ftDouble,
  SynTable.ftDouble, SynTable.ftDouble, SynTable.ftCurrency, SynTable.ftDate,
  SynTable.ftDate, SynTable.ftDate, SynTable.ftInt64, SynTable.ftBlob,
  SynTable.ftUTF8, SynTable.ftBlob, SynTable.ftBlob, SynTable.ftUTF8,
  SynTable.ftUTF8, SynTable.ftUTF8, SynTable.ftInt64, SynTable.ftUTF8,
  SynTable.ftCurrency, SynTable.ftUTF8, SynTable.ftDouble );
begin
  result := CONV_TABLE[FieldDataTypesMapSQL(UTF8ToString(aNativeType))];
end;

constructor TSQLDBNexusDBConnectionProperties.Create(const aServerName, aDatabaseName, aUserID, aPassWord: RawUTF8);
var lServerURL, lAlias: RawUTF8;
begin
  fDBMS := dNexusDB;
  inherited Create(aServerName,aDatabaseName,aUserID,aPassWord);
  fProtocol := GetNXProtocol(aServerName,lServerURL,lAlias);
  if fProtocol=nxpFOLDER then
    fServerName := '' else
  if fProtocol>nxpFOLDER then
    fServerName := lServerURL;
  if fProtocol>nxpUnknown then
    fDatabaseName := lAlias else
    fDatabaseName := '';
end;

procedure TSQLDBNexusDBConnectionProperties.GetForeignKeys;
begin
  with Execute('select F.FK_CONSTRAINT_TABLE_NAME||''.''||C.FK_CONSTRAINT_REFERENCING_COLUMNS_NAME col, ' +
    '       F.FK_CONSTRAINT_REFERENCES_TABLE_NAME||''.''||R.FK_CONSTRAINT_REFERENCED_COLUMNS_NAME ref ' +
    '  from #FOREIGNKEY_CONSTRAINTS F, ' + '       #FOREIGNKEY_CONSTRAINTS_REFERENCING_COLUMNS C, ' +
    '       #FOREIGNKEY_CONSTRAINTS_REFERENCED_COLUMNS R ' + ' where ' +
    '       F.FK_CONSTRAINT_TABLE_NAME = C.FK_CONSTRAINT_TABLE_NAME' +
    '   and F.FK_CONSTRAINT_NAME = C.FK_CONSTRAINT_NAME' +
    '       F.FK_CONSTRAINT_TABLE_NAME = R.FK_CONSTRAINT_TABLE_NAME' +
    '   and F.FK_CONSTRAINT_NAME = R.FK_CONSTRAINT_NAME', []) do
    while Step do
      fForeignKeys.Add(ColumnUTF8(0),ColumnUTF8(1));
end;

function TSQLDBNexusDBConnectionProperties.NewConnection: TSQLDBConnection;
begin
  result := TSQLDBNexusDBConnection.Create(self);
end;

function TSQLDBNexusDBConnectionProperties.DatabaseExists: boolean;
begin
  if (fProtocol=nxpFOLDER) and (fDatabaseName<>NEXUSDB_INMEMORY) then
    result := DirectoryExists(UTF8ToString(fDatabaseName)) else
    result := True; // if we cannot determine directly, assume it exists
end;

function TSQLDBNexusDBConnectionProperties.CreateDatabase: boolean;
begin
  if fProtocol=nxpFOLDER then
    if fDatabaseName=NEXUSDB_INMEMORY then
      result := true else
      result := ForceDirectories(UTF8ToString(fDatabaseName)) else
    result := false;
end;

function TSQLDBNexusDBConnectionProperties.DeleteDatabase: boolean;
begin
  if fProtocol=nxpFOLDER then
    if fDatabaseName=NEXUSDB_INMEMORY then
      result := true else
      result := DirectoryDelete(UTF8ToString(fDatabaseName)) else
    result := false;
end;


{ TSQLDBNexusDBConnection }

procedure TSQLDBNexusDBConnection.Commit;
begin
  inherited Commit;
  try
    fDatabase.Commit;
  except
    inc(fTransactionCount); // the transaction is still active
    raise;
  end;
end;

procedure TSQLDBNexusDBConnection.Connect;
var Log: ISynLog;
begin
  Log := SynDBLog.Enter;
  try
    ServerEngine.Active := True;
    ServerEngine.Connected := True;
    fDatabase.Name := ClassName;
    fDatabase.Connect;
    inherited Connect; // notify any re-connection 
  except
    on E: Exception do begin
      Log.Log(sllError,E);
      Disconnect; // clean up on fail
      raise;
    end;
  end;
end;

constructor TSQLDBNexusDBConnection.Create(aProperties: TSQLDBConnectionProperties);
var
  lProp: TSQLDBNexusDBConnectionProperties;
var Log: ISynLog;
begin
  Log := SynDBLog.Enter;
  inherited Create(aProperties);
  lProp := aProperties as TSQLDBNexusDBConnectionProperties; // type check to make sure
  if lProp.Protocol=nxpUnknown then
    raise ESQLDBNexusDB.CreateUTF8(
      '%.Create: Unknown NexusDB protocol in Servername=[%]',
      [self,lProp.ServerName]);
  fDatabase := TnxDatabase.Create(nil);
  fSession := TnxSession.Create(nil);
  fSession.UserName := UTF8ToString(lProp.UserID);
  fSession.Password := UTF8ToString(lProp.PassWord);
  fDatabase.Session := fSession;
  if lProp.Protocol=nxpFOLDER then begin
    SetServerEngine(NexusEmbeddedEngine);
    if not lProp.DatabaseExists then begin
      Log.Log(sllDB,'Database % does not exists -> create folder',[lProp.DatabaseName]);
      lProp.CreateDatabase;
    end;
    Database.AliasPath := lProp.DatabaseName;
    Log.Log(sllDB,'NexusDB % using database folder %',
      [fDatabase.Version,lProp.DatabaseName]);
  end else begin
    raise ESQLDBNexusDB.Create('Remote NexusDB engine not supported (yet)');
{    SetServerEngine(TnxRemoteServerEngine.Create(nil));
    Database.AliasName := lProp.DatabaseName;
    case lProp.Protocol of
      nxpTCPIP: FTransport := TnxWinsockTransport.Create(nil);
      nxpPIPE:  FTransport := TnxNamedPipeTransport.Create(nil);
      nxpCOM:   FTransport := TnxRegisteredCOMTransport.Create(nil);
      nxpMEM:   FTransport := TnxSharedMemoryTransport.Create(nil);
      nxpBFISH: FTransport := TnxBlowfishRC4SecuredTransport.Create(nil);
    end;
    TnxRemoteServerEngine(FServerEngine).Transport := FTransport;
    FTransport.ServerName := lProp.Servername;
    FTransport.CommandHandler := FCommandHandler;
    FTransport.EventLog := FEventLog; }
  end;
end;

destructor TSQLDBNexusDBConnection.Destroy;
begin
  Disconnect;
  inherited;
  FreeAndNil(fDatabase);
  FreeAndNil(fSession);
end;

procedure TSQLDBNexusDBConnection.Disconnect;
begin
  try
    inherited Disconnect; // flush any cached statements
  finally
    if Assigned(fDatabase) then
      fDatabase.Close;
    if Assigned(fSession) and fSession.Active then begin
      fSession.CloseInactiveTables;
      fSession.CloseInactiveFolders;
      fSession.Close;
    end;
  end;
end;

function TSQLDBNexusDBConnection.IsConnected: boolean;
begin
  result := Assigned(fDatabase) and fDatabase.Connected;
end;

function TSQLDBNexusDBConnection.NewStatement: TSQLDBStatement;
begin
  result := TSQLDBNexusDBStatement.Create(self);
end;

procedure TSQLDBNexusDBConnection.Rollback;
begin
  inherited Rollback;
  fDatabase.Rollback;
end;

procedure TSQLDBNexusDBConnection.SetServerEngine(aServerEngine: TnxBaseServerEngine);
begin
  if FServerEngine<>aServerEngine then begin
    FServerEngine := aServerEngine;
    fSession.ServerEngine := aServerEngine;
  end;
end;

procedure TSQLDBNexusDBConnection.StartTransaction;
begin
  inherited StartTransaction;
  if not fDatabase.TryStartTransaction then
    raise ESQLDBNexusDB.Create('Error occcured trying to start a transaction');
end;


{ TSQLDBNexusDBStatement }

function TSQLDBNexusDBStatement.DatasetPrepare(const aSQL: string): boolean;
begin
  (fQuery as TnxQuery).SQL.Text := aSQL;
  fQueryParams := TnxQuery(fQuery).Params;
  result := fQueryParams<>nil;
end;

procedure TSQLDBNexusDBStatement.DatasetExecSQL;
begin
  (fQuery as TnxQuery).ExecSQL;
end;

procedure TSQLDBNexusDBStatement.DatasetCreate;
begin
  fQuery := TnxQuery.Create(nil);
  with TnxQuery(fQuery) do begin
    Database := (fConnection as TSQLDBNexusDBConnection).Database;
    Session := TSQLDBNexusDBConnection(fConnection).Database.Session;
  end;
end;


{ low-level NexusDB engine functions }

var
  vNexusEmbeddedEngine: TnxServerEngine;

function GetNXProtocol(const aConnectionString: RawUTF8; out aServerName: RawUTF8;
  out aAlias: RawUTF8): TNXProtocol;
const
  NXPROTNAMES:array[nxpFOLDER..high(TNXProtocol)] of RawUTF8 = (
    'nxemb','nxtcp','nxpipe','nxcom','nxmem','nxbfish');
var Prot, Alias,l,r: RawUTF8;
    IsPath: boolean;
    pr:TNXProtocol;
begin
  Split(aConnectionString,':',Prot,Alias);
  IsPath := (Prot='') or // no protocol indicated: assume it's a path (relative)
    (Prot=NEXUSDB_INMEMORY) or
    ((Length(Prot)=1) and // check for drive letter
        (Prot[1] in ['a'..'z','A' .. 'Z']) and
        DirectoryExists(TFileName(Prot[1])+':\')) or // check if root folder can be found
    IdemPChar(Pointer(aConnectionString),'.\') or
    IdemPChar(Pointer(aConnectionString),'..\');
  if IsPath then begin
    result := nxpFOLDER;
    aAlias := aConnectionString;
  end else begin
    result := nxpUnknown;
    for pr := Low(NXPROTNAMES) to High(NXPROTNAMES) do
      if prot=NXPROTNAMES[pr] then begin
        result := pr;
        break;
      end;
    if result=nxpFOLDER then begin
      if prot<>'' then
        aAlias := Alias else
        aAlias := aConnectionString;
    end else
    if result>nxpFOLDER then begin
      Split(aConnectionString,'://',l,r);
      Split(r,'/',aServerName,aAlias);
    end;
  end;
end;

function DropNexusEmbeddedEngine: TnxServerEngine;
var cmp: TComponent;
    i: integer;
begin
  if PtrInt(vNexusEmbeddedEngine)<>-1 then begin
    if Assigned(vNexusEmbeddedEngine) then begin
      vNexusEmbeddedEngine.Close;
      vNexusEmbeddedEngine.SqlEngine := nil;
      vNexusEmbeddedEngine.EventLog := nil;
      // ensure our components get destroyed before destroying the engine
      for i := vNexusEmbeddedEngine.ComponentCount-1 downto 0 do begin
        cmp := vNexusEmbeddedEngine.Components[i];
        {$ifdef SYNDB_FULLNEXUSDB}
        if cmp is TnxServerCommandHandler then
        with TnxServerCommandHandler(cmp) do begin
           ServerEngine := nil;
           Close;
           Free;
        end else
        {$endif}
        if cmp is TnxSqlEngine then
        with TnxSqlEngine(cmp) do begin
           Close;
           Free;
        end else
        if cmp is TnxEventLog then
        with TnxEventLog(cmp) do begin
           Flush;
           Free;
        end;
      end;
    end;
    FreeAndNil(vNexusEmbeddedEngine);
  end;
  result := vNexusEmbeddedEngine;
end;

function NexusEmbeddedEngine: TnxServerEngine;
{$ifdef SYNDB_FULLNEXUSDB}
var CommandHandler: TnxServerCommandHandler;
{$endif}
begin
  if PtrInt(vNexusEmbeddedEngine)=-1 then
    raise ESQLDBNexusDB.Create('Nexus Embedded engine was already finalized!') else
  if vNexusEmbeddedEngine=nil then begin
    vNexusEmbeddedEngine := TnxServerEngine.Create(nil);
    //vNexusEmbeddedEngine.Options := vNexusEmbeddedEngine.Options+[seoInMemOnly];
    {$ifdef SYNDB_FULLNEXUSDB}
    CommandHandler := TnxServerCommandHandler.Create(vNexusEmbeddedEngine);
    {$endif}
    vNexusEmbeddedEngine.SqlEngine := TnxSqlEngine.Create(vNexusEmbeddedEngine);
    vNexusEmbeddedEngine.EventLog := TnxEventLog.Create(vNexusEmbeddedEngine);
    {$ifdef SYNDB_FULLNEXUSDB}
    CommandHandler.ServerEngine := vNexusEmbeddedEngine;
    {$endif}
  end;
  result := vNexusEmbeddedEngine;
end;

procedure FinalizeNXEmbeddedEngine;
begin
  if PtrInt(vNexusEmbeddedEngine)<>-1 then
  try
    DropNexusEmbeddedEngine;
  finally
    PtrInt(vNexusEmbeddedEngine) := -1; // mark always as finalized
  end;
end;


initialization
  TSQLDBNexusDBConnectionProperties.RegisterClassNameForDefinition;

finalization
  FinalizeNXEmbeddedEngine;
end.
