check why this is not working: If the field is not yet dirty, store the previous value

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

///

/// Represents the key used in the component-ORM mapping dictionary.
/// Combines a reference to an ORM record with a specific field name.
///

TORMKey = record
ORMRec: TQryRecInfo;
FieldName: string;
end;

///

/// Custom comparer for TORMKey to handle equality checks and hashing.
///

TORMKeyComparer = class(TEqualityComparer)
function Equals(const Left, Right: TORMKey): Boolean; override;
function GetHashCode(const Key: TORMKey): Integer; override;
end;

///

/// Manages the binding between UI components and ORM records.
///

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;

///

/// Helper methods extending TComponent to interface with ORMManager.
///

TComponentHelper = class helper for TComponent
public
///

/// Retrieves the ORM record associated with the component.
///

function GetORMRec: TQryRecInfo;

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

end;

///

/// Base class for query record information
/// Use descendants of this class to represent specific database records.
///

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;

///

/// Generic class to handle arrays of TQryRecInfo descendants.
///

TQryRecArray<T: class, constructor> = class
private
FQryRecInfoList: TObjectList;
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;
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;
var
Comp: TComponent;
CompList: TList;
begin
CompList := TList.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);
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(Value));
end;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// =============================
// TQryRecArray Implementation
// =============================

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

destructor TQryRecArray.Destroy;
begin
FQryRecInfoList.Free;
inherited;
end;

procedure TQryRecArray.ClearFilter;
begin
FFilterSQL := '';
end;

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

function TQryRecArray.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.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.GetEnumerator: TEnumerator;
begin
Result := FQryRecInfoList.GetEnumerator;
end;

class function TQryRecArray.FetchAll(DBConn: TUniConnection; const QryTxt: string): TQryRecArray;
var
Instance: TQryRecArray;
begin
Instance := TQryRecArray.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.