unit Main;

interface

uses
  Vcl.Forms, System.Classes, Vcl.Controls, Vcl.StdCtrls, Vcl.ComCtrls,
  ORM_utils, interposer_class, SysUtils, Dialogs, formgerenc_Classes, DM1,
  Vcl.WinXPickers, System.Rtti, System.TypInfo, Vcl.ExtCtrls;

type
  TMainForm = class(TForm)
    EditUserName: TEdit;
    EditPassword: TEdit;
    LabelUserName: TLabel;
    ButtonRegister: TButton;
    ButtonUpdateORM: TButton; // Button to update ORM property programmatically
    DatePickerProjectStart: TDatePicker;
    procedure ButtonRegisterClick(Sender: TObject);
    procedure ButtonUpdateORMClick(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure EditExitHandler(Sender: TObject); // Shared OnExit event handler
    procedure FormShow(Sender: TObject);
  private
    FQueryRec1: TEmployeeProject;
    FQueryRec2: TEmployeeProject;
    procedure SetupComponents;
  public
    /// <summary>
    /// Retrieves a component bound to a specific ORM record and field name.
    /// </summary>
    function RetrieveComponent(ORMRec: TQryRecInfo; FieldName: string): TComponent;

    /// <summary>
    /// Sets the value of a property in an ORM record using RTTI.
    /// </summary>
    procedure SetORMProperty(ORMRec: TQryRecInfo; const FieldName: string; const Value: TValue);

    /// <summary>
    /// Updates all UI components based on their bound ORM records.
    /// </summary>
    procedure UpdateAllComponents;
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

uses
  Data.DB, Uni;

{ TMainForm }

procedure TMainForm.ButtonRegisterClick(Sender: TObject);
begin
  // Unregister the EditPassword component from ORM binding
  ORMManager.UnregisterComponent(EditPassword);

  // Retrieve and display the component bound to 'EmployeeName'
  var MyComp := RetrieveComponent(FQueryRec2, 'EmployeeName');
  if Assigned(MyComp) then
    ShowMessage('Component bound to EmployeeName: ' + MyComp.Name)
  else
    ShowMessage('Component EmployeeName not found.');

  // Retrieve and display the component bound to 'EmployeeID'
  MyComp := RetrieveComponent(FQueryRec2, 'EmployeeID');
  if Assigned(MyComp) then
    ShowMessage('Component bound to EmployeeID: ' + MyComp.Name)
  else
    ShowMessage('Component EmployeeID not found.');
end;

procedure TMainForm.ButtonUpdateORMClick(Sender: TObject);
begin
  // Programmatically update the EmployeeName property in FQueryRec1
  FQueryRec1.EmployeeName := 'Programmatic Name Change';

  ShowMessage('ORM Property EmployeeName has been updated programmatically.');
end;

procedure TMainForm.EditExitHandler(Sender: TObject);
var
  Edit: TEdit;
  ORMRec: TQryRecInfo;
  FieldName: string;
  NewValue: TValue;
begin
  // Ensure the sender is a TEdit component
  if not (Sender is TEdit) then
    Exit;

  Edit := TEdit(Sender);

  // Retrieve ORMRec and FieldName using TComponentHelper properties
  ORMRec := Edit.GetORMRec;
  FieldName := Edit.GetORMRecField;

  if not Assigned(ORMRec) or (FieldName = '') then
    Exit;

  // Get the new value from the TEdit component
  NewValue := TValue.From<string>(Edit.Text);

  // Update the ORMRec's property using RTTI
  try
    SetORMProperty(ORMRec, FieldName, NewValue);
    ShowMessage(Format('%s ORM property updated to: %s', [FieldName, NewValue.AsString]));
  except
    on E: Exception do
      ShowMessage('Error updating ORM property: ' + E.Message);
  end;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  // Clean up ORM records
  FQueryRec1.Free;
  FQueryRec2.Free;
end;

procedure TMainForm.FormShow(Sender: TObject);
begin
  // Initialize the ORM record instances
  FQueryRec1 := TEmployeeProject.Create;
  FQueryRec2 := TEmployeeProject.Create;

  // Setup components with ORM properties and register them
  SetupComponents;

  // Connect to the local database
  // Assuming DM is a data module with a method ConnectLocal
  DM.ConnectLocal;

  // Fetch data into ORM records using DM.LocalServer
  FQueryRec1.Fetch(DM.LocalServer, 'SELECT employeename,employeeid,departmentid,projectid,projectname,projectbudget,projectdate,projectstartmoment,projectbudget * 2 as budgetlimit, age, older FROM orm_employee WHERE employeeid=12');
  FQueryRec2.Fetch(DM.LocalServer, 'SELECT employeename,employeeid,departmentid,projectid,projectname,projectbudget,projectdate,projectstartmoment,projectbudget * 2 as budgetlimit, age, older FROM orm_employee WHERE employeeid=13');

  // Initialize UI components with ORM record data
  UpdateAllComponents;

  // Assign the shared OnExit event handler to all registered TEdit components
  for var Comp in ORMManager.GetAllComponents do
    if Comp is TEdit then
      TEdit(Comp).OnExit := EditExitHandler;
end;

procedure TMainForm.SetupComponents;
begin
  // Register EditUserName with FQueryRec1 and FieldName 'EmployeeName'
  ORMManager.RegisterComponent(EditUserName, FQueryRec1, 'EmployeeName'); // Maps to TEdit.Text

  // Register EditPassword with FQueryRec2 and FieldName 'ProjectName'
  ORMManager.RegisterComponent(EditPassword, FQueryRec2, 'ProjectName'); // Maps to TEdit.Text

  // Register LabelUserName with FQueryRec1 and FieldName 'BudgetLimit'
  ORMManager.RegisterComponent(LabelUserName, FQueryRec1, 'BudgetLimit'); // Maps to TLabel.Caption

  // Register DatePickerProjectStart with FQueryRec2 and FieldName 'ProjectStartMoment'
  ORMManager.RegisterComponent(DatePickerProjectStart, FQueryRec2, 'ProjectStartMoment'); // Maps to TDatePicker.Date
end;

function TMainForm.RetrieveComponent(ORMRec: TQryRecInfo; FieldName: string): TComponent;
begin
  // Find and return the component based on ORMRec and FieldName
  Result := ORMManager.FindComponent(ORMRec, FieldName);
end;

procedure TMainForm.SetORMProperty(ORMRec: TQryRecInfo; const FieldName: string; const Value: TValue);
var
  Prop: TRttiProperty;
  Ctx: TRttiContext;
  ConvertedValue: TValue;
begin
  Ctx := TRttiContext.Create;
  try
    Prop := Ctx.GetType(ORMRec.ClassType).GetProperty(FieldName);
    if Assigned(Prop) and Prop.IsWritable then
    begin
      // Handle type conversions based on the property type
      case Prop.PropertyType.TypeKind of
        tkString, tkLString, tkWString, tkUString:
          ConvertedValue := TValue.From<string>(Value.AsString);
        tkInteger:
          ConvertedValue := TValue.From<Integer>(StrToIntDef(Value.AsString, 0));
        tkInt64:
          ConvertedValue := TValue.From<Int64>(StrToInt64Def(Value.AsString, 0));
        tkFloat:
          if Prop.PropertyType.Handle = TypeInfo(Currency) then
            ConvertedValue := TValue.From<Currency>(StrToCurrDef(Value.AsString, 0))
          else if Prop.PropertyType.Handle = TypeInfo(TDateTime) then
            ConvertedValue := TValue.From<TDateTime>(StrToDateDef(Value.AsString, 0))
          else
            ConvertedValue := Value;
        tkEnumeration:
          if Prop.PropertyType.Handle = TypeInfo(Boolean) then
            ConvertedValue := TValue.From<Boolean>(UpperCase(Value.AsString) = 'S') // Assuming 'S' for True
          else
            ConvertedValue := Value;
        // Add more type conversions as needed...
        else
          ConvertedValue := Value;
      end;

      // Set the property value
      Prop.SetValue(ORMRec, ConvertedValue);
    end
    else
      raise EORMUtilsException.CreateFmt('Property "%s" not found or not writable in ORMRec.', [FieldName]);
  finally
    // TRttiContext is a record and managed automatically
  end;
end;

procedure TMainForm.UpdateAllComponents;
begin
  // Iterate through all mappings and update UI components
  for var Comp in ORMManager.GetAllComponents do
  begin
    if Comp is TEdit then
      ORMManager.UpdateComponentFromORM(Comp, TEdit(Comp).GetORMRec, TEdit(Comp).GetORMRecField)
    else if Comp is TLabel then
      ORMManager.UpdateComponentFromORM(Comp, TLabel(Comp).GetORMRec, TLabel(Comp).GetORMRecField)
    else if Comp is TDatePicker then
      ORMManager.UpdateComponentFromORM(Comp, TDatePicker(Comp).GetORMRec, TDatePicker(Comp).GetORMRecField)
    else if Comp is TComboBox then
      ORMManager.UpdateComponentFromORM(Comp, TComboBox(Comp).GetORMRec, TComboBox(Comp).GetORMRecField)
    // Add more component types as needed...
    else
      ShowMessage('Unsupported component type: ' + Comp.ClassName);
  end;
end;

end.

-------------------------

unit interposer_class;

interface

uses
  Vcl.Forms, Vcl.Controls, CommCtrl, Messages, sPanel, sEdit, sPageControl,
  System.Classes, AdvCGrid, Uni, pixel_classes, ORM_utils, SysUtils, Dialogs,
  System.Generics.Defaults, Generics.Collections, System.Hash, System.Rtti,
  Vcl.StdCtrls, Vcl.WinXCtrls, Vcl.WinXPickers;

{$TYPEINFO ON}

type
  /// <summary>
  /// Represents the key used in the component-ORM mapping dictionary.
  /// Combines a reference to an ORM record with a specific field name.
  /// </summary>
  TORMKey = record
    ORMRec: TQryRecInfo;
    FieldName: string;
  end;

  /// <summary>
  /// Custom comparer for TORMKey to handle equality checks and hashing.
  /// </summary>
  TORMKeyComparer = class(TEqualityComparer<TORMKey>)
    function Equals(const Left, Right: TORMKey): Boolean; override;
    function GetHashCode(const Key: TORMKey): Integer; override;
  end;

  /// <summary>
  /// Manages the binding between UI components and ORM records.
  /// </summary>
  TORMManager = class
  private
    FComponentDict: TDictionary<TORMKey, TComponent>;
  public
    constructor Create;
    destructor Destroy; override;

    /// <summary>
    /// Registers a component with its corresponding ORM record and field name.
    /// </summary>
    procedure RegisterComponent(Component: TComponent; ORMRec: TQryRecInfo; const FieldName: string);

    /// <summary>
    /// Unregisters a component from ORM binding.
    /// </summary>
    procedure UnregisterComponent(Component: TComponent);

    /// <summary>
    /// Finds the component associated with a specific ORM record and field name.
    /// </summary>
    function FindComponent(ORMRec: TQryRecInfo; const FieldName: string): TComponent;

    /// <summary>
    /// Retrieves all registered components.
    /// </summary>
    function GetAllComponents: TArray<TComponent>;

    /// <summary>
    /// Updates a component's UI based on the latest ORM record data.
    /// </summary>
    procedure UpdateComponentFromORM(Component: TComponent; ORMRec: TQryRecInfo; const FieldName: string);

    /// <summary>
    /// Retrieves the ORM record associated with a given component.
    /// </summary>
    function GetORMRecForComponent(Component: TComponent): TQryRecInfo;

    /// <summary>
    /// Retrieves the field name associated with a given component.
    /// </summary>
    function GetFieldNameForComponent(Component: TComponent): string;
  end;

  /// <summary>
  /// Helper methods extending TComponent to interface with ORMManager.
  /// </summary>
  TComponentHelper = class helper for TComponent
  public
    /// <summary>
    /// Retrieves the ORM record associated with the component.
    /// </summary>
    function GetORMRec: TQryRecInfo;

    /// <summary>
    /// Retrieves the field name associated with the component.
    /// </summary>
    function GetORMRecField: string;
  end;

  // Interposer Classes (e.g., TsPanel, TsEdit, etc.)
  TsPanel = class(sPanel.TsPanel)
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  end;

  TsEdit = class(sEdit.TsEdit)
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  end;

  TsPageControl = class(sPageControl.TsPageControl)
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  private
    procedure TCMAdjustRect(var Msg: TMessage); message TCM_ADJUSTRECT;
  end;

  TAdvColumnGrid = class(AdvCGrid.TAdvColumnGrid)
  private
    FByPassAF: TArray<Boolean>;
    function GetCellz(ACol, ARow: Integer): TZString;
    procedure SetCellz(ACol, ARow: Integer; const Value: TZString);
    function GetBypassAF(ACol: Integer): Boolean;
    procedure SetBypassAF(ACol: Integer; const Value: Boolean);
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  public
    property Cellz[ACol, ARow: Integer]: TZString read GetCellz write SetCellz;
    property BypassAutoFit[ACol: Integer]: Boolean read GetBypassAF write SetBypassAF;
  published
    function CriaLinhaObjeto(MyObject: TObject): Integer;
    function CriaLinha: Integer;
    function GetColumnIndexByName(const nome: string): Integer;
    procedure SetBasicColumnConfig;
  end;

// Declare ORMManager as a global variable in the interface for accessibility
var
  ORMManager: TORMManager;

implementation

uses
  pixel_utils, System.Math, System.Types, Windows, TypInfo,
  Data.DB, Vcl.Graphics, ComCtrls, Vcl.Grids, Vcl.ExtCtrls;

// =============================
// TORMKeyComparer Implementation
// =============================

function TORMKeyComparer.Equals(const Left, Right: TORMKey): Boolean;
begin
  Result := (Left.ORMRec = Right.ORMRec) and SameText(Left.FieldName, Right.FieldName);
end;

function TORMKeyComparer.GetHashCode(const Key: TORMKey): Integer;
begin
  Result := Integer(NativeUInt(Key.ORMRec)) xor THashBobJenkins.GetHashValue(LowerCase(Key.FieldName));
end;

// =============================
// TORMManager Implementation
// =============================

constructor TORMManager.Create;
begin
  inherited Create;
  FComponentDict := TDictionary<TORMKey, TComponent>.Create(TORMKeyComparer.Create);
end;

destructor TORMManager.Destroy;
begin
  FComponentDict.Free;
  inherited;
end;

procedure TORMManager.RegisterComponent(Component: TComponent; ORMRec: TQryRecInfo; const FieldName: string);
var
  Key: TORMKey;
begin
  Key.ORMRec := ORMRec;
  Key.FieldName := FieldName;

  if not FComponentDict.ContainsKey(Key) then
  begin
    FComponentDict.Add(Key, Component);
  end;
end;

procedure TORMManager.UnregisterComponent(Component: TComponent);
var
  Key: TORMKey;
  CopyKeys: TArray<TORMKey>;
  i: Integer;
begin
  // To safely remove components, first copy the keys
  CopyKeys := FComponentDict.Keys.ToArray;
  for i := 0 to Length(CopyKeys) - 1 do
  begin
    Key := CopyKeys[i];
    if FComponentDict[Key] = Component then
      FComponentDict.Remove(Key);
  end;
end;

function TORMManager.FindComponent(ORMRec: TQryRecInfo; const FieldName: string): TComponent;
var
  Key: TORMKey;
begin
  Key.ORMRec := ORMRec;
  Key.FieldName := FieldName;
  if FComponentDict.TryGetValue(Key, Result) then
    Exit
  else
    Result := nil;
end;

function TORMManager.GetAllComponents: TArray<TComponent>;
var
  Comp: TComponent;
  CompList: TList<TComponent>;
begin
  CompList := TList<TComponent>.Create;
  try
    for Comp in FComponentDict.Values do
      CompList.Add(Comp);
    Result := CompList.ToArray;
  finally
    CompList.Free;
  end;
end;

procedure TORMManager.UpdateComponentFromORM(Component: TComponent; ORMRec: TQryRecInfo; const FieldName: string);
var
  FormattedText: string;
  DateValue: TDateTime;
begin
  FormattedText := ORMRec.GetFormattedText(FieldName);

  if Component is TEdit then
    TEdit(Component).Text := FormattedText
  else if Component is TLabel then
    TLabel(Component).Caption := FormattedText
  else if Component is TDatePicker then
  begin
    if TryStrToDate(FormattedText, DateValue) then
      TDatePicker(Component).Date := DateValue
    else
    begin
      // Handle invalid date appropriately
      ShowMessage(Format('Invalid date format for component: %s', [Component.Name]));
      // Optionally, set to a default date, e.g., current date
      // TDatePicker(Component).Date := Date;
    end;
  end
  else if Component is TComboBox then
    TComboBox(Component).Text := FormattedText
  // Add more component types as needed...
  else
    ShowMessage('Unsupported component type: ' + Component.ClassName);
end;

function TORMManager.GetORMRecForComponent(Component: TComponent): TQryRecInfo;
var
  Key: TORMKey;
begin
  Result := nil;
  for Key in FComponentDict.Keys do
    if FComponentDict[Key] = Component then
    begin
      Result := Key.ORMRec;
      Exit;
    end;
end;

function TORMManager.GetFieldNameForComponent(Component: TComponent): string;
var
  Key: TORMKey;
begin
  Result := '';
  for Key in FComponentDict.Keys do
    if FComponentDict[Key] = Component then
    begin
      Result := Key.FieldName;
      Exit;
    end;
end;

// =============================
// TComponentHelper Implementation
// =============================

function TComponentHelper.GetORMRec: TQryRecInfo;
begin
  Result := ORMManager.GetORMRecForComponent(Self);
end;

function TComponentHelper.GetORMRecField: string;
begin
  Result := ORMManager.GetFieldNameForComponent(Self);
end;

// =============================
// Interposer Classes Implementations
// =============================

procedure TsPanel.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  StrPCopy(Params.WinClassName, 'Painel');
end;

procedure TsEdit.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  StrPCopy(Params.WinClassName, 'Editor');
end;

procedure TsPageControl.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  StrPCopy(Params.WinClassName, 'PagCtrl');
end;

procedure TsPageControl.TCMAdjustRect(var Msg: TMessage);
begin
  inherited;
  case TabPosition of
    tpTop:
      begin
        PRect(Msg.LParam)^.Left := 0;
        PRect(Msg.LParam)^.Right := ClientWidth;
        PRect(Msg.LParam)^.Bottom := ClientHeight;
      end;
  end;
end;

procedure TAdvColumnGrid.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  StrPCopy(Params.WinClassName, 'Grid');
end;

function TAdvColumnGrid.GetCellz(ACol, ARow: Integer): TZString;
begin
  Result := Self.Cells[ACol, ARow];
end;

procedure TAdvColumnGrid.SetCellz(ACol, ARow: Integer; const Value: TZString);
begin
  Self.Cells[ACol, ARow] := string(Value);
end;

function TAdvColumnGrid.GetBypassAF(ACol: Integer): Boolean;
begin
  Result := FByPassAF[ACol];
end;

procedure TAdvColumnGrid.SetBypassAF(ACol: Integer; const Value: Boolean);
begin
  if Length(FByPassAF) < ACol + 1 then
    SetLength(FByPassAF, ACol + 1);
  FByPassAF[ACol] := Value;
end;

function TAdvColumnGrid.CriaLinha: Integer;
begin
  Self.RowCount := Self.RowCount + 1;
  Self.ClearRows(Self.RowCount - 1, 1);
  Result := Self.RowCount - 1;
end;

function TAdvColumnGrid.CriaLinhaObjeto(MyObject: TObject): Integer;
begin
  Self.RowCount := Self.RowCount + 1;
  Self.ClearRows(Self.RowCount - 1, 1);
  Self.Objects[0, Self.RowCount - 1] := MyObject;
  Result := Self.RowCount - 1;
end;

function TAdvColumnGrid.GetColumnIndexByName(const nome: string): Integer;
begin
  Result := 0;
  for var i := 1 to Pred(Self.ColCount) do
    if Self.Columns[i].Name = nome then
      Exit(i);
end;

procedure TAdvColumnGrid.SetBasicColumnConfig;
var
  i: Integer;
begin
  with Self do
  begin
    SuppressColumn(0);
    for i := 1 to ColCount - 1 do
      if Columns[i].Tag = 0 then
      begin
        Columns[i].HeaderAlignment := taCenter;
        Columns[i].HeaderFont.Name := 'Tahoma';
        Columns[i].HeaderFont.Size := MulDiv(8, 96, Self.CurrentPPI);
        Columns[i].HeaderFont.Color := clWhite;
        Columns[i].HeaderFont.Style := [fsBold];
      end;

    for i := 1 to ColCount - 1 do
      if Columns[i].Tag = 0 then
      begin
        Columns[i].Font.Name := 'Verdana';
        Columns[i].Font.Size := MulDiv(10, 96, Self.CurrentPPI);
        Columns[i].Font.Style := [];
      end;

    for i := 1 to ColCount - 1 do
      if Columns[i].Tag = 0 then
      begin
        Columns[i].ShowBands := True;
        Colors[i, 0] := RGB($41, $3F, $42);
      end;

    for i := 1 to ColCount - 1 do
      Columns[i].Tag := 1;

    Bands.PrimaryColor := $00EFEFEF;
    Bands.SecondaryColor := $00F9F9F9;
    MouseActions.DirectEdit := True;
    MouseActions.DirectComboDrop := True;
    Bands.Active := True;
    ActiveCellShow := False;
    Navigation.SkipDisabledRows := True;
    Navigation.AutoComboSelect := False;
    ShowSelection := True;
    HideFocusRect := True;
    Options := Options - [goRangeSelect] + [goRowSelect];
    HoverRow := True;
    HoverRowColor := clYellow;
    ActiveRowShow := True;
    ActiveRowColor := clLime;
    SelectionColor := clLime;
    SelectionTextColor := clBlack;
    ColumnSize.Stretch := False;
    ColumnSize.StretchColumn := -1;
    ColumnSize.SynchWithGrid := False;
    FixedRowAlways := True;
    FixedRows := 1;
  end;
end;

// =============================
// Initialization and Finalization
// =============================

initialization
  ORMManager := TORMManager.Create;

finalization
  ORMManager.Free;

end.

-----------------------------

unit formgerenc_Classes;

{$TYPEINFO ON}

interface

uses
  orm_utils, pixel_classes, ORM_CustomAttributes;

type
  TEmployeeProject = class(TQryRecInfo)
  private
    [Table('employees'), PrimaryKey, RefColumn('emp_id')]
    _EmployeeID: TNullable<Integer>;
    [Table('employees'), PrimaryKey, RefColumn('dept_id')]
    _DepartmentID: TNullable<Integer>;
    [Table('employees'), RefColumn('emp_name')]
    _EmployeeName: TNullable<String>;
    [Table('projects'), PrimaryKey, RefColumn('proj_id')]
    _ProjectID: TNullable<Integer>;
    [Table('projects'), RefColumn('proj_name')]
    _ProjectName: TNullable<String>;
    [Table('projects'), RefColumn('proj_budget'), FmtProp(tiMoney)]
    _ProjectBudget: TNullable<Currency>;


    [FmtProp(tiMoney)]
    _BudgetLimit: Currency;

    _Age: Integer;
    _Older: Boolean;


    [Table('projects'), RefColumn('proj_date'), FmtProp(tiDate)]
    _ProjectDate: TNullable<TDateTime>;
    [Table('projects'), RefColumn('proj_starttime'), FmtProp(tiDateTime)]
    _ProjectStartMoment: TNullable<TDateTime>;

  published
    property EmployeeID: TNullable<Integer> index 0 read _EmployeeID write SetInfoNullableInteger;
    property DepartmentID: TNullable<Integer> index 1 read _DepartmentID write SetInfoNullableInteger;
    property EmployeeName: TNullable<String> index 2 read _EmployeeName write SetInfoNullableString;
    property ProjectID: TNullable<Integer> index 3 read _ProjectID write SetInfoNullableInteger;
    property ProjectName: TNullable<String> index 4 read _ProjectName write SetInfoNullableString;
    property ProjectBudget: TNullable<Currency> index 5 read _ProjectBudget write SetInfoNullableCurrency;
    property BudgetLimit: Currency index 6 read _BudgetLimit write SetInfoCurrency;
    property Age: Integer index 7 read _Age write SetInfoInteger;
    property Older: Boolean index 8 read _Older write SetInfoBoolean;
    property ProjectDate: TNullable<TDateTime> index 9 read _ProjectDate write SetInfoNullableDateTime;
    property ProjectStartMoment: TNullable<TDateTime> index 10 read _ProjectStartMoment write SetInfoNullableDateTime;
  end;

implementation

end.

--------------------------

unit pixel_classes;

interface

uses
 System.SysUtils, Rtti, TypInfo;

{$TYPEINFO ON}

type
 TZString = type string;

type
  TNullValue = record
  end;

  TNullable<T{: record}> = record
  public
    Value: T;
    HasValue: Boolean;

    class operator Implicit(const AValue: T): TNullable<T>;
    class operator Implicit(const AValue: TNullable<T>): T;
    class operator Implicit(const AValue: TNullValue): TNullable<T>;
    class operator Explicit(const AValue: T): TNullable<T>;

    // add these...
    class operator Equal(const A: TNullable<T>; const B: TNullValue): Boolean;
    class operator NotEqual(const A: TNullable<T>; const B: TNullValue): Boolean;
  end;

var NullValue: TNullValue;

implementation

class operator TNullable<T>.Implicit(const AValue: T): TNullable<T>;
begin
  Result.Value := AValue;
  Result.HasValue := True;
end;

class operator TNullable<T>.Implicit(const AValue: TNullable<T>): T;
begin
  if AValue.HasValue then
    Result := AValue.Value
  else
    Result := Default(T); // or raise an exception
end;

class operator TNullable<T>.Implicit(const AValue: TNullValue): TNullable<T>;
begin
  Result.Value := Default(T);
  Result.HasValue := False;
end;

class operator TNullable<T>.Explicit(const AValue: T): TNullable<T>;
begin
  Result.Value := AValue;
  Result.HasValue := True;
end;

class operator TNullable<T>.Equal(const A: TNullable<T>; const B: TNullValue): Boolean;
begin
  Result := not A.HasValue;
end;

class operator TNullable<T>.NotEqual(const A: TNullable<T>; const B: TNullValue): Boolean;
begin
  Result := A.HasValue;
end;

end.

-----------------

unit orm_utils;

interface

uses
  System.SysUtils,
  System.Classes,
  System.Generics.Collections,
  System.Rtti,
  System.TypInfo,
  Data.DB,
  Uni,
  pixel_classes,
  orm_customattributes;

type
  TQryRecInfo = class;

  // Define the event type
  TPropertyChangedEvent = procedure(Sender: TQryRecInfo; const PropertyName: string) of object;

  EORMUtilsException = class(Exception);

  /// <summary>
  /// Base class for query record information
  /// Use descendants of this class to represent specific database records.
  /// </summary>
  TQryRecInfo = class
  private
    FDirtyFields: TDictionary<string, Boolean>;
    FPreviousValues: TDictionary<string, TValue>;
    FOnPropertyChanged: TPropertyChangedEvent; // New event
    procedure SetInfo(Index: Integer; const Value: TValue);
  public
    constructor Create;
    destructor Destroy; override;

    // Event property
    property OnPropertyChanged: TPropertyChangedEvent read FOnPropertyChanged write FOnPropertyChanged;

    // Methods to set nullable fields
    procedure SetInfoNullableString(Index: Integer; const Value: TNullable<string>);
    procedure SetInfoNullableInteger(Index: Integer; const Value: TNullable<Integer>);
    procedure SetInfoNullableInt64(Index: Integer; const Value: TNullable<Int64>);
    procedure SetInfoNullableExtended(Index: Integer; const Value: TNullable<Extended>);
    procedure SetInfoNullableCurrency(Index: Integer; const Value: TNullable<Currency>);
    procedure SetInfoNullableDateTime(Index: Integer; const Value: TNullable<TDateTime>);
    procedure SetInfoNullableBoolean(Index: Integer; const Value: TNullable<Boolean>);
    procedure SetInfoNullableVariant(Index: Integer; const Value: TNullable<Variant>);

    // Methods to set non-nullable simple types
    procedure SetInfoCurrency(Index: Integer; const Value: Currency);
    procedure SetInfoString(Index: Integer; const Value: string);
    procedure SetInfoInteger(Index: Integer; const Value: Integer);
    procedure SetInfoInt64(Index: Integer; const Value: Int64);
    procedure SetInfoExtended(Index: Integer; const Value: Extended);
    procedure SetInfoDateTime(Index: Integer; const Value: TDateTime);
    procedure SetInfoBoolean(Index: Integer; const Value: Boolean);
    procedure SetInfoVariant(Index: Integer; const Value: Variant);

    // Methods to handle dirty fields
    function IsFieldDirty(const FieldName: string): Boolean;
    procedure ClearDirtyFlags;
    property DirtyFields: TDictionary<string, Boolean> read FDirtyFields;

    // Methods to handle previous values
    function GetPreviousValue(const FieldName: string): TValue;
    procedure ClearPreviousValues;
    property PreviousValues: TDictionary<string, TValue> read FPreviousValues;

    /// <summary>
    /// Retrieves the formatted text for a given field based on the FmtProp attribute.
    /// </summary>
    function GetFormattedText(const FieldName: string): string;

    /// <summary>
    /// Fetches data for this record from the database based on the provided SQL.
    /// </summary>
    procedure Fetch(DBConn: TUniConnection; const QryTxt: string);
  end;

  /// <summary>
  /// Generic class to handle arrays of TQryRecInfo descendants.
  /// </summary>
  TQryRecArray<T: class, constructor> = class
  private
    FQryRecInfoList: TObjectList<T>;
    FFilterSQL: string;
    FDBConn: TUniConnection;
    function GetCount: Integer;
  public
    constructor Create;
    destructor Destroy; override;

    /// <summary>
    /// Fetches all records based on the provided SQL text and populates the array.
    /// </summary>
    /// <param name="QryTxt">The base SQL query text.</param>
    procedure FetchAllInstance(DBConn: TUniConnection; const QryTxt: string);

    /// <summary>
    /// Clears any SQL filters applied.
    /// </summary>
    procedure ClearFilter;

    /// <summary>
    /// Accesses a record at the specified index.
    /// </summary>
    /// <param name="Index">The zero-based index of the record.</param>
    /// <returns>The record at the specified index.</returns>
    function Rec(Index: Integer): T;

    /// <summary>
    /// Number of records in the array.
    /// </summary>
    property RecCount: Integer read GetCount;

    /// <summary>
    /// Database connection used for fetching records.
    /// </summary>
    property DBConn: TUniConnection read FDBConn write FDBConn;

    /// <summary>
    /// Additional SQL filter to apply to the query.
    /// </summary>
    property SQLFilter: string read FFilterSQL write FFilterSQL;

    /// <summary>
    /// Provides support for for-in loops by returning an enumerator.
    /// </summary>
    function GetEnumerator: TEnumerator<T>;

    /// <summary>
    /// Factory method to create and fetch records in a single step.
    /// </summary>
    /// <param name="DBConn">Database connection.</param>
    /// <param name="QryTxt">SQL query text.</param>
    /// <returns>Populated instance of TQryRecArray<T>.</returns>
    class function FetchAll(DBConn: TUniConnection; const QryTxt: string): TQryRecArray<T>; static;
  end;

// Example descendant of TQryRecInfo (You can create your own)
  TDynRec = class(TQryRecInfo)
    // Define additional properties as needed
  end;

implementation

uses
  System.Variants,
  StrUtils,
  pixel_utils; // Ensure this unit is in your project and properly implemented

{ TQryRecInfo }

constructor TQryRecInfo.Create;
begin
  inherited Create;
  FDirtyFields := TDictionary<string, Boolean>.Create;
  FPreviousValues := TDictionary<string, TValue>.Create;
end;

destructor TQryRecInfo.Destroy;
begin
  FDirtyFields.Free;
  FPreviousValues.Free;
  inherited;
end;

procedure TQryRecInfo.ClearDirtyFlags;
begin
  FDirtyFields.Clear;
end;

procedure TQryRecInfo.ClearPreviousValues;
begin
  FPreviousValues.Clear;
end;

function TQryRecInfo.IsFieldDirty(const FieldName: string): Boolean;
begin
  Result := FDirtyFields.ContainsKey(FieldName) and FDirtyFields[FieldName];
end;

function TQryRecInfo.GetPreviousValue(const FieldName: string): TValue;
begin
  if not FPreviousValues.TryGetValue(FieldName, Result) then
    raise EORMUtilsException.CreateFmt('No previous value found for field "%s".', [FieldName]);
end;

function FormataInfo(info: Variant; tipo: TFormatType): string;
begin
  if VarIsNull(info) or VarIsEmpty(info) then
    Exit('');

  case tipo of
    tiString:
      Result := VarToStr(info);
    tiInteger:
      Result := IntToStr(Integer(info));
    tiMoney:
      Result := FormatFloat(FMT_MONEY, Currency(info));
    tiFloat:
      Result := FloatToStr(info);
    tiDate:
      if VarIsType(info, [varDate, varDouble]) then
      begin
        if VarToDateTime(info) < EncodeDate(1900, 1, 1) then
          Result := ''
        else
          Result := FormatDateTime('dd/mm/yyyy', VarToDateTime(info));
      end
      else
        raise EORMUtilsException.CreateFmt('FormataInfo: Cannot treat information [%s] as tiDate.', [VarToStr(info)]);
    tiDateTime:
      if VarIsType(info, [varDate, varDouble]) then
      begin
        if VarToDateTime(info) = 0 then
          Result := ''
        else
          Result := FormatDateTime('dd/mm/yyyy hh:mm:ss', VarToDateTime(info));
      end
      else
        raise EORMUtilsException.CreateFmt('FormataInfo: Cannot treat information [%s] as tiDateTime.', [VarToStr(info)]);
    tiBoolean:
      Result := IfThen(Boolean(info), 'S', 'N');
    tiByteArray:
      Result := ByteArrayToHex(info);
    tiNone:
      Result := '';
  else
    raise EORMUtilsException.CreateFmt('FormataInfo: Format type [%s] not recognized.', [GetEnumName(TypeInfo(TFormatType), Integer(tipo))]);
  end;
end;

function TQryRecInfo.GetFormattedText(const FieldName: string): string;
var
  Ctx: TRttiContext;
  RttiType: TRttiType;
  Prop: TRttiProperty;
  Field: TRttiField;
  Value: TValue;
  FmtType: TFormatType;
  UnderlyingType: TRttiType;
  HasValueField: TRttiField;
  ValueField: TRttiField;
  HasValue: Boolean;
  ActualValue: TValue;
  Attr: TCustomAttribute;
  FmtAttr: FmtPropAttribute;
begin
  Result := '';
  Ctx := TRttiContext.Create;
  try
    RttiType := Ctx.GetType(Self.ClassType);

    // Find the property by name
    Prop := RttiType.GetProperty(FieldName);
    if not Assigned(Prop) then
      raise EORMUtilsException.CreateFmt('Property "%s" not found.', [FieldName]);

    // Get the value of the property
    Value := Prop.GetValue(Self);
    FmtType := tiNone;

    // Check for FmtProp attribute on the property
    for Attr in Prop.GetAttributes do
    begin
      if Attr is FmtPropAttribute then
      begin
        FmtAttr := FmtPropAttribute(Attr);
        FmtType := FmtAttr.FormatType;
        Break;
      end;
    end;

    // If not found on property, check on the corresponding field
    if FmtType = tiNone then
    begin
      Field := RttiType.GetField('_' + FieldName);
      if Assigned(Field) then
      begin
        for Attr in Field.GetAttributes do
        begin
          if Attr is FmtPropAttribute then
          begin
            FmtAttr := FmtPropAttribute(Attr);
            FmtType := FmtAttr.FormatType;
            Break;
          end;
        end;
      end;
    end;

    // Determine default FmtType based on property type if not set by attribute
    if FmtType = tiNone then
    begin
      UnderlyingType := Prop.PropertyType;

      // Handle TNullable<T>
      if UnderlyingType.TypeKind = tkRecord then
      begin
        ValueField := UnderlyingType.GetField('Value');
        if Assigned(ValueField) then
          UnderlyingType := ValueField.FieldType;
      end;

      case UnderlyingType.TypeKind of
        tkInteger, tkInt64:
          FmtType := tiInteger;
        tkFloat:
          if UnderlyingType.Handle = TypeInfo(TDateTime) then
            FmtType := tiDateTime
          else
            FmtType := tiFloat;
        tkString, tkLString, tkWString, tkUString:
          FmtType := tiString;
        tkEnumeration:
          if UnderlyingType.Handle = TypeInfo(Boolean) then
            FmtType := tiBoolean
          else
            FmtType := tiString;
        tkVariant:
          FmtType := tiString;
      else
        FmtType := tiNone;
      end;
    end;

    // Extract actual value from TNullable<T> if necessary
    if Value.Kind = tkRecord then
    begin
      // Assuming TNullable<T> has 'HasValue' and 'Value' fields
      UnderlyingType := Prop.PropertyType;
      HasValueField := UnderlyingType.GetField('HasValue');
      ValueField := UnderlyingType.GetField('Value');

      if Assigned(HasValueField) and Assigned(ValueField) then
      begin
        HasValue := HasValueField.GetValue(Value.GetReferenceToRawData).AsBoolean;
        if HasValue then
        begin
          ActualValue := ValueField.GetValue(Value.GetReferenceToRawData);
          // Format the actual value
          Result := FormataInfo(ActualValue.AsVariant, FmtType);
        end
        else
          Result := '';
      end
      else
      begin
        // Not TNullable<T>, handle value directly
        if not Value.IsEmpty then
          Result := FormataInfo(Value.AsVariant, FmtType)
        else
          Result := '';
      end;
    end
    else
    begin
      // Not a record, handle value directly
      if not Value.IsEmpty then
        Result := FormataInfo(Value.AsVariant, FmtType)
      else
        Result := '';
    end;
  finally
    // No need to free TRttiContext as it's a record
  end;
end;

procedure TQryRecInfo.Fetch(DBConn: TUniConnection; const QryTxt: string);
var
  qr: TUniQuery;
  Ctx: TRttiContext;
  ObjType: TRttiType;
  Prop: TRttiProperty;
  Field: TField;
  ORMFieldName: string;
begin
  if not Assigned(DBConn) or Trim(QryTxt).IsEmpty then
    Exit;

  qr := TUniQuery.Create(nil);
  try
    qr.Connection := DBConn;
    qr.SpecificOptions.Values['FetchAll'] := 'True';
    qr.SpecificOptions.Values['CreateConnection'] := 'False';
    qr.SQL.Text := QryTxt;
    qr.Open;

    if qr.RecordCount > 0 then
    begin
      Ctx := TRttiContext.Create;
      try
        ObjType := Ctx.GetType(Self.ClassType);

        for Prop in ObjType.GetProperties do
        begin
          if not Prop.IsWritable then
            Continue;

          // Attempt to find the corresponding field in the query
          Field := qr.FindField(Prop.Name);
          if not Assigned(Field) then
            Continue; // Field not found, skip

          ORMFieldName := '_' + Prop.Name;
          // Handle TNullable<T> types
          if Prop.PropertyType.TypeKind = tkRecord then
          begin
            // Expecting TNullable<T>
            // Attempt to find the backing field for the nullable property
            if ObjType.GetField(ORMFieldName) = nil then
              Continue; // Backing field not found, skip

            if Prop.PropertyType.Name.StartsWith('TNullable<') then
            begin
              // Use RTTI to determine the generic type parameter
              if Field.IsNull then
                ObjType.GetField(ORMFieldName).SetValue(Self, TValue.From<TNullable<string>>(NullValue))
              else
              begin
                if Prop.PropertyType.Name = 'TNullable<System.string>' then
                  ObjType.GetField(ORMFieldName).SetValue(Self, TValue.From<TNullable<string>>(Field.AsString))
                else if Prop.PropertyType.Name = 'TNullable<System.Integer>' then
                  ObjType.GetField(ORMFieldName).SetValue(Self, TValue.From<TNullable<Integer>>(Field.AsInteger))
                else if Prop.PropertyType.Name = 'TNullable<System.Int64>' then
                  ObjType.GetField(ORMFieldName).SetValue(Self, TValue.From<TNullable<Int64>>(Field.AsLargeInt))
                else if Prop.PropertyType.Name = 'TNullable<System.Extended>' then
                  ObjType.GetField(ORMFieldName).SetValue(Self, TValue.From<TNullable<Extended>>(Field.AsFloat))
                else if Prop.PropertyType.Name = 'TNullable<System.Currency>' then
                  ObjType.GetField(ORMFieldName).SetValue(Self, TValue.From<TNullable<Currency>>(Field.AsCurrency))
                else if Prop.PropertyType.Name = 'TNullable<System.TDateTime>' then
                  ObjType.GetField(ORMFieldName).SetValue(Self, TValue.From<TNullable<TDateTime>>(Field.AsDateTime))
                else if Prop.PropertyType.Name = 'TNullable<System.Boolean>' then
                  ObjType.GetField(ORMFieldName).SetValue(Self, TValue.From<TNullable<Boolean>>(Field.AsBoolean))
                else if Prop.PropertyType.Name = 'TNullable<System.Variant>' then
                  ObjType.GetField(ORMFieldName).SetValue(Self, TValue.From<TNullable<Variant>>(Field.Value))
                else
                  raise EORMUtilsException.CreateFmt('Unsupported TNullable type: %s', [Prop.PropertyType.Name]);
              end;
            end
            else
              raise EORMUtilsException.CreateFmt('Unsupported record type: %s', [Prop.PropertyType.Name]);
          end
          else
          begin
            // Handle non-nullable types
            case Prop.PropertyType.TypeKind of
              tkString, tkLString, tkWString, tkUString:
                ObjType.GetField(ORMFieldName).SetValue(Self, Field.AsString);
              tkInteger:
                ObjType.GetField(ORMFieldName).SetValue(Self, Field.AsInteger);
              tkInt64:
                ObjType.GetField(ORMFieldName).SetValue(Self, Field.AsLargeInt);
              tkFloat:
                if Prop.PropertyType.Handle = TypeInfo(Currency) then
                  ObjType.GetField(ORMFieldName).SetValue(Self, Field.AsCurrency)
                else
                  ObjType.GetField(ORMFieldName).SetValue(Self, Field.AsFloat);
              tkVariant:
                ObjType.GetField(ORMFieldName).SetValue(Self, TValue.FromVariant(Field.AsVariant));
              tkEnumeration:
                if Prop.PropertyType.Handle = TypeInfo(Boolean) then
                  ObjType.GetField(ORMFieldName).SetValue(Self, Field.AsBoolean)
                else
                  raise EORMUtilsException.CreateFmt('Unsupported enumeration type: %s', [Prop.PropertyType.Name]);
            else
              // Add additional handling for other types if necessary
            end;
          end;
        end;
      finally
        // No need to free TRttiContext as it's a record
      end;
    end;
  finally
    qr.Free;
  end;
end;

procedure TQryRecInfo.SetInfo(Index: Integer; const Value: TValue);
var
  PropName: string;
  PropKind: TTypeKind;
  Ctx: TRttiContext;
  Prop: TRttiProperty;
  Field: TRttiField;
  OldValue: TValue;
begin
  // Retrieve property name and kind based on index
  // Assuming that the index corresponds to the property order
  Ctx := TRttiContext.Create;
  try
    Prop := Ctx.GetType(Self.ClassType).GetProperties()[Index];
    if not Assigned(Prop) then
      raise EORMUtilsException.CreateFmt('Error: The index "%d" does not correspond to any property in class "%s".', [Index, ClassName]);

    PropName := Prop.Name;
    PropKind := Prop.PropertyType.TypeKind;

    Field := Ctx.GetType(Self.ClassType).GetField('_' + PropName);
    if not Assigned(Field) then
      raise EORMUtilsException.CreateFmt('Error: Field "_%s" not found in class "%s".', [PropName, ClassName]);

    // If the field is not yet dirty, store the previous value
    if not FDirtyFields.ContainsKey(PropName) then
    begin
      OldValue := Field.GetValue(Self);
      FPreviousValues.AddOrSetValue(PropName, OldValue);
    end;

    // Set the new value
    Field.SetValue(Self, Value);

    // Mark the property as dirty
    FDirtyFields.AddOrSetValue(PropName, True);

    // Trigger the OnPropertyChanged event
    if Assigned(FOnPropertyChanged) then
      FOnPropertyChanged(Self, PropName);
  finally
    // No need to free TRttiContext as it's a record
  end;
end;

procedure TQryRecInfo.SetInfoCurrency(Index: Integer; const Value: Currency);
begin
  SetInfo(Index, TValue.From<Currency>(Value));
end;

procedure TQryRecInfo.SetInfoString(Index: Integer; const Value: string);
begin
  SetInfo(Index, TValue.From<string>(Value));
end;

procedure TQryRecInfo.SetInfoInteger(Index: Integer; const Value: Integer);
begin
  SetInfo(Index, TValue.From<Integer>(Value));
end;

procedure TQryRecInfo.SetInfoInt64(Index: Integer; const Value: Int64);
begin
  SetInfo(Index, TValue.From<Int64>(Value));
end;

procedure TQryRecInfo.SetInfoExtended(Index: Integer; const Value: Extended);
begin
  SetInfo(Index, TValue.From<Extended>(Value));
end;

procedure TQryRecInfo.SetInfoDateTime(Index: Integer; const Value: TDateTime);
begin
  SetInfo(Index, TValue.From<TDateTime>(Value));
end;

procedure TQryRecInfo.SetInfoBoolean(Index: Integer; const Value: Boolean);
begin
  SetInfo(Index, TValue.From<Boolean>(Value));
end;

procedure TQryRecInfo.SetInfoVariant(Index: Integer; const Value: Variant);
begin
  SetInfo(Index, TValue.From<Variant>(Value));
end;

procedure TQryRecInfo.SetInfoNullableBoolean(Index: Integer; const Value: TNullable<Boolean>);
begin
  SetInfo(Index, TValue.From<TNullable<Boolean>>(Value));
end;

procedure TQryRecInfo.SetInfoNullableCurrency(Index: Integer; const Value: TNullable<Currency>);
begin
  SetInfo(Index, TValue.From<TNullable<Currency>>(Value));
end;

procedure TQryRecInfo.SetInfoNullableDateTime(Index: Integer; const Value: TNullable<TDateTime>);
begin
  SetInfo(Index, TValue.From<TNullable<TDateTime>>(Value));
end;

procedure TQryRecInfo.SetInfoNullableExtended(Index: Integer; const Value: TNullable<Extended>);
begin
  SetInfo(Index, TValue.From<TNullable<Extended>>(Value));
end;

procedure TQryRecInfo.SetInfoNullableInteger(Index: Integer; const Value: TNullable<Integer>);
begin
  SetInfo(Index, TValue.From<TNullable<Integer>>(Value));
end;

procedure TQryRecInfo.SetInfoNullableInt64(Index: Integer; const Value: TNullable<Int64>);
begin
  SetInfo(Index, TValue.From<TNullable<Int64>>(Value));
end;

procedure TQryRecInfo.SetInfoNullableString(Index: Integer; const Value: TNullable<string>);
begin
  SetInfo(Index, TValue.From<TNullable<string>>(Value));
end;

procedure TQryRecInfo.SetInfoNullableVariant(Index: Integer; const Value: TNullable<Variant>);
begin
  SetInfo(Index, TValue.From<TNullable<Variant>>(Value));
end;

{ TQryRecArray<T> }

constructor TQryRecArray<T>.Create;
begin
  inherited Create;
  FQryRecInfoList := TObjectList<T>.Create(True); // Owns objects
end;

destructor TQryRecArray<T>.Destroy;
begin
  FQryRecInfoList.Free;
  inherited;
end;

procedure TQryRecArray<T>.ClearFilter;
begin
  FFilterSQL := '';
end;

function TQryRecArray<T>.GetCount: Integer;
begin
  Result := FQryRecInfoList.Count;
end;

function TQryRecArray<T>.Rec(Index: Integer): T;
begin
  if (Index < 0) or (Index >= FQryRecInfoList.Count) then
    raise EORMUtilsException.CreateFmt('Index %d out of bounds.', [Index]);

  Result := FQryRecInfoList[Index];
end;

procedure TQryRecArray<T>.FetchAllInstance(DBConn: TUniConnection; const QryTxt: string);
var
  qr: TUniQuery;
  RecInstance: T;
  Ctx: TRttiContext;
  ObjType: TRttiType;
  Prop: TRttiProperty;
  Field: TField;
  ORMFieldName: string;
begin
  if not Assigned(DBConn) then
    raise EORMUtilsException.Create('Database connection (DBConn) is not assigned.');

  if Trim(QryTxt).IsEmpty then
    raise EORMUtilsException.Create('Query text (QryTxt) cannot be empty.');

  qr := TUniQuery.Create(nil);
  try
    qr.Connection := DBConn;
    qr.SpecificOptions.Values['FetchAll'] := 'True';
    qr.SpecificOptions.Values['CreateConnection'] := 'False';
    qr.SQL.Text := QryTxt;
    qr.Open;

    if qr.IsEmpty then
      Exit; // No records to fetch

    Ctx := TRttiContext.Create;
    ObjType := Ctx.GetType(T);

    try
      while not qr.Eof do
      begin
        RecInstance := T.Create;
        try
          // Iterate over each property of T and assign values from the query
          for Prop in ObjType.GetProperties do
          begin
            if not Prop.IsWritable then
              Continue;

            // Attempt to find the corresponding field in the query
            Field := qr.FindField(Prop.Name);
            if not Assigned(Field) then
              Continue; // Field not found, skip

            ORMFieldName := '_' + Prop.Name;

            // Handle TNullable<T> types
            if Prop.PropertyType.TypeKind = tkRecord then
            begin
              // Expecting TNullable<T>
              // Attempt to find the backing field for the nullable property
              if ObjType.GetField(ORMFieldName) = nil then
                Continue; // Backing field not found, skip

              if Prop.PropertyType.Name.StartsWith('TNullable<') then
              begin
                // Use RTTI to determine the generic type parameter
                if Field.IsNull then
                  ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.From<TNullable<string>>(NullValue))
                else
                begin
                  // Determine the generic type and set the value accordingly

                  if Prop.PropertyType.Name = 'TNullable<System.string>' then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.From<TNullable<string>>(Field.AsString))
                  else if Prop.PropertyType.Name = 'TNullable<System.Integer>' then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.From<TNullable<Integer>>(Field.AsInteger))
                  else if Prop.PropertyType.Name = 'TNullable<System.Int64>' then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.From<TNullable<Int64>>(Field.AsLargeInt))
                  else if Prop.PropertyType.Name = 'TNullable<System.Extended>' then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.From<TNullable<Extended>>(Field.AsFloat))
                  else if Prop.PropertyType.Name = 'TNullable<System.Currency>' then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.From<TNullable<Currency>>(Field.AsCurrency))
                  else if Prop.PropertyType.Name = 'TNullable<System.TDateTime>' then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.From<TNullable<TDateTime>>(Field.AsDateTime))
                  else if Prop.PropertyType.Name = 'TNullable<System.Boolean>' then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.From<TNullable<Boolean>>(Field.AsBoolean))
                  else if Prop.PropertyType.Name = 'TNullable<System.Variant>' then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.From<TNullable<Variant>>(Field.Value))
                  else
                    raise EORMUtilsException.CreateFmt('Unsupported TNullable type: %s', [Prop.PropertyType.Name]);
                end;
              end
              else
                raise EORMUtilsException.CreateFmt('Unsupported record type: %s', [Prop.PropertyType.Name]);
            end
            else
            begin
              // Handle non-nullable types
              case Prop.PropertyType.TypeKind of
                tkString, tkLString, tkWString, tkUString:
                  ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), Field.AsString);
                tkInteger:
                  ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), Field.AsInteger);
                tkInt64:
                  ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), Field.AsLargeInt);
                tkFloat:
                  if Prop.PropertyType.Handle = TypeInfo(Currency) then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), Field.AsCurrency)
                  else if Prop.PropertyType.Handle = TypeInfo(TDateTime) then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), Field.AsDateTime)
                  else
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), Field.AsFloat);
                tkVariant:
                  ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), TValue.FromVariant(Field.AsVariant));
                tkEnumeration:
                  if Prop.PropertyType.Handle = TypeInfo(Boolean) then
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), Field.AsBoolean)
                  else
                    ObjType.GetField(ORMFieldName).SetValue(Pointer(RecInstance), Field.AsString);
              else
                // Unsupported type; you can extend this as needed
                Continue;
              end;
            end;
          end;

          // Add the populated record to the list
          FQryRecInfoList.Add(RecInstance);
        except
          RecInstance.Free;
          raise; // Re-raise the exception after cleanup
        end;
        qr.Next;
      end;
    finally
      // No need to free TRttiContext as it's a record
    end;
  finally
    qr.Free;
  end;
end;

function TQryRecArray<T>.GetEnumerator: TEnumerator<T>;
begin
  Result := FQryRecInfoList.GetEnumerator;
end;

class function TQryRecArray<T>.FetchAll(DBConn: TUniConnection; const QryTxt: string): TQryRecArray<T>;
var
  Instance: TQryRecArray<T>;
begin
  Instance := TQryRecArray<T>.Create;
  try
    Instance.FetchAllInstance(DBConn, QryTxt);
    Result := Instance;
  except
    Instance.Free;
    raise;
  end;
end;

end.

-------------------------

Carrefully review the Delphi code above, find out what is wrong with SetORMProperty that is giving an invalid typcast error.
As a reference, analyze how TQryRecInfo.Fetch handles SetValue for both TNullable and non-nullable