Store a choice procedure
Now let’s add a simple selection procedure to our UDR module. To do this, we will change the registration function firebird_udr_plugin.
function firebird_udr_plugin(AStatus: IStatus; AUnloadFlagLocal: BooleanPtr;
  AUdrPlugin: IUdrPlugin): BooleanPtr; cdecl;
begin
  // We register our functions
  AUdrPlugin.registerFunction(AStatus, 'sum_args',
    TSumArgsFunctionFactory.Create());
  // We register our procedures
  AUdrPlugin.registerProcedure(AStatus, 'sum_args_proc',
    TSumArgsProcedureFactory.Create());
  AUdrPlugin.registerProcedure(AStatus, 'gen_rows', TGenRowsFactory.Create());
  // We register our triggers
  //AUdrPlugin.registerTrigger(AStatus, 'test_trigger',
  //  TMyTriggerFactory.Create());
  theirUnloadFlag := AUnloadFlagLocal;
  Result := @myUnloadFlag;
end;| Note | Comment Don’t forget to add the  | 
The procedure factory is completely identical as for the case with an executable storedprocedure. The procedure instance methods are also identical, with the exception of theopen method, which we will analyze in a little more detail.
unit GenRowsProc;
{$IFDEF FPC}
{$MODE DELPHI}{$H+}
{$ENDIF}
interface
uses
  Firebird, SysUtils;
type
  { **********************************************************
    create procedure gen_rows (
      start  integer,
      finish integer
    ) returns (n integer)
    external name 'myudr!gen_rows'
    engine udr;
    ********************************************************* }
  TInput = record
    start: Integer;
    startNull: WordBool;
    finish: Integer;
    finishNull: WordBool;
  end;
  PInput = ^TInput;
  TOutput = record
    n: Integer;
    nNull: WordBool;
  end;
  POutput = ^TOutput;
  // Factory for creating an instance of the external procedure TGenRowsProcedure
   TGenRowsFactory = class(IUdrProcedureFactoryImpl)
     // Called when the factory is destroyed
     procedure dispose(); override;
     { Executed each time an external function is loaded into the metadata cache.
       Used to change the format of the input and output messages.
       @param(AStatus Status vector)
       @param(AContext External function execution context)
       @param(AMetadata External function metadata)
       @param(AInBuilder Message builder for input metadata)
       @param(AOutBuilder Message builder for output metadata)
     }
     procedure setup(AStatus: IStatus; AContext: IExternalContext;
       AMetadata: IRoutineMetadata; AInBuilder: IMetadataBuilder;
       AOutBuilder: IMetadataBuilder); override;
     { Create a new instance of the external procedure TGenRowsProcedure
       @param(AStatus Status vector)
       @param(AContext External function execution context)
       @param(AMetadata External function metadata)
       @returns(External function instance)
     }
     function newItem(AStatus: IStatus; AContext: IExternalContext;
       AMetadata: IRoutineMetadata): IExternalProcedure; override;
   end;
   // External procedure TGenRowsProcedure.
   TGenRowsProcedure = class(IExternalProcedureImpl)
   public
     // Called when the procedure instance is destroyed
     procedure dispose(); override;
     { This method is called just before open and tells
       to the kernel our requested set of characters to exchange data within this
       method. During this call, the context uses the character set obtained from
       ExternalEngine::getCharSet.
       @param(AStatus Status vector)
       @param(AContext External function execution context)
       @param(AName Character set name)
       @param(AName Character set name length)
     }
     procedure getCharSet(AStatus: IStatus; AContext: IExternalContext;
       AName: PAnsiChar; ANameSize: cardinal); override;
     { Execution of external procedure
       @param(AStatus Status vector)
       @param(AContext External function execution context)
       @param(AInMsg Pointer to input message)
       @param(AOutMsg Pointer to output message)
       @returns(Data set for selective procedure or
                nil for run procedures)
     }
     function open(AStatus: IStatus; AContext: IExternalContext; AInMsg: Pointer;
       AOutMsg: Pointer): IExternalResultSet; override;
   end;
   // Output data set for the TGenRowsProcedure procedure
   TGenRowsResultSet = class(IExternalResultSetImpl)
     Input: PInput;
     Output: POutput;
     // Called when the dataset instance is destroyed
     procedure dispose(); override;
     { Retrieve the next record from the dataset. Somewhat analogous to
       SUSPEND. In this method, the next record from the data set should
       be prepared.
       @param(AStatus Status vector)
       @returns(True if the dataset has an entry to retrieve,
                False if there are no more entries)
     }
     function fetch(AStatus: IStatus): Boolean; override;
   end;
implementation
{ TGenRowsFactory }
procedure TGenRowsFactory.dispose;
begin
   Destroy;
end;
function TGenRowsFactory.newItem(AStatus: IStatus; AContext: IExternalContext;
   AMetadata: IRoutineMetadata): IExternalProcedure;
begin
   Result := TGenRowsProcedure.create;
end;
procedure TGenRowsFactory.setup(AStatus: IStatus; AContext: IExternalContext;
   AMetadata: IRoutineMetadata; AInBuilder, AOutBuilder: IMetadataBuilder);
begin
end;
{ TGenRowsProcedure }
procedure TGenRowsProcedure.dispose;
begin
   Destroy;
end;
procedure TGenRowsProcedure.getCharSet(AStatus: IStatus;
   AContext: IExternalContext; AName: PAnsiChar; ANameSize: cardinal);
begin
end;
function TGenRowsProcedure.open(AStatus: IStatus; AContext: IExternalContext;
   AInMsg, AOutMsg: Pointer): IExternalResultSet;
begin
   Result := TGenRowsResultSet.create;
   with TGenRowsResultSet(Result) do
   begin
     Input := AInMsg;
     Output := AOutMsg;
   end;
   // if one of the input arguments is NULL, return nothing
   if PInput(AInMsg).startNull or PInput(AInMsg).finishNull then
   begin
     POutput(AOutMsg).nNull := True;
// intentionally set the output so that
// TGenRowsResultSet.fetch method returned false
     Output.n := Input.finish;
     exit;
   end;
   // checks
   if PInput(AInMsg).start > PInput(AInMsg).finish then
     raise Exception.Create('First parameter greater then second parameter.');
   with TGenRowsResultSet(Result) do
   begin
     // initial value
     Output.nNull := False;
     Output.n := Input.start - 1;
   end;
end;
{ TGenRowsResultSet }
procedure TGenRowsResultSet.dispose;
begin
   Destroy;
end;
// If it returns True, then the next record from the data set is retrieved.
// If it returns False, then the records in the data set are over
// new values in the output vector are calculated each time
// when calling this method
function TGenRowsResultSet.fetch(AStatus: IStatus): Boolean;
begin
  Inc(Output.n);
  Result := (Output.n <= Input.finish);
end;
end.In the open method of the TGenRowsProcedure procedure instance, we check the firstand second input arguments for the value NULL, if one of the arguments is NULL,then the output argument is NULL, in addition, the procedure should not return anyrow when fetching via the SELECT statement, so we assign Output.n such a value thatthe TGenRowsResultSet.fetch` method returns False.
In addition, we check that the first argument does not exceed the value of the second,otherwise we throw an exception. Don’t worry, this exception will be caught in the UDRsubsystem and converted to a Firebird exception. This is one of the advantages of thenew UDRs over Legacy UDFs.
Since we are creating a selection procedure, the open method must return a datasetinstance that implements the IExternalResultSet interface. To simplify, let’s inheritour data set from the IExternalResultSetImpl class.
The dispose method is designed to release allocated resources. In it, we simply callthe destructor.
The fetch method is called when the next record is retrieved by the SELECTstatement. This method is essentially analogous to the SUSPEND statement used inregular PSQL stored procedures. Each time it is called, it prepares new values for theoutput message. The method returns true if the record should be returned to thecaller, and false if there is no more data to retrieve. In our case, we simplyincrement the current value of the output variable until it is greater than the maximumlimit.
| Note | Comment Delphi does not support the  You can use any collection class, populate it in the  |