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 ORM_CustomAttributes;

{$TYPEINFO ON}

interface

uses
  System.SysUtils, System.Classes, System.Rtti;

type
  TFormatType = (tiString, tiInteger, tiDate, tiDateTime, tiMoney, tiFloat, tiBoolean, tiByteArray, tiNone);

  /// <summary>
  /// Attribute to specify the database table associated with a class or field.
  /// </summary>
  TableAttribute = class(TCustomAttribute)
  private
    FTableName: string;
  public
    constructor Create(const ATableName: string);
    property TableName: string read FTableName;
  end;

  /// <summary>
  /// Attribute to mark a field as a primary key.
  /// </summary>
  PrimaryKeyAttribute = class(TCustomAttribute)
  public
    constructor Create;
  end;

  /// <summary>
  /// Attribute to specify the database column associated with a field.
  /// </summary>
  RefColumnAttribute = class(TCustomAttribute)
  private
    FColumnName: string;
  public
    constructor Create(const AColumnName: string);
    property ColumnName: string read FColumnName;
  end;

  /// <summary>
  /// Attribute to specify the formatting of a field.
  /// </summary>
  FmtPropAttribute = class(TCustomAttribute)
  private
    FFormatType: TFormatType;
  public
    constructor Create(AFormatType: TFormatType);
    property FormatType: TFormatType read FFormatType;
  end;

  /// <summary>
  /// Attribute to specify the column index of a property in a TStringGrid.
  /// </summary>
  GridColumnAttribute = class(TCustomAttribute)
  private
    FColumnIndex: Integer;
  public
    constructor Create(AColumnIndex: Integer);
    property ColumnIndex: Integer read FColumnIndex;
  end;

  /// <summary>
  /// Attribute to specify the TComponent a property is mapped to.
  /// </summary>
  ComponentMappingAttribute = class(TCustomAttribute)
  private
    FComponentName: string;
  public
    constructor Create(const AComponentName: string);
    property ComponentName: string read FComponentName;
  end;

implementation

{ TableAttribute }

constructor TableAttribute.Create(const ATableName: string);
begin
  inherited Create;
  FTableName := ATableName;
end;

{ RefColumnAttribute }

constructor RefColumnAttribute.Create(const AColumnName: string);
begin
  inherited Create;
  FColumnName := AColumnName;
end;

{ PrimaryKeyAttribute }

constructor PrimaryKeyAttribute.Create;
begin
  inherited Create;
end;

{ FmtPropAttribute }

constructor FmtPropAttribute.Create(AFormatType: TFormatType);
begin
  inherited Create;
  FFormatType := AFormatType;
end;

{ GridColumnAttribute }

constructor GridColumnAttribute.Create(AColumnIndex: Integer);
begin
  inherited Create;
  FColumnIndex := AColumnIndex;
end;

{ ComponentMappingAttribute }

constructor ComponentMappingAttribute.Create(const AComponentName: string);
begin
  inherited Create;
  FComponentName := AComponentName;
end;

end.

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


unit orm_utils;

interface

uses
  System.SysUtils,
  System.Classes,
  System.Generics.Collections,
  System.Generics.Defaults,
  System.Rtti,
  System.TypInfo,
  Data.DB,
  Uni,
  pixel_classes,
  orm_customattributes,
  Vcl.Controls,
  Vcl.Forms,
  Dialogs,
  Variants;

type
  TQryRecInfo = class;

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

  EORMUtilsException = class(Exception);

  /// <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;

    /// <summary>
    /// Registers components based on their Hint property.
    /// </summary>
    procedure RegisterComponentsByHint(MyForm: TForm; ORMRec: TQryRecInfo);
  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;

  /// <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;

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

implementation

uses
  pixel_utils,
  System.Math,
  System.Types,
  Windows,
  Vcl.StdCtrls,
  Vcl.WinXCtrls,
  Vcl.WinXPickers,
  StrUtils,
  System.Hash;

// =============================
// 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;

procedure TORMManager.RegisterComponentsByHint(MyForm: TForm; ORMRec: TQryRecInfo);
var
  Context: TRttiContext;
  RttiType: TRttiType;
  RttiProperty: TRttiProperty;
  Component: TComponent;
  HintValue: TValue;
begin
  Context := TRttiContext.Create;
  try
    for Component in MyForm do
    begin
      RttiType := Context.GetType(Component.ClassType);
      RttiProperty := RttiType.GetProperty('Hint');

      if Assigned(RttiProperty) then
      begin
        HintValue := RttiProperty.GetValue(Component);
        if HintValue.IsEmpty then
          Continue;

        RttiType := Context.GetType(ORMRec.ClassType);
        for RttiProperty in RttiType.GetProperties do
        begin
          if (RttiProperty.Visibility = mvPublished) then
          begin
            if SameText(HintValue.AsString, RttiProperty.Name) then
            begin
              RegisterComponent(Component, ORMRec, RttiProperty.Name);
              Break;
            end;
          end;
        end;
      end;
    end;
  finally
    Context.Free;
  end;
end;

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

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

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

// =============================
// TQryRecInfo Implementation
// =============================

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;

// Helper function to format information based on type
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);
    }

    var UpdatedComp:= ORMManager.FindComponent(Self, PropName);
    if Assigned(UpdatedComp) then ORMManager.UpdateComponentFromORM(UpdatedComp, UpdatedComp.GetORMRec, UpdatedComp.GetORMRecField);

  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> Implementation
// =============================

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;

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

initialization
  ORMManager := TORMManager.Create;

finalization
  ORMManager.Free;

end.

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


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

fix the function GenerateUpdateStatements below so it just updates the fields what are marked as dirty:

function GenerateUpdateStatements(Instance: TObject): string;
var
  Context: TRttiContext;
  RttiType: TRttiType;
  Field: TRttiField;
  Attr: TCustomAttribute;
  TableName: string;
  RefColumn: string;
  IsPrimaryKey: Boolean;
  TableFields: TObjectDictionary<string, TList<TRttiField>>;
  FieldsList: TList<TRttiField>;
  NonPKColumns: TList<string>;
  PKColumns: TList<string>;
  UpdateSQL: string;
  Key: string;
  ResultBuilder: TStringBuilder;
  FieldValue: TValue;
  ValueStr: string;

  // Nullable variables for different types
  NullableString: TNullable<string>;
  NullableInteger: TNullable<Integer>;
  NullableInt64: TNullable<Int64>;
  NullableExtended: TNullable<Extended>;
  NullableCurrency: TNullable<Currency>;
  NullableDateTime: TNullable<TDateTime>;
  NullableBoolean: TNullable<Boolean>;
  NullableVariant: TNullable<Variant>;
begin
  ResultBuilder := TStringBuilder.Create;
  try
    TableFields := TObjectDictionary<string, TList<TRttiField>>.Create([doOwnsValues]);
    try
      Context := TRttiContext.Create;
      RttiType := Context.GetType(Instance.ClassType);
      for Field in RttiType.GetFields do
      begin
        TableName := '';
        RefColumn := '';
        IsPrimaryKey := False;

        for Attr in Field.GetAttributes do
        begin
          if Attr is TableAttribute then
            TableName := TableAttribute(Attr).TableName
          else if Attr is RefColumnAttribute then
            RefColumn := RefColumnAttribute(Attr).ColumnName
          else if Attr is PrimaryKeyAttribute then
            IsPrimaryKey := True;
        end;

        if (TableName <> '') then
        begin
          if not TableFields.TryGetValue(TableName, FieldsList) then
          begin
            FieldsList := TList<TRttiField>.Create;
            TableFields.Add(TableName, FieldsList);
          end;
          FieldsList.Add(Field);
        end;
      end;

      // Generate SQL Update statements
      for Key in TableFields.Keys do
      begin
        FieldsList := TableFields[Key];
        NonPKColumns := TList<string>.Create;
        PKColumns := TList<string>.Create;
        try
          for Field in FieldsList do
          begin
            RefColumn := '';
            IsPrimaryKey := False;

            for Attr in Field.GetAttributes do
            begin
              if Attr is RefColumnAttribute then
                RefColumn := RefColumnAttribute(Attr).ColumnName
              else if Attr is PrimaryKeyAttribute then
                IsPrimaryKey := True;
            end;

            // Use the field name if RefColumn is not specified
            if RefColumn = '' then
              RefColumn := Copy(Field.Name, 2, MaxInt); // Remove leading 'F'

            FieldValue := Field.GetValue(Instance);

            // Handle various TNullable types
            if FieldValue.TryAsType<TNullable<string>>(NullableString) then
            begin
              if NullableString.HasValue then
                ValueStr := QuotedStr(NullableString.Value)
              else
                ValueStr := 'NULL';
            end
            else if FieldValue.TryAsType<TNullable<Integer>>(NullableInteger) then
            begin
              if NullableInteger.HasValue then
                ValueStr := NullableInteger.Value.ToString
              else
                ValueStr := 'NULL';
            end
            else if FieldValue.TryAsType<TNullable<Int64>>(NullableInt64) then
            begin
              if NullableInt64.HasValue then
                ValueStr := NullableInt64.Value.ToString
              else
                ValueStr := 'NULL';
            end
            else if FieldValue.TryAsType<TNullable<Extended>>(NullableExtended) then
            begin
              if NullableExtended.HasValue then
                ValueStr := NullableExtended.Value.ToString
              else
                ValueStr := 'NULL';
            end
            else if FieldValue.TryAsType<TNullable<Currency>>(NullableCurrency) then
            begin
              if NullableCurrency.HasValue then
                ValueStr := NullableCurrency.Value.ToString
              else
                ValueStr := 'NULL';
            end
            else if FieldValue.TryAsType<TNullable<TDateTime>>(NullableDateTime) then
            begin
              if NullableDateTime.HasValue then
                ValueStr := QuotedStr(DateTimeToStr(NullableDateTime.Value))
              else
                ValueStr := 'NULL';
            end
            else if FieldValue.TryAsType<TNullable<Boolean>>(NullableBoolean) then
            begin
              if NullableBoolean.HasValue then
                ValueStr := IfThen(NullableBoolean.Value, '1', '0')
              else
                ValueStr := 'NULL';
            end
            else if FieldValue.TryAsType<TNullable<Variant>>(NullableVariant) then
            begin
              if NullableVariant.HasValue then
                ValueStr := QuotedStr(NullableVariant.Value)
              else
                ValueStr := 'NULL';
            end
            else
            begin
              // Default case if none of the above nullable types match
              ValueStr := FieldValue.ToString;
            end;

            if IsPrimaryKey then
              PKColumns.Add(Format('%s = %s', [RefColumn, ValueStr]))
            else
              NonPKColumns.Add(Format('%s = %s', [RefColumn, ValueStr]));
          end;

          // Build SET and WHERE clauses
          if NonPKColumns.Count = 0 then
          begin
            ResultBuilder.AppendFormat('No non-primary key columns to update for table %s', [Key]).AppendLine;
            Continue;
          end;

          if PKColumns.Count = 0 then
          begin
            ResultBuilder.AppendFormat('No primary key columns found for table %s. Cannot generate UPDATE statement.', [Key]).AppendLine;
            Continue;
          end;

          UpdateSQL := Format('UPDATE %s SET %s WHERE %s;',
            [Key,
             String.Join(', ', NonPKColumns.ToArray),
             String.Join(' AND ', PKColumns.ToArray)]);

          // Output the generated SQL
          ResultBuilder.AppendFormat('Generated SQL for table %s:', [Key]).AppendLine;
          ResultBuilder.AppendLine(UpdateSQL);
          ResultBuilder.AppendLine;
        finally
          NonPKColumns.Free;
          PKColumns.Free;
        end;
      end;
    finally
      TableFields.Free;
      Context.Free;
    end;

    Result := ResultBuilder.ToString;
  finally
    ResultBuilder.Free;
  end;
end;