{$mode objfpc}
{$H+}

unit tsdb;

Interface

Uses
  sqldb, types, tstypes, tsstring, tsutils, pqconnection;

const
  // Ini file constants
  DefaultDBConfigFileName = '/etc/dbdigest.ini';
  SSection    = 'Database';
  KeyName     = 'Name';
  KeyHost     = 'Host';
  KeyUser     = 'UserName';
  KeyPassword = 'Password';
  KeyPort     = 'Port';

  // Query to run to get all test run test results.
  // For test results that did not change, the last test result ID is returned.
  // Needs formatting with 2 IDS : Run ID, Platform ID
  SQLTestResultIDS =
   'with testrunresultids as ( ' +
   '  select ' +
   '    tr_id as theid ' +
   '  from ' +
   '    testresults ' +
   '  where ' +
   '    (tr_testrun_fk=%d) ' +
   '  union ' +
   '  select ' +
   '    tl_testresults_fk as theid ' +
   '  from ' +
   '    tests ' +
   '    inner join testlastresults on (tl_test_fk=t_id) and (tl_platform_fk=%d) ' +
   ')';

  // Get all test results for a testrun (but not compile/run log)

  SQLSelectTestResults =
    SQLTestResultIDS +
    'select ' +
    '  T_ID as Id,T_NAME as Filename,TR_SKIP as Skipped, TR_OK as OK,TR_RESULT as Result ' +
    'from ' +
    '  testrunresultids ' +
    '  left join testresults on (tr_id=theid) ' +
    '  inner join tests on (tr_test_fk=t_id)';

Type
  TMapType = (mtCPU, mtOS, mtVersion);

  { TTestSQL }

  TTestSQL = class(TObject)
  Const
    Bools : Array[Boolean] of String = ('f','t');
  private
    FRelSrcDir: String;
    FTestSrcDir: string;
    FConnection : TPQConnection;
    FDatabaseName : String;
    FHost : String;
    FUser : String;
    FPassword : String;
    FPort : Word;
    Flogprefix : String;
    Class Procedure FreeQueryResult (Var aQry : TSQLQuery);
    Class Function  GetIntResultField (aQry : TSQLQuery; aFieldIndex : Integer) : Integer;
    Class Function  GetInt64ResultField (aQry : TSQLQuery; aFieldIndex : Integer) : Int64;
    Class Function  GetStrResultField (aQry : TSQLQuery; aFieldIndex : Integer) : String;
    function InsertTestHistory(TestRunID, TestPreviousID: Integer): boolean;
    // Overload adds prefix to actual call
    procedure Verbose(aLevel : TVerboseLevel; const aMsg : string);
    { ---------------------------------------------------------------------
        Low-level DB access.
      ---------------------------------------------------------------------}

    // create and open a query, return in Res.
    Function  OpenQuery (Qry : String; Out Res : TSQLQuery; Silent : Boolean) : Boolean ;

  Public
    { ---------------------------------------------------------------------
      High-level access
      ---------------------------------------------------------------------}
    // Constructor.
    Constructor create(aDatabaseName,aHost,aUser,aPassword : String; aPort : Word);
    // Destructor
    Destructor destroy; override;
    // Try to connect to database with params given in constructor.
    Function ConnectToDatabase : Boolean;
    // Disconnect from database
    Procedure DisconnectDatabase;
    // Create query object.
    function CreateQuery(const ASQL: String): TSQLQuery;
    // Execute a query, return true if it executed without error.
    Function  ExecuteQuery (Qry : String; Silent : Boolean) : Boolean ;
    // Run query, return first field as integer. -1 on error or no data.
    function GetIDQueryResult(Qry: TSQLQuery): Int64;
    // Run SQL, return first field as integer. -1 on error or no data.
    Function  IDQuery(Qry : String) : Integer;
    // Run query, return first field as int64. -1 on error or no data.
    Function  ID64Query(Qry : String) : Int64;
    // Run query, return first field as string. Empty string on error or no data.
    Function  StringQuery(Qry : String) : String;
    Function CreateMap(aType : TMapType) : TIntegerDynArray;
    // Adding things
    // Add a category.
    Function AddCategory(const aName : String) : Integer;
    // Add a CPU.
    Function AddCPU(const aName : String) : Integer;
    // Add an OS.
    Function AddOS(const aName : String) : Integer;
    // Add a compiler version.
    function AddVersion(const aName: String; aReleaseDate: TDateTime): Integer;
    // Add a platform.
    Function AddPlatform(const aData : TTestRunData) : Integer;
    // Add a test and return the ID. If the test already exists, return it's ID
    Function AddTest(Name : String; AddSource : Boolean) : Integer;
    // Add a test run. Return the test run ID.
    function AddRun(const aData: TTestRunData): Int64;
    // Ad test result and return ID. If a result exists already for the given run/test, update and return ID.
    Function AddTestResult(aData : TTestResultData) : Int64;
    // Add LastTestResult. If it exists already with given platform/test, update result ID.
    function AddLastResult(TestID, PlatformID: Integer; ResultID: Int64): Boolean;
    // Add previousTestResult. If it exists already with given platform/test, update result ID.
    function AddPreviousResult(TestID, PlatformID: Integer; ResultID: Int64): Boolean;
    // Add Check-All-RTL results. Adds logs for failed tests, if available.
    Function AddCheckAllRtl(aData : TCheckAllRTL) : Int64;
    // Add Check-All-RTL failed run log
    function AddCheckAllRtlLog(aCheckAllRTLID: int64; aStep: Byte; const aLog: String): Int64;
    //
    // Get ID based on key. All keys are case sensitive. If a key does not exist, -1 is returned.
    //
    // Get test ID based on test name.
    Function GetTestID(aName : string) : Integer;
    Function GetTestName(aID : Integer) : string;
    Function GetTestFileName(aID : Integer) : String;
    Function GetTestSource(aID : Integer) : String;
    // Get OS ID based on OS name.
    Function GetOSID(aName : String) : Integer;
    Function GetOSName(aID : Integer) : String;
    // Get CPU ID based on CPU name.
    Function GetCPUID(Name : String) : Integer;
    Function GetCPUName(aID : Integer) : String;

    // Get category ID based on Category name.
    Function GetCategoryID(aName : String) : Integer;
    Function GetCategoryName(aID : Integer) : String;
    // Get version ID based on version name.
    Function GetVersionID(aName : String) : Integer;
    Function GetVersionName(aID : Integer) : string;
    // Get platform ID based on OS, cpu, category, config.
    function GetPlatformID(aData: TTestRunData; aAllowCreate: Boolean): Integer;
    function GetPlatformID(aVersionID, aOSID, aCPUID, aCategoryID : Integer; const aMachine, aConfig : String): Integer;
    function GetPlatformID(aRunID : Int64): Integer;
    // Get run ID based on platform/date.
    Function GetRunID(aData : TTestRunData) : Int64;
    function GetNextRunID(RunID: Int64): Int64;
    function GetPreviousRunID(RunID: Int64): Int64;
    Function GetRunData(aID : Int64; out aData : TTestRunData) : Boolean;
    Function GetLastRunByPlatformAndDate(aPLatformID : Integer; aDate : TDateTime) : Integer;
    // Get testinfo based on test ID
    function GetTestInfo(aID: Int64; out aInfo: TTestInfo): Boolean;
    // Get last test result ID based on platform/test.
    function GetLastTestResult(aTestID, aPlatFormID: Integer): TTestResultData;
    function GetFailCount(aRunID : Int64) : Int64;
    // Update tests
    Function UpdateTest(ID : Integer; Info : TConfig; Const Source : String) : Boolean;
    function UpdateTestResult(aData: TTestResultData): Int64;
    function UpdateTestRun(aData : TTestRunData): Boolean;
    Function GetFailCount(aRunID : Integer) : Int64;

    // Create test if it does not exist yet.
    Function RequireTestID(const aName : String): Integer;
    // Delete all results from a test run.
    Function CleanTestRun(ID : Integer) : Boolean;
    // Escape SQL (quotes etc.
    Class Function  EscapeSQL(Const S : String) : String;
    // return SQL date
    Class Function  SQLDate(D : TDateTime) : String;
    // Rel src dir
    Property RelSrcDir : String Read FRelSrcDir Write FRelSrcDir;
    // test src dir.
    Property TestSrcDir : string read FTestSrcDir Write FTestSrcDir;
    // Prefix to use when logging (in case of multi-thread)
    Property LogPrefix : String Read FLogPrefix Write FLogPrefix;
  end;


Implementation

Uses
  SysUtils;

{ ---------------------------------------------------------------------
    Low-level DB access.
  ---------------------------------------------------------------------}

function TTestSQL.ConnectToDatabase: Boolean;

begin
  Result:=False;
  Verbose(V_SQL,'Connection params : '+FDatabaseName+' '+FHost+' '+FUser+' '+IntToStr(FPort));
  FConnection:=TPQConnection.Create(Nil);
  try
    FConnection.Hostname:=FHost;
    FConnection.DatabaseName:=FDatabaseName;
    FConnection.Username:=FUser;
    FConnection.Password:=FPassword;
    FConnection.Connected:=true;
    FConnection.Transaction:=TSQLTransaction.Create(FConnection);
    if (FPort<>0) then
      FConnection.Params.Values['Port']:=IntToStr(FPort);
    FConnection.Connected:=True;
    Result:=True
  except
    On E : Exception do
      begin
      Verbose(V_ERROR,'Failed to connect to database : '+E.Message);
      FreeAndNil(FConnection);
      end;
  end;
end;

procedure TTestSQL.DisconnectDatabase;

begin
  FreeAndNil(FConnection);
end;

function TTestSQL.AddCategory(const aName: String): Integer;

Const
  SQLInsert = 'INSERT INTO TESTCATEGORY (TA_NAME) VALUES (''%s'') RETURNING TA_ID';

begin
  Result:=IDQuery(Format(SQLInsert,[EscapeSQL(aName)]));
end;

function TTestSQL.AddCPU(const aName: String): Integer;

Const
  SQLInsert = 'INSERT INTO TESTCPU (TC_NAME) VALUES (''%s'') RETURNING TC_ID';

begin
  Result:=IDQuery(Format(SQLInsert,[EscapeSQL(aName)]));
end;

function TTestSQL.AddOS(const aName: String): Integer;
Const
  SQLInsert = 'INSERT INTO TESTOS (TO_NAME) VALUES (''%s'') RETURNING TO_ID';

begin
  Result:=IDQuery(Format(SQLInsert,[EscapeSQL(aName)]));
end;

function TTestSQL.AddVersion(const aName: String; aReleaseDate : TDateTime): Integer;
Const
  SQLInsert = 'INSERT INTO TESTVERSION (TV_VERSION,TV_RELEASEDATE) VALUES (''%s'',''%s'') RETURNING TV_ID';

begin
  Result:=IDQuery(Format(SQLInsert,[EscapeSQL(aName),SQLDate(aReleaseDate)]));
end;


function TTestSQL.CreateQuery(const ASQL: String): TSQLQuery;

begin
  Result:=TSQLQuery.Create(FConnection);
  Result.Database:=FConnection;
  Result.Transaction:=FConnection.Transaction;
  Result.SQL.Text:=ASQL;
end;


function TTestSQL.ExecuteQuery(Qry: String; Silent: Boolean): Boolean;

begin
  Verbose(V_SQL,'Executing query:'+Qry);
  Result:=False;
  try
    With CreateQuery(Qry) do
      try
        ExecSQL;
        Result:=True;
        (Transaction as TSQLTransaction).Commit;
      finally
        Free;
      end;
  except
    On E : exception do
      begin
      FConnection.Transaction.RollBack;
      if not Silent then
        Verbose(V_WARNING,'Query : '+Qry+'Failed : '+E.Message);
      end;
  end;
end;

function TTestSQL.OpenQuery(Qry: String; out Res: TSQLQuery; Silent: Boolean): Boolean;

begin
  Result:=False;
  Verbose(V_SQL,'Running query:'+Qry);
  Res:=CreateQuery(Qry);
  try
    Res.Open;
    Result:=True;
  except
    On E : exception do
      begin
      FreeAndNil(Res);
      Try
        FConnection.Transaction.RollBack;
      except
      end;
      if not Silent then
        Verbose(V_WARNING,'Query : '+Qry+'Failed : '+E.Message);
      end;
  end;
end;

class function TTestSQL.GetIntResultField(aQry: TSQLQuery; aFieldIndex: Integer): Integer;


begin
  If (aQry=Nil) or (aQry.IsEmpty) or (aFieldIndex>=aQry.Fields.Count) then
    Result:=-1
  else
    Result:=aQry.Fields[aFieldIndex].AsInteger;
  tsutils.Verbose(V_SQL,'Field value '+IntToStr(Result));
end;

class function TTestSQL.GetInt64ResultField(aQry: TSQLQuery; aFieldIndex: Integer): Int64;
begin
  If (aQry=Nil) or (aQry.IsEmpty) or (aFieldIndex>=aQry.Fields.Count) then
    Result:=-1
  else
    Result:=aQry.Fields[aFieldIndex].AsLargeInt;
  tsutils.Verbose(V_SQL,'Field value '+IntToStr(Result));
end;

class function TTestSQL.GetStrResultField(aQry: TSQLQuery; aFieldIndex: Integer): String;
begin
  If (aQry=Nil) or (aQry.IsEmpty) or (aFieldIndex>=aQry.Fields.Count) then
    Result:=''
  else
    Result:=aQry.Fields[aFieldIndex].AsString;
  tsutils.Verbose(V_SQL,'Field value '+Result);
end;

procedure TTestSQL.Verbose(aLevel: TVerboseLevel; const aMsg: string);
begin
  tsutils.Verbose(aLevel,logPrefix+aMsg);
end;

function TTestSQL.AddPlatform(const aData : TTestRunData) : Integer;

const
  SQLInsert = 'INSERT INTO TESTPLATFORM (TP_CPU_FK, TP_OS_FK, TP_VERSION_FK, TP_CATEGORY_FK, TP_CONFIG, TP_MACHINE) '+
              ' VALUES (%d, %d, %d, %d, ''%s'', ''%s'') '+
              '  RETURNING TP_ID';

begin
  With aData do
    Result:=IDQuery(Format(SQLInsert,[CPUID,OSID,VersionID,CategoryID,EscapeSQL(config),EscapeSQL(machine)]));
end;

class procedure TTestSQL.FreeQueryResult(var aQry: TSQLQuery);

begin
  if Assigned(aQry) and Assigned(aQry.Transaction) then
    aQry.SQLTransaction.Commit;
  FreeAndNil(aQry);
end;

function TTestSQL.GetIDQueryResult(Qry: TSQLQuery): Int64;


begin
  Result:=-1;
  Qry.Open;
  if Not Qry.IsEmpty then
    Result:=Qry.Fields[0].AsLargeInt;
  Qry.SQLTransaction.Commit;
end;

function TTestSQL.IDQuery(Qry: String): Integer;

Var
  Res : TSQLQuery;

begin
  Result:=-1;
  If OpenQuery(Qry,Res,False) then
    try
      Result:=GetIntResultField(Res,0);
    finally
      FreeQueryResult(Res);
    end;
end;

function TTestSQL.ID64Query(Qry: String): Int64;
Var
  Res : TSQLQuery;

begin
  Result:=-1;
  If OpenQuery(Qry,Res,False) then
    try
      Result:=GetInt64ResultField(Res,0);
    finally
      FreeQueryResult(Res);
    end;
end;

function TTestSQL.StringQuery(Qry: String): String;

Var
  Res : TSQLQuery;

begin
  Result:='';
  If OpenQuery(Qry,Res,False) then
    try
      Result:=GetStrResultField(Res,0);
      Verbose(V_SQL,'StringQuery: '+Result);
    finally
      FreeQueryResult(Res);
    end;
end;

function TTestSQL.CreateMap(aType: TMapType): TIntegerDynArray;
var
  Qry : TSQLQuery;
  lSQL : string;
  lIdx : Integer;

begin
  Result:=[];
  Case aType of
    mtCPU : lSQL:='SELECT TC_ID FROM TESTCPU order by TC_ID';
    mtOS  : lSQL:='SELECT TO_ID FROM TESTOS order by TO_ID';
    mtVersion  : lSQL:='SELECT TV_ID FROM TESTVERSION order by TV_ID';
  end;
  Qry:=CreateQuery(lSQL);
  Verbose(V_SQL,'CreateMap: '+lSQL);
  try
    Qry.PacketRecords:=-1;
    Qry.Open;
    SetLength(Result,Qry.RecordCount);
    lIDx:=0;
    While not Qry.EOF do
      begin
      Result[lIdx]:=Qry.Fields[0].AsInteger;
      inc(lIdx);
      Qry.Next;
      end;
  finally
    Qry.Free;
  end;
end;

constructor TTestSQL.create(aDatabaseName, aHost, aUser, aPassword: String; aPort: Word);
begin
  FDatabaseName:=aDatabaseName;
  FHost:=aHost;
  FUser:=aUser;
  FPassword:=aPassword;
  FPort:=aPort;
end;

destructor TTestSQL.destroy;
begin
  DisconnectDatabase;
  inherited destroy;
end;

class function TTestSQL.EscapeSQL(const S: String): String;

begin
//  Result:=StringReplace(S,'\','\\',[rfReplaceAll]);
  Result:=StringReplace(S,'''','''''',[rfReplaceAll]);
  if (Result<>S) then
    tsutils.Verbose(V_SQL,'EscapeSQL : "'+S+'" -> "'+Result+'"');
end;


class function TTestSQL.SQLDate(D: TDateTime): String;

begin
  Result:=FormatDateTime('YYYY/MM/DD hh:nn:ss',D);
end;

{ ---------------------------------------------------------------------
  High-level access
  ---------------------------------------------------------------------}


function TTestSQL.GetTestID(aName: string): Integer;

Const
  SFromName = 'SELECT T_ID FROM TESTS WHERE (T_NAME=''%s'')';

begin
  Result:=IDQuery(Format(SFromName,[aName]));
end;

function TTestSQL.GetTestName(aID: Integer): string;
begin
  Result:=StringQuery(Format('SELECT T_NAME FROM TESTCPU WHERE (T_ID=%d)',[aID]));
end;

function TTestSQL.GetTestFileName(aID: Integer): String;
begin
  Result:=StringQuery(Format('SELECT T_NAME FROM TESTS WHERE (T_ID=%d)',[aID]));
end;

function TTestSQL.GetTestSource(aID: Integer): String;
begin
  Result:=StringQuery(Format('select T_SOURCE from TESTS where (T_ID=%d)',[aid]));
end;

function TTestSQL.GetOSID(aName: String): Integer;

Const
  SFromName = 'SELECT TO_ID FROM TESTOS WHERE (TO_NAME=''%s'')';

begin
  Result:=IDQuery(Format(SFromName,[aName]));
end;

function TTestSQL.GetOSName(aID: Integer): String;
begin
  Result:=StringQuery(Format('SELECT TO_NAME FROM TESTOS WHERE (TO_ID=%d)',[aID]));
end;

function TTestSQL.GetVersionID(aName: String): Integer;

Const
  SFromName = 'SELECT TV_ID FROM TESTVERSION WHERE (TV_VERSION=''%s'')';

begin
  Result:=IDQuery(Format(SFromName,[aName]));
end;

function TTestSQL.GetVersionName(aID: Integer): string;

const
  SQLSelectVersion = 'SELECT TV_VERSION FROM TESTVERSION WHERE (TV_ID=%d)';

begin
  Result:=StringQuery(Format(SQLSelectVersion,[aID]));
end;

function TTestSQL.GetPlatformID(aData: TTestRunData; aAllowCreate: Boolean): Integer;

Const
  SQLSelect = 'SELECT TP_ID FROM TESTPLATFORM ' +
             ' WHERE ' +
             '  (TP_VERSION_FK=%d)' +
             '  AND (TP_OS_FK=%d)' +
             '  AND (TP_CPU_FK=%d)' +
             '  AND (TP_CATEGORY_FK=%d)' +
             '  AND (TP_CONFIG=''%s'')' +
             '  AND (TP_MACHINE=''%s'')';

begin
  With aData do
    Result:=IDQuery(Format(SQLSelect,[VersionID,OSID,CPUID,CategoryID,Config,Machine]));
  if (Result=-1) and aAllowCreate then
    Result:=AddPlatform(aData)
end;

function TTestSQL.GetPlatformID(aVersionID, aOSID, aCPUID, aCategoryID: Integer; const aMachine, aConfig: String): Integer;
var
  TR : TTestRunData;
begin
  TR:=Default(TTestRunData);
  TR.VersionID:=aVersionID;
  TR.OSID:=aOSID;
  TR.CPUID:=aCPUID;
  TR.CategoryID:=aCategoryID;
  TR.config:=aConfig;
  TR.Machine:=aMachine;
  Result:=GetPlatformID(TR,False);
end;

function TTestSQL.GetPlatformID(aRunID: Int64): Integer;
Const
  SFromID = 'SELECT TU_PLATFORM_FK FROM TESTRUN WHERE (TU_ID=%d)';
begin
  Result:=IDQuery(Format(SFromID,[aRunID]));
end;

function TTestSQL.GetCPUID(Name: String): Integer;

Const
  SFromName = 'SELECT TC_ID FROM TESTCPU WHERE (TC_NAME=''%s'')';

begin
  Result:=IDQuery(Format(SFromName,[Name]));
end;

function TTestSQL.GetCPUName(aID: Integer): String;
begin
  Result:=StringQuery(Format('SELECT TC_NAME FROM TESTCPU WHERE (TC_ID=%d)',[aID]));
end;

function TTestSQL.GetCategoryID(aName: String): Integer;

Const
  SFromName = 'SELECT TA_ID FROM TESTCATEGORY WHERE (TA_NAME=''%s'')';

begin
  Result:=IDQuery(Format(SFromName,[aName]));
end;

function TTestSQL.GetCategoryName(aID: Integer): String;
begin
  Result:=StringQuery(Format('SELECT TA_NAME FROM TESTCATEGORY WHERE (TA_ID=%d)',[aID]));
end;

function TTestSQL.GetRunID(aData: TTestRunData): Int64;


Const
  SFromIDS = 'SELECT TU_ID FROM TESTRUN WHERE '+
             ' (TU_PLATFORM_FK=%d) '+
             ' AND (TU_DATE=''%s'')';

begin
  With aData do
    Result:=ID64Query(Format(SFromIDS,[PlatFormID,SQLDate(Date)]));
end;

function TTestSQL.GetRunData(aID: Int64; out aData: TTestRunData): Boolean;

const
  SQLSelectRUNData =
             'select ' +
             '  TA_NAME, TV_VERSION, TV_RELEASEDATE, TV_ID, TC_NAME, TO_NAME, TestPlatform.* , TESTRUN.* ' +
             'from  ' +
             '  TESTRUN  ' +
             '  INNER JOIN TESTPLATFORM ON (TP_ID=TU_PLATFORM_FK) ' +
             '  INNER JOIN TESTOS ON (TO_ID=TP_OS_FK) ' +
             '  INNER JOIN TESTCPU ON (TC_ID=TP_CPU_FK) ' +
             '  INNER JOIN TESTCATEGORY ON (TA_ID=TP_CATEGORY_FK) ' +
             '  INNER JOIN TESTVERSION ON (TV_ID=TP_VERSION_FK) ' +
             'WHERE (TU_ID=%d)';

var
  Qry : TSQLQuery;
  ST : TTestStatus;
  S : string;

begin

  S:=Format(SQLSelectRunData,[aID]);
  Qry:=CreateQuery(S);
  Verbose(V_SQL,'GetRunData: '+s);
  try
    Qry.Open;
    Result:=Not Qry.IsEmpty;
    if Result then
      With Qry do
        begin
        aData.RunID:=aID;
        aData.os:=FieldByName('TO_NAME').AsString;
        aData.OSID:=FieldByName('TP_OS_FK').AsInteger;
        aData.cpu:=FieldByName('TC_NAME').AsString;
        aData.CPUID:=FieldByName('TP_CPU_FK').AsInteger;
        aData.version:=FieldByName('TV_VERSION').AsString;
        aData.versionID:=FieldByName('TV_ID').asInteger;
        aData.category:=FieldByName('TA_NAME').AsString;
        aData.PlatformID:=FieldByName('TP_ID').AsInteger;
        aData.Config:=FieldByName('TP_CONFIG').AsString;
        aData.Machine:=FieldByName('TP_MACHINE').AsString;
        aData.submitter:=FieldByName('TU_SUBMITTER').AsString;
        // aData.:=FieldByName('TV_RELEASEDATE').AsString;
        aData.Date:=FieldByName('TU_DATE').AsDateTime;
        aData.CompilerDate:=FieldByName('TU_COMPILERDATE').AsString;
        aData.CompilerFullversion:=FieldByName('TU_COMPILERFULLVERSION').AsString;
        aData.CompilerRevision:=FieldByName('TU_COMPILERREVISION').AsString;
        aData.TestsRevision:=FieldByName('TU_TESTSREVISION').AsString;
        aData.RTLRevision:=FieldByName('TU_RTLREVISION').AsString;
        aData.PackagesRevision:=FieldByName('TU_PACKAGESREVISION').AsString;
        for ST in TValidTestStatus do
          aData.StatusCount[ST]:=FieldByName(SQLField[ST]).AsInteger;
        end;
  finally
    Qry.Free;
  end;
end;

function TTestSQL.GetLastRunByPlatformAndDate(aPLatformID: Integer; aDate: TDateTime): Integer;
const
  SQLSelect =
    'select '+
    '  TU_ID '+
    'from '+
    '  testrun '+
    'where '+
    '  (tu_platform_fk=%d) '+
    '  and (tu_date<''%s'') '+
    'order by '+
    '  tu_date desc '+
    'limit 1';
begin
  Result:=ID64Query(Format(SQLSelect,[aPlatformID,SQLDate(aDate)]));
end;

function TTestSQL.GetTestInfo(aID: Int64; out aInfo: TTestInfo): Boolean;

  function splitID(aString : String) : TIntegerDynArray;
  var
    lArray : TStringDynArray;
    i,count : integer;
    S : string;
  begin
    Result:=[];
    lArray:=aString.Split(',');
    SetLength(Result,Length(lArray));
    count:=0;
    for S in lArray do
      if TryStrToInt(Trim(S),i) then
        begin
        Result[Count]:=I;
        inc(count);
        end;
    SetLength(Result,Count);
  end;

const
  SQLSelect = 'SELECT * FROM TESTS WHERE (T_ID=%d)';

var
  Qry : TSQLQuery;

begin
  aInfo:=Default(TTestInfo);
  Qry:=CreateQuery(Format(SQLSelect,[aID]));
  try
    Qry.Open;
    Result:=Not Qry.IsEmpty;
    if Not Result then
      exit;

    aInfo.Name:=Qry.FieldByname('T_Name').AsString;
    aInfo.CPU:=Qry.FieldByname('T_CPU').AsString;
    aInfo.OS:=Qry.FieldByname('T_OS').Asstring;
    aInfo.Version:=Qry.FieldByname('T_VERSION').Asstring;
    aInfo.AddDate:=Qry.FieldByname('T_ADDDATE').AsDateTime;
    aInfo.Graph:=Qry.FieldByname('T_GRAPH').Asboolean;
    aInfo.Interactive:=Qry.FieldByname('T_INTERACTIVE').Asboolean;
    aInfo.Result:=Qry.FieldByname('T_RESULT').AsInteger;
    aInfo.Fail:=Qry.FieldByname('T_FAIL').Asboolean;
    aInfo.ReCompile:=Qry.FieldByname('T_RECOMPILE').Asboolean;
    aInfo.NoRun:=Qry.FieldByname('T_NORUN').Asboolean;
    aInfo.NeedLibrary:=Qry.FieldByname('T_NEEDLIBRARY').Asboolean;
    aInfo.KnownRunError:=Qry.FieldByname('T_KNOWNRUNERROR').AsInteger;
    aInfo.Known:=Qry.FieldByname('T_Known').Asboolean;
    aInfo.Note:=Qry.FieldByname('T_NOTE').AsString;
    aInfo.Description:=Qry.FieldByname('T_DESCRIPTION').AsString;
    aInfo.Source:=Qry.FieldByname('T_SOURCE').AsString;
    aInfo.Opts:=Qry.FieldByname('T_OPTS').AsString;
    aInfo.DelOptions:=Qry.FieldByname('T_DELOPTS').AsString;
    aInfo.SkipCPU:=Qry.FieldByname('T_SKIPCPU').AsString;
    aInfo.SkipEmu:=Qry.FieldByname('T_SKIPEMU').AsString;
    aInfo.NeedTarget:=Qry.FieldByname('T_NEEDTARGET').AsString;
    aInfo.SkipTarget:=Qry.FieldByname('T_SKIPTARGET').AsString;
    aInfo.MaxVersion:=Qry.FieldByname('T_MAXVERSION').AsString;
    aInfo.KnownRunNote:=Qry.FieldByname('T_KNOWNRUNNOTE').AsString;
    aInfo.KnownCompileNote:=Qry.FieldByname('T_KNOWNCOMPILENOTE').AsString;
    aInfo.RecompileOpt:=Qry.FieldByname('T_RECOMPILEOPT').AsString;
    aInfo.KnownCompileError:=Qry.FieldByname('T_KNOWNCOMPILEERROR').AsInteger;
    aInfo.NeededAfter:=Qry.FieldByname('T_NEEDEDAFTER').AsBoolean;
    aInfo.IsKnownRunError:=Qry.FieldByname('T_ISKNOWNRUNERROR').AsBoolean;
    aInfo.Timeout:=Qry.FieldByname('T_TIMEOUT').AsInteger;
    aInfo.Category:=Qry.FieldByname('T_CATEGORY').AsString;
    aInfo.Files:=Qry.FieldByname('T_FILES').AsString;
    aInfo.ConfigFileSrc:=Qry.FieldByname('T_CONFIGFILESRC').AsString;
    aInfo.ConfigFileDst:=Qry.FieldByname('T_CONFIGFILEDST').AsString;
    aInfo.WpoParas:=Qry.FieldByname('T_WPOPARAS').AsString;
    aInfo.WpoPasses:=Qry.FieldByname('T_WPOPASSES').AsInteger;
    aInfo.DelFiles:=Qry.FieldByname('T_DELFILES').AsString;
    aInfo.ExpectMsgs:=SplitID(Qry.FieldByname('T_EXPECTMSGS').AsString);

  finally
    Qry.Free;
  end;
end;

function TTestSQL.AddRun(const aData : TTestRunData): Int64;

Const
  SInsertRun = 'INSERT INTO TESTRUN '+
               '(TU_PLATFORM_FK, TU_SUBMITTER, TU_DATE, '+
               ' TU_COMPILERDATE, TU_COMPILERFULLVERSION, TU_COMPILERREVISION, '+
               ' TU_TESTSREVISION, TU_RTLREVISION, TU_PACKAGESREVISION  )'+
               ' VALUES '+
               '(%d,''%s'',''%s'', '+
               ' ''%s'',''%s'',''%s'', '+
               ' ''%s'',''%s'',''%s'' '+
               ') RETURNING TU_ID';

var
  Qry : string;
  PreviousID : Int64;

begin
  With aData do
    qry:=Format(SInsertRun,[PlatformID,
                            EscapeSQL(Submitter),
                            SQLDate(Date),
                            EscapeSQL(CompilerDate),
                            EscapeSQL(CompilerFullVersion),
                            EscapeSQL(CompilerRevision),
                            EscapeSQL(TestsRevision),
                            EscapeSQL(RTLRevision),
                            EscapeSQL(PackagesRevision)]);
  Result:=IDQuery(Qry);
  PreviousID:=GetLastRunByPlatformAndDate(aData.PlatformID,aData.Date);
  if PreviousID<>-1 then
    InsertTestHistory(Result,PreviousID);
end;


function TTestSQL.InsertTestHistory(TestRunID,TestPreviousID : Integer) : boolean;

const
  SQLInsert = 'INSERT INTO TESTRUNHISTORY '+
              '  (TH_ID_FK,TH_PREVIOUS_FK) '+
              'VALUES '+
              '  (%d,%d) '+
              'ON CONFLICT (TH_ID_FK) '+
              'DO UPDATE SET '+
              '  TH_PREVIOUS_FK=EXCLUDED.TH_PREVIOUS_FK';
var
  qry : string;

begin
  Qry:=format(SQLInsert,[TestRunID,TestPreviousID]);
  Result:=ExecuteQuery(Qry,False);
end;




function TTestSQL.AddTest(Name: String; AddSource: Boolean): Integer;

Const
  SInsertTest = 'INSERT INTO TESTS (T_NAME,T_ADDDATE)'+
                ' VALUES (''%s'',NOW()) ON CONFLICT (T_NAME) DO UPDATE SET T_ADDDATE=NOW() RETURNING T_ID';

Var
  Info : TConfig;
  lSrcDir : String;
  lFileName : string;
begin
  Info:=Default(TConfig);
  Result:=-1;
  lSrcDir:=IncludeTrailingPathDelimiter(TestSrcDir+RelSrcDir);
  lFileName:=ExpandFileName(lSrcDir+Name);
  Verbose(V_Debug,'Checking test filename: '+lFileName);
  Result:=IDQuery(Format(SInsertTest,[Name]));
  If Result=-1 then
    begin
    Verbose(V_WARNING,'Could not add test!');
    exit;
    end;
  If (FileExists(lFileName) and GetConfig(logprefix,lFileName,Info))
     or GetUnitTestConfig(logprefix,Name,lSrcDir,Info) then
    begin
    If AddSource then
      UpdateTest(Result,Info,tsutils.GetFileContents(Name))
    else
      UpdateTest(Result,Info,'');
    end
  else
    Verbose(V_WARNING,'Could not find test "'+Name+'" or info about this test.');
end;


function TTestSQL.UpdateTest(ID: Integer; Info: TConfig; const Source: String): Boolean;

Const
  SQLUpdateTest = 'Update TESTS SET '+
                  ' %s '+
                  'WHERE'+
                  ' (T_ID=%d)';

  function JoinIDS(IDS: Array of Integer) : string;
  var
    S : String;
    I : Integer;
  begin
    S:='';
    For I:=0 to Length(IDS)-1 do
      begin
      if I>0 then
        S:=S+',';
      S:=S+IntToStr(IDS[i]);
      end;
    Result:=S;
  end;

  procedure AddField(var S : String; const aName,aValue : String);
  begin
    if S<>'' then
      S:=S+', ';
    S:=S+Format('%s = ''%s''',[aName,EscapeSQl(aValue)])
  end;
  procedure AddField(var S : String; const aName : String; aValue : Integer);
  begin
    if S<>'' then
      S:=S+', ';
    S:=S+Format('%s = %d',[aName,aValue])
  end;

Var
  Qry :  String;

begin
  Qry:='';
  With Info do
    begin
    AddField(Qry,'T_CPU',NeedCPU);
    AddField(Qry,'T_OS',OS);
    AddField(Qry,'T_VERSION',MinVersion);
    AddField(Qry,'T_GRAPH',Bools[usesGraph]);
    AddField(Qry,'T_INTERACTIVE',Bools[IsInteractive]);
    AddField(Qry,'T_RESULT',ResultCode);
    AddField(Qry,'T_FAIL',Bools[ShouldFail]);
    AddField(Qry,'T_RECOMPILE',Bools[NeedRecompile]);
    AddField(Qry,'T_NORUN',Bools[NoRun]);
    AddField(Qry,'T_DESCRIPTION',Description);
    AddField(Qry,'T_NEEDLIBRARY',Bools[NeedLibrary]);
    AddField(Qry,'T_KNOWNRUNERROR',KnownRunError);
    AddField(Qry,'T_KNOWN',Bools[IsKnownCompileError]);
    AddField(Qry,'T_Note',Note);
    AddField(Qry,'T_OPTS',NeedOptions);
    AddField(Qry,'T_DELOPTS',DelOptions);
    AddField(Qry,'T_SKIPCPU',SkipCPU);
    AddField(Qry,'T_SKIPEMU',SkipEmu);
    AddField(Qry,'T_NEEDTARGET',NeedTarget);
    AddField(Qry,'T_SKIPTARGET',SkipTarget);
    AddField(Qry,'T_MAXVERSION',MaxVersion);
    AddField(Qry,'T_KNOWNRUNNOTE',KnownRunNote);
    AddField(Qry,'T_KNOWNCOMPILENOTE',KnownCompileNote);
    AddField(Qry,'T_RECOMPILEOPT',RecompileOpt);
    AddField(Qry,'T_KNOWNCOMPILEERROR',KnownCompileError);
    AddField(Qry,'T_NEEDEDAFTER',Bools[NeededAfter]);
    AddField(Qry,'T_ISKNOWNRUNERROR',Bools[IsKnownRunError]);
    AddField(Qry,'T_TIMEOUT', Timeout);
    AddField(Qry,'T_CATEGORY',Category);
    AddField(Qry,'T_FILES',Files);
    AddField(Qry,'T_CONFIGFILESRC',ConfigFileSrc);
    AddField(Qry,'T_CONFIGFILEDST',ConfigFileDst);
    AddField(Qry,'T_WPOPARAS',WpoParas);
    AddField(Qry,'T_WPOPASSES',WpoPasses);
    AddField(Qry,'T_DELFILES',DelFiles);
    AddField(Qry,'T_EXPECTMSGS',JoinIDS(ExpectMsgs));
    If (Source<>'') then
      AddField(Qry,'T_SOURCE',Source);
    end;
  Qry:=Format(SQLUpdateTest,[Qry,ID]);
  Result:=ExecuteQuery(Qry,False);
end;

function TTestSQL.UpdateTestResult(aData: TTestResultData): Int64;

const
  SQLUpdate = 'UPDATE TESTRESULTS SET '+
              '  TR_RESULT = %d, '+
              '  TR_TESTRUN_FK = %d, '+
              '  TR_OK = ''%s'', '+
              '  TR_SKIP = ''%s'', '+
              '  TR_LOG = ''%s'' '+
              'WHERE (TR_ID=%d)';
var
  Qry : String;
  OK, Skipped : Boolean;

begin
  with aData do
    begin
    OK:=TestOK[TestResult];
    Skipped:=TestSkipped[TestResult];
    Qry:=Format(SQLUpdate, [Ord(TestResult),RunID,Bools[OK],Bools[Skipped],EscapeSQL(Log),aData.ID]);
    Result:=aData.ID;
    end;
  ExecuteQuery(Qry,False);
end;

function TTestSQL.AddTestResult(aData: TTestResultData): Int64;

Const
  SQLInsert = 'Insert into TESTRESULTS '+
              '  (TR_TEST_FK,TR_TESTRUN_FK,TR_OK,TR_SKIP,TR_RESULT,TR_LOG) '+
              'VALUES '+
              '  (%d,%d,''%s'',''%s'',%d, ''%s'') '+
              'ON CONFLICT (TR_TEST_FK,TR_TESTRUN_FK) '+
              'DO UPDATE SET '+
              '  TR_OK = EXCLUDED.TR_OK, '+
              '  TR_SKIP = EXCLUDED.TR_SKIP, '+
              '  TR_RESULT = EXCLUDED.TR_RESULT, '+
              '  TR_LOG = EXCLUDED.TR_LOG '+
              'RETURNING TR_ID ';

Var
  Qry : String;
  OK, Skipped : Boolean;

begin
  Result:=-1;
  With aData do
    begin
    OK:=TestOK[TestResult];
    Skipped:=TestSkipped[TestResult];
    Qry:=Format(SQLInsert, [TestID,RunID,Bools[OK],Bools[Skipped],Ord(TestResult),EscapeSQL(Log)]);
    end;
  Result:=ID64Query(Qry);
end;

function TTestSQL.GetLastTestResult(aTestID, aPlatFormID: Integer): TTestResultData;

Const
  SQLSelect = 'SELECT TESTRESULTS.*, TU_DATE FROM '+
              ' TESTLASTRESULTS '+
              ' INNER JOIN TESTRESULTS ON (TL_TESTRESULTS_FK=TR_ID) '+
              ' INNER JOIN TESTRUN ON (TR_TESTRUN_FK=TU_ID) '+
              'WHERE '+
              ' (TL_TEST_FK=%d) '+
              ' AND (TL_PLATFORM_FK=%d)';

var
  Qry : TSQLQuery;
  S : String;

begin
  Result:=Default(TTestResultData);
  Result.TestID:=aTestID;
  Result.PlatformID:=aPlatformID;
  S:=Format(SQLSelect,[aTestID,aPlatformID]);
  Qry:=CreateQuery(S);
  Verbose(V_SQL,'GetLastTestResult: '+s);
  try
    Qry.Open;
    If not Qry.IsEmpty then
      begin
      Result.ID:=Qry.FieldByName('TR_ID').AsLargeInt;
      Result.TestResult:=TTestStatus(Qry.FieldByName('TR_RESULT').AsInteger);
      Result.RunID:=Qry.FieldByName('TR_TESTRUN_FK').AsLargeInt;
      Result.Log:=Qry.FieldByName('TR_LOG').AsString;
      Result.Date:=Qry.FieldByName('TU_DATE').AsDateTime;
      end
    else
      Result.ID:=-1;
  finally
    if Qry.SQLTransaction.Active then
      Qry.SQLTransaction.Commit;
    Qry.Free;
  end;

end;

function TTestSQL.GetFailCount(aRunID: Int64): Int64;
const
  SQLSelectFailCount =
               'SELECT (TU_FAILEDTOCOMPILE + TU_FAILEDTOFAIL + TU_FAILEDTORUN) as thecount '+
              ' FROM TESTRUN WHERE (TU_ID=%d)';

begin
  Result:=ID64Query(Format(SQLSelectFailCount,[aRunID]));
end;

function TTestSQL.AddLastResult(TestID, PlatformID: Integer; ResultID: Int64) : Boolean;

const
  SQLInsert = 'Insert into TESTLASTRESULTS '+
             '  (TL_TEST_FK,TL_PLATFORM_FK,TL_TESTRESULTS_FK) '+
             'VALUES '+
             '  (%d,%d,%d) '+
             'ON CONFLICT (TL_TEST_FK,TL_PLATFORM_FK) '+
             'DO UPDATE SET TL_TESTRESULTS_FK = EXCLUDED.TL_TESTRESULTS_FK ';

begin
  Result:=ExecuteQuery(Format(SQLInsert,[TestId,PlatFormID,ResultID]),False);
end;

function TTestSQL.AddPreviousResult(TestID, PlatformID: Integer; ResultID: Int64): Boolean;
const
  SQLInsert = 'Insert into TESTPREVIOUSRESULTS '+
             '  (TPR_TEST_FK,TPR_PLATFORM_FK,TPR_TESTRESULTS_FK) '+
             'VALUES '+
             '  (%d,%d,%d) '+
             'ON CONFLICT (TPR_TEST_FK,TPR_PLATFORM_FK) '+
             'DO UPDATE SET TPR_TESTRESULTS_FK = EXCLUDED.TPR_TESTRESULTS_FK ';

begin
  Result:=ExecuteQuery(Format(SQLInsert,[TestId,PlatFormID,ResultID]),False);
end;

function TTestSQL.AddCheckAllRtlLog(aCheckAllRTLID : int64; aStep : Byte; const aLog : String): Int64;

const
  SQLInsertLog = 'INSERT INTO public.checkallrtllog '+
	'  (cal_checkallrtl_fk, cal_step, cal_log) '+
	'VALUES '+
        '  (:cal_checkallrtl_fk, :cal_step, :cal_log) '+
	'returning cal_id';

var
  Qry : TSQLQuery;
begin
  Qry:=CreateQuery(SQLInsertLog);
  try
    Qry.ParamByName('cal_checkallrtl_fk').AsLargeInt:=aCheckAllRTLID;
    Qry.ParamByName('cal_step').AsInteger:=aStep;
    Qry.ParamByName('cal_log').AsString:=aLog;
    Result:=GetIDQueryResult(Qry);
  finally
    Qry.Free;
  end;
end;

function TTestSQL.AddCheckAllRtl(aData: TCheckAllRTL): Int64;

const
  SQLInsertCAR =
             'INSERT INTO public.checkallrtl( '+
             '  ca_platform_fk, ca_date, ca_step1, ca_step2, ca_step3, ca_step4, ca_step5, ca_step6)'+
             'VALUES (:ca_platform_fk, :ca_date, :ca_step1, :ca_step2, :ca_step3, :ca_step4, :ca_step5, :ca_step6) '+
             '  returning ca_id';
var
  Qry : TSQLQuery;
  i : TCheckStage;

begin
  Qry:=CreateQuery(SQLInsertCar);
  try
    Qry.ParamByName('ca_platform_fk').AsInteger:=aData.Platform;
    Qry.ParamByName('ca_date').AsDateTime:=aData.Date;
    Qry.ParamByName('ca_step1').AsBoolean:=aData.Steps[1];
    Qry.ParamByName('ca_step2').AsBoolean:=aData.Steps[2];
    Qry.ParamByName('ca_step3').AsBoolean:=aData.Steps[3];
    Qry.ParamByName('ca_step4').AsBoolean:=aData.Steps[4];
    Qry.ParamByName('ca_step5').AsBoolean:=aData.Steps[5];
    Qry.ParamByName('ca_step6').AsBoolean:=aData.Steps[6];
    Qry.Open;
    Result:=GetIDQueryResult(Qry);
    if Result<>-1 then
      begin
      For I in TCheckStage do
        if (not aData.Steps[i]) and (aData.Logs[i]<>'') then
          AddCheckAllRtlLog(Result,i,aData.Logs[i]);
      end;
  finally
    Qry.Free;
  end;
end;

function TTestSQL.UpdateTestRun(aData: TTestRunData): Boolean;
var
  Qry : string;
  I : TValidTestStatus;

  Procedure AddTo(S : String);

  begin
    if Qry<>'' then
      Qry:=Qry+' , ';
    Qry:=Qry+S;
  end;

begin
  Qry:='';
  for I in TValidTestStatus do
    AddTo(format('%s=%d',[SQLField[i],aData.StatusCount[i]]));
  qry:='UPDATE TESTRUN SET '+Qry+' WHERE TU_ID='+format('%d',[aData.RunID]);
  ExecuteQuery(Qry,False);
  Result:=True;
end;

function TTestSQL.GetFailCount(aRunID: Integer): Int64;
begin
  Result:=ID64Query(Format('SELECT (TU_FAILEDTOCOMPILE + TU_FAILEDTOFAIL + TU_FAILEDTORUN) FROM TESTRUN WHERE (TU_ID=%d)',[aRunID]));
end;

function TTestSQL.RequireTestID(const aName: String): Integer;

begin
  Result:=GetTestID(aName);
  If Result=-1 then
    Result:=AddTest(aName,True);
  If Result=-1 then
    Verbose(V_WARNING,'Could not find or create entry for test '+aName);
end;

function TTestSQL.CleanTestRun(ID: Integer): Boolean;

Const
  SDeleteRun = 'DELETE FROM TESTRESULTS WHERE TR_TESTRUN_FK=%d';

begin
  Result:=ExecuteQuery(Format(SDeleteRun,[ID]),False);
end;

function TTestSQL.GetPreviousRunID(RunID: Int64): Int64;

begin
  Result:=ID64Query(Format('SELECT TH_PREVIOUS_FK FROM TESTRUNHISTORY WHERE (TH_ID_FK=%d)',[RunID]));
end;

function TTestSQL.GetNextRunID(RunID: Int64): Int64;

begin
  Result:=ID64Query(Format('SELECT TH_ID_FK FROM TESTRUNHISTORY WHERE (TH_PREVIOUS_FK=%d)',[RunID]));
end;


end.
