FirebirdSQL logo

Структура UDR

Мы будем описывать структуру UDR на языке Pascal. Для объясненияминимальной структуры для построения UDR будем использовать стандартныепримеры из examples/udr/ переведённых на Pascal.

Создайте новый проект динамической библиотеки, который назовёмMyUdr. В результате у вас должен получиться файл MyUdr.dpr (если высоздавали проект в Delphi) или файл MyUdr.lpr (если вы создали проектв Lazarus). Теперь изменим главный файл проекта так, чтобы он выгляделследующим образом:

library MyUdr;

{$IFDEF FPC}
  {$MODE DELPHI}{$H+}
{$ENDIF}

uses
{$IFDEF unix}
    cthreads,
    // the c memory manager is on some systems much faster for multi-threading
    cmem,
{$ENDIF}
  UdrInit in 'UdrInit.pas',
  SumArgsFunc in 'SumArgsFunc.pas';

exports firebird_udr_plugin;

end.

В данном случае необходимо экспортировать всего одну функциюfirebird_udr_plugin, которая является точкой входа для плагина внешнихмодулей UDR. Реализация этой функции будет находиться в модуле UdrInit.

Note
Замечание

Если вы разрабатываете вашу UDR в Free Pascal, то вам потребуютсядополнительные директивы. Директива {$mode objfpc} требуется длявключения режима Object Pascal. Вместо неё вы можете использоватьдирективу {$mode delphi} для обеспечения совместимости с Delphi.Поскольку мои примеры должны успешно компилироваться как в FPC, так и вDelphi я выбираю режим {$mode delphi}.

Директива {$H+} включает поддержку длинных строк. Это необходимо есливы будете пользоваться типы string, ansistring, а не тольконуль-терминированные строки PChar, PAnsiChar, PWideChar.

Кроме того, нам потребуется подключить отдельные модули для поддержкимногопоточности в Linux и других Unix-подобных операционных системах.

Регистрация функций

Теперь добавим модуль UdrInit, он должен выглядеть следующим образом:

unit UdrInit;

{$IFDEF FPC}
  {$MODE DELPHI}{$H+}
{$ENDIF}

interface

uses
  Firebird;

// точка входа для External Engine модуля UDR
function firebird_udr_plugin(AStatus: IStatus; AUnloadFlagLocal: BooleanPtr;
  AUdrPlugin: IUdrPlugin): BooleanPtr; cdecl;

implementation

uses
  SumArgsFunc;

var
  myUnloadFlag: Boolean;
  theirUnloadFlag: BooleanPtr;

function firebird_udr_plugin(AStatus: IStatus; AUnloadFlagLocal: BooleanPtr;
  AUdrPlugin: IUdrPlugin): BooleanPtr; cdecl;
begin
  // регистрируем наши функции
  AUdrPlugin.registerFunction(AStatus, 'sum_args',
    TSumArgsFunctionFactory.Create());
  // регистрируем наши процедуры
  //AUdrPlugin.registerProcedure(AStatus, 'sum_args_proc',
  //  TSumArgsProcedureFactory.Create());
  //AUdrPlugin.registerProcedure(AStatus, 'gen_rows', TGenRowsFactory.Create());
  // регистрируем наши триггеры
  //AUdrPlugin.registerTrigger(AStatus, 'test_trigger',
  //  TMyTriggerFactory.Create());

  theirUnloadFlag := AUnloadFlagLocal;
  Result := @myUnloadFlag;
end;

initialization

myUnloadFlag := false;

finalization

if ((theirUnloadFlag <> nil) and not myUnloadFlag) then
  theirUnloadFlag^ := true;

end.

В функции firebird_udr_plugin необходимо зарегистрировать фабрикинаших внешних процедур, функций и триггеров. Для каждой функции,процедуры или триггера необходимо написать свою фабрику. Это делается спомощью методов интерфейса IUdrPlugin:

  • registerFunction - регистрирует внешнюю функцию;

  • registerProcedure - регистрирует внешнюю процедуру;

  • registerTrigger - регистрирует внешний триггер.

Первым аргументом этих функций является указатель на статус вектор,далее следует внутреннее имя функции (процедуры или триггера).Внутреннее имя будет использоваться при созданиипроцедуры/функции/триггера на SQL. Третьим аргументом передаётсяэкземпляр фабрики для создания функции (процедуры или триггера).

Экземпляр триггера

Внешний триггер должен реализовать интерфейс IExternalTrigger. Дляупрощения просто наследуем класс IExternalTriggerImpl.

Метод dispose вызывается при уничтожении экземпляра триггера, в нём мыдолжны освободить ранее выделенные ресурсы. В данном случае простовызываем деструктор.

Метод getCharSet используется для того, чтобы сообщить контексту внешнего триггерунабор символов, который мы хотим использовать при работе с соединением из текущего контекста.По умолчанию соединение из текущего контекста работает в кодировке текущего подключения, что не всегда удобно.

Метод execute вызывается при выполнении триггера на одно из событий длякоторого создан триггер. В этот метод передаётся указатель на статусвектор, указатель на контекст внешнего триггера, действие (событие)которое вызвало срабатывание триггера и указатели на сообщения длястарых и новых значений полей. Возможные действия (события) триггераперечислены константами в интерфейсе IExternalTrigger. Такие константыначинаются с префикса ACTION_. Знания о текущем действие необходимо,поскольку в Firebird существуют триггеры созданные для несколькихсобытий сразу. Сообщения необходимы только для триггеров на действиятаблицы, для DDL триггеров, а также для триггеров на события подключения иотключения от базы данных и триггеров на события старта, завершения иотката транзакции указатели на сообщения будут инициализированызначением nil. В отличие от процедур и функций сообщения триггеровстроятся для полей таблицы на события которой создан триггер.Статические структуры для таких сообщений строятся по тем же принципам,что и структуры сообщений для входных и выходных параметров процедуры,только вместо переменных берутся поля таблицы.

Note
Замечание

Обратите внимание, что если вы используете отображение сообщений наструктуры, то ваши триггеры могут сломаться после изменения составаполей таблицы и их типов. Чтобы этого не произошло, используйте работу ссообщением через смещения получаемые из IMessageMetadata. Это не такактуально для процедур и функций, поскольку входные и выходные параметрыменяются не так уж часто. Или хотя бы вы делаете это явно, что можетнатолкнуть вас на мысль, что необходимо переделать и внешнююпроцедуру/функцию.

В нашем простейшем триггере мы определяем тип события, и в теле триггеравыполняем следующий PSQL аналог

...
  if (:new.B IS NULL) THEN
    :new.B = :new.A + 1;
...

Фабрика функций

Теперь необходимо написать фабрику и саму функцию. Они будут расположеныв модуле SumArgsFunc. Примеры для написания процедур и триггеров будутпредставлены позже.

unit SumArgsFunc;

{$IFDEF FPC}
{$MODE DELPHI}{$H+}
{$ENDIF}

interface

uses
  Firebird;

{ *********************************************************
    create function sum_args (
      n1 integer,
      n2 integer,
      n3 integer
    ) returns integer
    external name 'myudr!sum_args'
    engine udr;
 ********************************************************* }

type
  // структура на которое будет отображено входное сообщение
  TSumArgsInMsg = record
    n1: Integer;
    n1Null: WordBool;
    n2: Integer;
    n2Null: WordBool;
    n3: Integer;
    n3Null: WordBool;
  end;
  PSumArgsInMsg = ^TSumArgsInMsg;

  // структура на которое будет отображено выходное сообщение
  TSumArgsOutMsg = record
    result: Integer;
    resultNull: WordBool;
  end;
  PSumArgsOutMsg = ^TSumArgsOutMsg;

  // Фабрика для создания экземпляра внешней функции TSumArgsFunction
  TSumArgsFunctionFactory = class(IUdrFunctionFactoryImpl)
    // Вызывается при уничтожении фабрики
    procedure dispose(); override;

    { Выполняется каждый раз при загрузке внешней функции в кеш метаданных.
      Используется для изменения формата входного и выходного сообщения.

      @param(AStatus Статус вектор)
      @param(AContext Контекст выполнения внешней функции)
      @param(AMetadata Метаданные внешней функции)
      @param(AInBuilder Построитель сообщения для входных метаданных)
      @param(AOutBuilder Построитель сообщения для выходных метаданных)
    }
    procedure setup(AStatus: IStatus; AContext: IExternalContext;
      AMetadata: IRoutineMetadata; AInBuilder: IMetadataBuilder;
      AOutBuilder: IMetadataBuilder); override;

    { Создание нового экземпляра внешней функции TSumArgsFunction

      @param(AStatus Статус вектор)
      @param(AContext Контекст выполнения внешней функции)
      @param(AMetadata Метаданные внешней функции)
      @returns(Экземпляр внешней функции)
    }
    function newItem(AStatus: IStatus; AContext: IExternalContext;
      AMetadata: IRoutineMetadata): IExternalFunction; override;
  end;

  // Внешняя функция TSumArgsFunction.
  TSumArgsFunction = class(IExternalFunctionImpl)
    // Вызывается при уничтожении экземпляра функции
    procedure dispose(); override;

    { Этот метод вызывается непосредственно перед execute и сообщает
      ядру наш запрошенный набор символов для обмена данными внутри
      этого метода. Во время этого вызова контекст использует набор символов,
      полученный из ExternalEngine::getCharSet.

      @param(AStatus Статус вектор)
      @param(AContext Контекст выполнения внешней функции)
      @param(AName Имя набора символов)
      @param(AName Длина имени набора символов)
    }
    procedure getCharSet(AStatus: IStatus; AContext: IExternalContext;
      AName: PAnsiChar; ANameSize: Cardinal); override;

    { Выполнение внешней функции

      @param(AStatus Статус вектор)
      @param(AContext Контекст выполнения внешней функции)
      @param(AInMsg Указатель на входное сообщение)
      @param(AOutMsg Указатель на выходное сообщение)
    }
    procedure execute(AStatus: IStatus; AContext: IExternalContext;
      AInMsg: Pointer; AOutMsg: Pointer); override;
  end;

implementation

{ TSumArgsFunctionFactory }

procedure TSumArgsFunctionFactory.dispose;
begin
  Destroy;
end;

function TSumArgsFunctionFactory.newItem(AStatus: IStatus;
  AContext: IExternalContext; AMetadata: IRoutineMetadata): IExternalFunction;
begin
  Result := TSumArgsFunction.Create();
end;

procedure TSumArgsFunctionFactory.setup(AStatus: IStatus;
  AContext: IExternalContext; AMetadata: IRoutineMetadata;
  AInBuilder, AOutBuilder: IMetadataBuilder);
begin

end;

{ TSumArgsFunction }

procedure TSumArgsFunction.dispose;
begin
  Destroy;
end;

procedure TSumArgsFunction.execute(AStatus: IStatus; AContext: IExternalContext;
  AInMsg, AOutMsg: Pointer);
var
  xInput: PSumArgsInMsg;
  xOutput: PSumArgsOutMsg;
begin
  // преобразовываем указатели на вход и выход к типизированным
  xInput := PSumArgsInMsg(AInMsg);
  xOutput := PSumArgsOutMsg(AOutMsg);
  // по умолчанию выходной аргумент = NULL, а поэтому выставляем ему nullFlag
  xOutput^.resultNull := True;
  // если один из аргументов NULL значит и результат NULL
  // в противном случае считаем сумму аргументов
  with xInput^ do
  begin
    if not (n1Null or n2Null or n3Null) then
    begin
      xOutput^.result := n1 + n2 + n3;
      // раз есть результат, то сбрасываем NULL флаг
      xOutput^.resultNull := False;
    end;
  end;
end;

procedure TSumArgsFunction.getCharSet(AStatus: IStatus;
  AContext: IExternalContext; AName: PAnsiChar; ANameSize: Cardinal);
begin
end;

end.

Фабрика внешней функции должна реализовать интерфейсIUdrFunctionFactory. Для упрощения просто наследуем классIUdrFunctionFactoryImpl. Для каждой внешней функции нужна своя фабрика.Впрочем, если фабрики не имеют специфики для создания некоторой функции,то можно написать обобщённую фабрику с помощью дженериков. Позже мыприведём пример как это сделать.

Метод dispose вызывается при уничтожении фабрики, в нём мы должныосвободить ранее выделенные ресурсы. В данном случае просто вызываемдеструктор.

Метод setup выполняется каждый раз при загрузке внешней функции в кешметаданных. В нём можно делать различные действия которые необходимыперед созданием экземпляра функции, например изменить формат для входныхи выходных сообщений. Более подробно поговорим о нём позже.

Метод newItem вызывается для создания экземпляра внешней функции. В этотметод передаётся указатель на статус вектор, контекст внешней функции иметаданные внешней функции. С помощью IRoutineMetadata вы можетеполучить формат входного и выходного сообщения, тело внешней функции идругие метаданные. В этом методе вы можете создавать различныеэкземпляры внешней функции в зависимости от её объявления в PSQL.Метаданные можно передать в созданный экземпляр внешней функции если этонеобходимо. В нашем случае мы просто создаём экземпляр внешней функцииTSumArgsFunction.