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.

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

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

interface

uses
  Vcl.Forms, System.Classes, Vcl.Controls, Vcl.StdCtrls, Vcl.ComCtrls, pixel_classes,
  ORM_utils, interposer_class, SysUtils, Dialogs, formgerenc_Classes, DM1, Variants, StrUtils,
  Vcl.WinXPickers, System.Rtti, System.TypInfo, Vcl.ExtCtrls, System.Generics.Collections, ORM_CustomAttributes;

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);
  ORMManager.UnregisterComponent(EditUserName);

  ORMManager.RegisterComponentsByHint(Self, FQueryRec2);

  Exit;

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

function GenerateUpdateStatements(Instance: TQryRecInfo): 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;
  propName: string; // Variable to store the property name

  // 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;
      try
        RttiType := Context.GetType(Instance.ClassType);
        for Field in RttiType.GetFields do
        begin
          TableName := '';
          RefColumn := '';
          IsPrimaryKey := False;

          // Retrieve attributes for each field
          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;

          // Only process fields that have a TableAttribute
          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 each table
        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;

              // Retrieve attributes for each field within the table
              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 without the leading '_' if RefColumn is not specified
              if RefColumn = '' then
                RefColumn := Copy(Field.Name, 2, MaxInt); // Remove leading '_'

              // Derive the property name by removing the leading '_' from the field name
              if Field.Name.StartsWith('_') then
                propName := Copy(Field.Name, 2, MaxInt)
              else
                propName := Field.Name;

              // Retrieve the value of the field
              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
              begin
                // Always include primary key columns in the WHERE clause
                PKColumns.Add(Format('%s = %s', [RefColumn, ValueStr]));
              end
              else
              begin
                // Only include non-primary key columns that are marked as dirty
                if Instance.IsFieldDirty(propName) then
                  NonPKColumns.Add(Format('%s = %s', [RefColumn, ValueStr]));
              end;
            end;

            // Skip generating UPDATE statements if there are no dirty non-PK columns
            if NonPKColumns.Count = 0 then
            begin
              ResultBuilder.AppendFormat('No non-primary key columns to update for table %s.', [Key]).AppendLine;
              Continue;
            end;

            // Ensure that primary key columns are present to form the WHERE clause
            if PKColumns.Count = 0 then
            begin
              ResultBuilder.AppendFormat('No primary key columns found for table %s. Cannot generate UPDATE statement.', [Key]).AppendLine;
              Continue;
            end;

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

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

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

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

  var UpdatedComp:= ORMManager.FindComponent(FQueryRec2, 'EmployeeName');

  ORMManager.UpdateComponentFromORM(UpdatedComp, UpdatedComp.GetORMRec, UpdatedComp.GetORMRecField);
  }

//    FQueryRec2.ClearDirtyFlags;

   // Check if the property is dirty
    if FQueryRec2.IsFieldDirty('EmployeeName') then
    begin
      // Get the previous value
      var PrevValue := FQueryRec2.GetPreviousValue('EmployeeName');
      // Convert TValue to TNullable<string>
      var PrevEmployeeName := PrevValue.AsType<TNullable<string>>();
      if PrevEmployeeName.HasValue then
        showmessage('Previous EmployeeName: ' + PrevEmployeeName.Value)
      else
        showmessage('Previous EmployeeName is null.');
    end;

  showmessage(GenerateUpdateStatements(FQueryRec2));

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

    ORMManager.UpdateComponentFromORM(Edit, Edit.GetORMRec, Edit.GetORMRecField);

    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, FQueryRec2, '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, FQueryRec2, '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
  Ctx: TRttiContext;
  Prop: TRttiProperty;
  ORMFieldName: string;
  ObjType: TRttiType;
begin
  Ctx := TRttiContext.Create;
  try
    ObjType := Ctx.GetType(ORMRec.ClassType);
    Prop := ObjType.GetProperty(FieldName);

    if Assigned(Prop) and Prop.IsWritable then
    begin
      ORMFieldName := '_' + FieldName;

      if ObjType.GetField(ORMFieldName) = nil then
        Exit; // Backing field not found, skip

      if Prop.PropertyType.TypeKind = tkRecord then
      begin
        // Check if the property is a TNullable<T>
        if Prop.PropertyType.Name.StartsWith('TNullable<') then
        begin
          if Value.IsEmpty or VarIsNull(Value.AsVariant) then
          begin
            // Set the property to null
            if Prop.PropertyType.Name = 'TNullable<System.string>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<string>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Integer>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Integer>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Int64>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Int64>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Extended>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Extended>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Currency>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Currency>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.TDateTime>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<TDateTime>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Boolean>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Boolean>>(NullValue))
            else if Prop.PropertyType.Name = 'TNullable<System.Variant>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Variant>>(NullValue));
          end
          else
          begin
            // Set the property with the actual value, adjust types
            if Prop.PropertyType.Name = 'TNullable<System.string>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<string>>(Value.AsString))
            else if Prop.PropertyType.Name = 'TNullable<System.Integer>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Integer>>(Value.AsInteger))
            else if Prop.PropertyType.Name = 'TNullable<System.Int64>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Int64>>(Value.AsInt64))
            else if Prop.PropertyType.Name = 'TNullable<System.Extended>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Extended>>(Value.AsExtended))
            else if Prop.PropertyType.Name = 'TNullable<System.Currency>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Currency>>(Value.AsCurrency))
            else if Prop.PropertyType.Name = 'TNullable<System.TDateTime>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<TDateTime>>(Value.AsType<TDateTime>))
            else if Prop.PropertyType.Name = 'TNullable<System.Boolean>' then
              Prop.SetValue(Pointer(ORMRec), TValue.From<TNullable<Boolean>>(Value.AsBoolean))
            // No need to handle TNullable<Variant> since no conversion required except as fallback
            else
              raise EORMUtilsException.CreateFmt('Unsupported nullable 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(ORMRec), Value.AsString);
          tkInteger:
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsInteger);
          tkInt64:
            ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsInt64);
          tkFloat:
            if Prop.PropertyType.Handle = TypeInfo(Currency) then
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsCurrency)
            else if Prop.PropertyType.Handle = TypeInfo(TDateTime) then
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), TValue.From<TDateTime>(Value.AsType<TDateTime>))
            else
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsExtended);
          tkEnumeration:
            if Prop.PropertyType.Handle = TypeInfo(Boolean) then
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsBoolean)
            else
              ObjType.GetField(ORMFieldName).SetValue(Pointer(ORMRec), Value.AsString);
        else
          raise EORMUtilsException.CreateFmt('Unsupported type for field "%s" in record "%s".', [FieldName, ORMRec.ClassName]);
        end;
      end;

      // After setting the ORM property, find the associated component and update it
      var Component := ORMManager.FindComponent(ORMRec, FieldName);
      if Assigned(Component) then
      begin
        ORMManager.UpdateComponentFromORM(Component, ORMRec, FieldName);
      end;

    end
    else
      raise EORMUtilsException.CreateFmt('Property "%s" not found or not writable in record "%s".', [FieldName, ORMRec.ClassName]);
  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.

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

enhance TQryRecInfo so it has a isNewRec boolean that says if the instance has any of its properties changed afetr Fetch or FetchAllInstance.
Make necessary adjustements on orm_utils unit