Написание UDR на языке Pascal

?v=1

В Firebird уже достаточно давно существует возможность расширения возможностей языка PSQL с помощью написания внешних функций — UDF (User Defined Functions). UDF можно писать практически на любом компилируемом языке программирования.

В Firebird 3.0 была введена плагинная архитектура для расширения возможностей Firebird. Одним из таких плагинов является External Engine (внешние движки). Механизм UDR (User Defined Routines — определяемые пользователем подпрограммы) добавляет слой поверх интерфейса движка FirebirdExternal.

В данном руководстве мы расскажем как объявлять UDR, о их внутренних механизмах, возможностях и приведём примеры написания UDR на языке Pascal. Кроме того, будут затронуты некоторые аспекты использования нового объектно-ориентированного API.


Замечание

Данная статья предназначена для обучения написанию UDR с помощью объектного Firebird API.
Написанные функции и процедуры могут не иметь практического применения.

UDR имеют следующие преимущества по сравнению с Legacy UDF:


  • можно писать не только функции возвращающие скалярный результат, но и хранимые процедуры (как выполняемые, так и селективные), а так же триггеры;
  • улучшенный контроль входных и выходных параметров. В ряде случаев (передача по дескриптору) типы и другие свойства входных параметров вообще не контролировались, однако вы могли получить эти свойства внутри UDF. UDR предоставляют более унифицированный способ объявления входных и выходных параметров, так как это делается в случае с обычными PSQL функциями и процедурами;
  • UDR доступен контекст текущего соединения или транзакции, что позволяет выполнять
    некоторые манипуляции с текущей базой данных в этом контексте;
  • доступна генерация ошибок Firebird при возникновении исключений, нет необходимости возвращать специальное значение;
  • внешние процедуры и функции (UDR) можно группировать в PSQL пакетах;
  • UDR могут быть написаны на любом языке программирования (необязательно компилируемые в объектные коды), для этого необходимо чтобы был написан соответствующий External Engine плагин. Например, существуют плагины для написания внешних модулей на Java или на любом из .NET языков.


Замечание

Текущая реализация UDR использует PSQL заглушку. Например, она используется для
проверки параметров и возвращаемых значений на соответствие ограничениям. Заглушка
была использована из-за негибкости для прямого вызова внутренних функций. Результаты
теста по сравнению производительности UDR и UDF показывает, что UDR примерно в
2.5 раза медленнее на примере простейшей функции сложения двух аргументов. Скорость
UDR приблизительно равна скорости обычной PSQL функции. Возможно в будущем этот
момент будет оптимизирован. В более сложных функциях эти накладные расходы могут стать
незаметными.

Далее в различных частях этого руководства при употреблении терминов внешняя процедура,
функция или триггер мы будем иметь ввиду именно UDR (а не UDF).


Замечание

Все наши примеры работают на Delphi 2009 и старше, а так же на Free Pascal. Все
примеры могут быть скомпилированы как в Delphi, так и в Free Pascal, если это
не оговорено отдельно.


Firebird API

Для написания внешних процедур, функций или триггеров на компилируемых языках программирования нам потребуются знания о новом объектно ориентированном API Firebird. Данное руководство не включает полного описания Firebird API. Вы можете ознакомится с ним в каталоге документации, распространяемой вместе с Firebird (doc/Using_OO_API.html).

Подключаемые файлы для различных языков программирования, содержащие интерфейсы API, не распространяются в составе дистрибутива Firebird под Windows, однако вы можете извлечь их из распространяемых под Linux сжатых tarbar файлов (путь внутри архива /opt/firebird/include/firebird/Firebird.pas).


CLOOP

CLOOP — Cross Language Object Oriented Programming. Этот инструмент не входит в поставку Firebird. Его можно найти в исходных кодах https://github.com/FirebirdSQL/firebird/tree/B3_0_Release/extern/cloop. После того как инструмент будет собран, можно на основе файла описания интерфейсов include/firebird/FirebirdInterface.idl сгенерировать API для вашего языка программирования (IdlFbInterfaces.h или Firebird.pas).

Для Object pascal это делается следующей командой:

cloop FirebirdInterface.idl pascal Firebird.pas Firebird --uses SysUtils \
  --interfaceFile Pascal.interface.pas \
  --implementationFile Pascal.implementation.pas \
  --exceptionClass FbException --prefix I \
  --functionsFile fb_get_master_interface.pas

Файлы Pascal.interface.pas, Pascal.implementation.pas и fb_get_master_interface.pas можно найти по адресу https://github.com/FirebirdSQL/firebird/tree/B3_0_Release/src/misc/pascal.


Замечание

В данном случае для интерфейсов Firebird API будет добавлен префикс I, так как это принято в Object Pascal.


Константы

В полученном файле Firebird.pas отсутствуют isc_* константы. Эти константы для языков C/C++ можно найти под адресу https://github.com/FirebirdSQL/firebird/blob/B3_0_Release/src/include/consts_pub.h. Для получения констант для языка Pascal воспользуемся AWK скриптом для преобразование синтаксиса. В Windows вам потребуется установить Gawk for Windows или воспользоваться Windows Subsystem for Linux (доступно в Windows 10). Это делается следующей командой:

awk -f Pascal.Constants.awk consts_pub.h > const.pas

Содержимое полученного файла необходимо скопировать в пустую секцию const файла Firebird.pas сразу после implementation. Файл Pascal.Constants.awk, можно найти по адресу
https://github.com/FirebirdSQL/firebird/tree/B3_0_Release/src/misc/pascal.


Управление временем жизни

Интерфейсы Firebird не основываются на спецификации COM, поэтому управление их временем жизни осуществляется иначе.

В Firebird существует два интерфейса, имеющих дело с управлением временем жизни: IDisposable и IReferenceCounted. Последний особенно активен при создании других интерфейсов: IPlugin подсчитывает ссылки, как и многие другие интерфейсы, используемые подключаемыми модулями. К ним относятся интерфейсы, которые описывают соединение с базой данных, управление транзакциями и операторы SQL.

Не всегда нужны дополнительные издержки интерфейса с подсчетом ссылок. Например, IMaster, основной интерфейс, который вызывает функции, доступные для остальной части API, имеет неограниченное время жизни по определению. Для других интерфейсов API время жизни строго определяется временем жизни родительского интерфейса; интерфейс IStatus не является
многопоточным. Для интерфейсов с ограниченным временем жизни полезно иметь простой способ их уничтожения, то есть функцию dispose ().


Подсказка

Если вы не знаете, как уничтожается объект, посмотрите его иерархию, если в ней есть
интерфейс IReferenceCounted, то используется подсчёт ссылок.
Для интерфейсов с подсчётом ссылок, по завершению работы с объектом необходимо
уменьшить счётчик ссылок вызовом метода release ().


Объявление UDR

UDR могут быть добавлены или удалены из базы данных с помощью DDL команд подобно тому, как вы добавляете или удаляете обычные PSQL процедуры, функции или триггеры. В этом случае вместо тела триггера указывается место его расположения во внешнем модуле с помощью предложения EXTERNAL NAME.

Рассмотрим синтаксис этого предложения, он будет общим для внешних процедур, функций и триггеров.

Синтаксис:

EXTERNAL NAME '' ENGINE  [AS ]

 ::= '![!]'

Аргументом этого предложения EXTERNAL NAME является строка, указывающая на расположение функции во внешнем модуле. Для внешних модулей, использующих движок UDR, в этой строке через разделитель указано имя внешнего модуля, имя функции внутри модуля и определённая пользователем информация. В качестве разделителя используется восклицательный знак (!).

В предложении ENGINE указывается имя движка для обработки подключения внешних модулей. В Firebird для работы с внешними модулями написанных на компилируемых языках (C, C++, Pascal) используется движок UDR. Для внешних функциях написанных на Java требуется движок Java.

После ключевого слова AS может быть указан строковый литерал — «тело» внешнего модуля (процедуры, функции или триггера), оно может быть использовано внешним модулем для различных целей. Например, может быть указан SQL запрос для доступа к внешней БД или текст на некотором языке для интерпретации вашей функцией.


Внешние функции


Синтаксис
{CREATE [OR ALTER] | RECREATE} FUNCTION funcname
[( [,  ...])] 
RETURNS  [COLLATE collation] [DETERMINISTIC]   
EXTERNAL NAME  ENGINE 
[AS ] 

 ::=  [{= |DEFAULT} ] 

 ::= {literal | NULL | context_var} 

 ::= paramname  [NOT NULL] [COLLATE collation] 

 ::= '! [!]' 

 ::=  | [TYPE OF] domain | TYPE OF COLUMN rel.col

 ::= 
  {SMALLINT | INT[EGER] | BIGINT} 
| BOOLEAN 
| {FLOAT | DOUBLE PRECISION} 
| {DATE | TIME | TIMESTAMP} 
| {DECIMAL | NUMERIC} [(precision [, scale])] 
| {CHAR | CHARACTER | CHARACTER VARYING | VARCHAR} [(size)] [CHARACTER SET charset] 
| {NCHAR |NATIONAL CHARACTER | NATIONAL CHAR} [VARYING] [(size)] 
| BLOB [SUB_TYPE {subtype_num | subtype_name}] [SEGMENT SIZE seglen] [CHARACTER SET charset] 
| BLOB [(seglen [, subtype_num])]

Все параметры внешней функции можно изменить с помощью оператора ALTER FUNCTION.

Синтаксис:

ALTER FUNCTION funcname [( [,  ...])]
RETURNS  [COLLATE collation] [DETERMINISTIC]
EXTERNAL NAME  ENGINE 
[AS ]

 ::= '![!]'

Удалить внешнюю функцию можно с помощью оператора DROP FUNCTION.

Синтаксис:

DROP FUNCTION funcname

Здесь мы не будем описывать синтаксис входных параметров и выходного результата. Он полностью соответствует синтаксису для обычных PSQL функций, который подробно описан в «Руководстве по языку SQL». Вместо этого приведём примеры объявления внешних функций с пояснениями.

Функция сложения трёх аргументов

create function sum_args (
    n1 integer,
    n2 integer,
    n3 integer
) returns integer
    external name 'udrcpp_example!sum_args'
    engine udr;

Реализация функции находится в модуле udrcpp_example. Внутри этого модуля функция зарегистрирована под именем sum_args. Для работы внешней функции используется движок UDR.

Функция на языке Java

create or alter function regex_replace (
  regex varchar(60),
  str varchar(60),
  replacement varchar(60)
) returns varchar(60)
  external name 'org.firebirdsql.fbjava.examples.fbjava_example.FbRegex.replace(
      String, String, String)'
  engine java;

Реализация функции находится в статической функции replace класса org.firebirdsql.fbjava.examples.fbjava_example.FbRegex. Для работы внешней функции используется движок Java.


Внешние процедуры


Синтаксис
{CREATE [OR ALTER] | RECREATE} PROCEDURE procname 
[( [,  ...])] 
RETURNS ( [ ...]) 
EXTERNAL NAME  ENGINE  
[AS ] 

 ::=  [{= | DEFAULT} ] 

 ::=   ::= {literal | NULL | context_var}

 ::= paramname  [NOT NULL] [COLLATE collation] 

 ::= '![!]' 

 ::=  | [TYPE OF] domain | TYPE OF COLUMN rel.col 

 ::=
    {SMALLINT | INT[EGER] | BIGINT} 
  | BOOLEAN 
  | {FLOAT | DOUBLE PRECISION}
  | {DATE | TIME | TIMESTAMP} 
  | {DECIMAL | NUMERIC} [(precision [,scale])] 
  | {CHAR | CHARACTER | CHARACTER VARYING | VARCHAR} [(size)] [CHARACTER SET charset] 
  | {NCHAR | NATIONAL CHARACTER | NATIONAL CHAR} [VARYING] [(size)] 
  | BLOB [SUB_TYPE {subtype_num | subtype_name}] [SEGMENT SIZE seglen] [CHARACTER SET charset] 
  | BLOB [(seglen [, subtype_num])]

Все параметры внешней процедуры можно изменить с помощью оператора ALTER PROCEDURE.

Синтаксис:

ALTER PROCEDURE procname [( [,  ...])]
RETURNS ( [,  ...]) EXTERNAL NAME
 ENGINE  [AS ]

Удалить внешнюю процедуру можно с помощью оператора DROP PROCEDURE.

Синтаксис:

DROP PROCEDURE procname

Здесь мы не будем описывать синтаксис входных и выходных параметров. Он полностью соответствует синтаксису для обычных PSQL процедур, который подробно описан в «Руководстве по языку SQL». Вместо этого приведём примеры объявления внешних процедур с пояснениями.

create procedure gen_rows_pascal (
    start_n integer not null,
    end_n integer not null
) returns (
    result integer not null
)
    external name 'pascaludr!gen_rows'
    engine udr;

Реализация функции находится в модуле pascaludr. Внутри этого модуля процедура зарегистрирована под именем gen_rows. Для работы внешней процедуры используется движок UDR.

create or alter procedure write_log (
  message varchar(100)
)
  external name 'pascaludr!write_log'
  engine udr;

Реализация функции находится в модуле pascaludr. Внутри этого модуля процедура зарегистрирована под именем write_log. Для работы внешней процедуры используется движок UDR.

create or alter procedure employee_pgsql (
  -- Firebird 3.0.0 has a bug with external procedures without parameters
  dummy integer = 1
) returns (
  id type of column employee.id,
  name type of column employee.name
)
  external name 'org.firebirdsql.fbjava.examples.fbjava_example.FbJdbc
    .executeQuery()!jdbc:postgresql:employee|postgres|postgres'
  engine java
  as 'select * from employee';

Реализация функции находится в статической функции executeQuery класса
org.firebirdsql.fbjava.examples.fbjava_example.FbJdbc. После восклицательного знака (!) располагаются сведения для подключения к внешней базе данных через JDBC. Для работы внешней функции используется движок Java. Здесь в качестве «тела» внешней процедуру передаётся SQL запрос для извлечения данных.


Замечание

В этой процедуре используется заглушка, в которой передаётся неиспользуемый параметр. Это связано с тем, что в Firebird 3.0 присутствует баг с обработкой внешних процедур без параметров.


Размещение внешних процедур и функций внутри пакетов

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


Синтаксис
{CREATE [OR ALTER] | RECREATE} PACKAGE package_name
AS
BEGIN
  [ ...]
END

{CREATE | RECREATE} PACKAGE BODY package_name
AS
BEGIN
  [ ...]
  [ ...]
END

 ::= ; | ;

 ::=
  FUNCTION func_name [()]
  RETURNS  [COLLATE collation] [DETERMINISTIC]

 ::=
  PROCEDURE proc_name [()]
  [RETURNS ()]

 ::=  | 

 ::=
  FUNCTION func_name [()]
  RETURNS  [COLLATE collation] [DETERMINISTIC]
  

 ::= 
  PROCEDURE proc_name [()]
  [RETURNS ()]
  

 ::=  | 

 ::=
AS    
  []
BEGIN
  []    
END

 ::=  [ ...]

 ::=
    ;
  | ;
  | ; 
  | 

 ::=  | 

 ::=  | 

 ::=
  EXTERNAL NAME  ENGINE 
  [AS ]

 ::= '![!]'

Для внешних процедур и функций в заголовке пакета указываются имя, входные параметры, их типы, значения по умолчанию, и выходные параметры, а в теле пакета всё тоже самое, кроме значений по умолчанию, а также место расположения во внешнем модуле (предложение EXTERNAL NAME), имя движка, и возможно «тело» процедуры/функции.

Предположим вы написали UDR для работы с регулярными выражениями, которая расположена во внешнем модуле (динамической библиотеке) PCRE, и у вас есть ещё несколько UDR выполняющих другие задачи. Если бы мы не использовали PSQL пакеты, то все наши внешние процедуры и функции были бы перемешаны как друг с другом, так и с обычными PSQL процедурами и функциями. Это усложняет поиск зависимостей и внесение изменений во внешние модули, а кроме того создаёт путаницу, и заставляет как минимум использовать префиксы для группировки процедур и функций. PSQL пакеты значительно облегчают нам эту задачу.


RegExp Package
SET TERM ^;

CREATE OR ALTER PACKAGE REGEXP
AS
BEGIN
  PROCEDURE preg_match(
      APattern VARCHAR(8192), ASubject VARCHAR(8192))
    RETURNS (Matches VARCHAR(8192));

  FUNCTION preg_is_match(
      APattern VARCHAR(8192), ASubject VARCHAR(8192))
    RETURNS BOOLEAN;

  FUNCTION preg_replace(
      APattern VARCHAR(8192),
      AReplacement VARCHAR(8192),
      ASubject VARCHAR(8192))
    RETURNS VARCHAR(8192);

  PROCEDURE preg_split(
      APattern VARCHAR(8192),
      ASubject VARCHAR(8192))
    RETURNS (Lines VARCHAR(8192));

  FUNCTION preg_quote(
      AStr VARCHAR(8192),
      ADelimiter CHAR(10) DEFAULT NULL)
    RETURNS VARCHAR(8192);
END^

RECREATE PACKAGE BODY REGEXP
AS
BEGIN
  PROCEDURE preg_match(
      APattern VARCHAR(8192),
      ASubject VARCHAR(8192))
    RETURNS (Matches VARCHAR(8192))
    EXTERNAL NAME 'PCRE!preg_match' ENGINE UDR;

  FUNCTION preg_is_match(
      APattern VARCHAR(8192),
      ASubject VARCHAR(8192))
    RETURNS BOOLEAN
  AS
  BEGIN
    RETURN EXISTS(
      SELECT * FROM preg_match(:APattern, :ASubject));
  END

  FUNCTION preg_replace(
      APattern VARCHAR(8192),
      AReplacement VARCHAR(8192),
      ASubject VARCHAR(8192))
    RETURNS VARCHAR(8192)
    EXTERNAL NAME 'PCRE!preg_replace' ENGINE UDR;

  PROCEDURE preg_split(
      APattern VARCHAR(8192),
      ASubject VARCHAR(8192))
    RETURNS (Lines VARCHAR(8192))
    EXTERNAL NAME 'PCRE!preg_split' ENGINE UDR;

  FUNCTION preg_quote(
      AStr VARCHAR(8192),
      ADelimiter CHAR(10))
    RETURNS VARCHAR(8192)
    EXTERNAL NAME 'PCRE!preg_quote' ENGINE UDR;
END^

SET TERM ;^


Внешние триггеры


Синтаксис
{CREATE [OR ALTER] | RECREATE} TRIGGER trigname 
{ |  |  |  } 


 ::= 
  EXTERNAL NAME  ENGINE  
  [AS ] 

 ::= 
  FOR {tablename | viewname} [ACTIVE | INACTIVE] 
  {BEFORE | AFTER}  [POSITION number]

 ::= 
  [ACTIVE | INACTIVE] 
  {BEFORE | AFTER}  [POSITION number] 
  ON {tablename | viewname} 

 ::= 
  [ACTIVE | INACTIVE] ON db_event [POSITION number] 

 ::= 
  [ACTIVE | INACTIVE] 
  {BEFORE | AFTER}  [POSITION number]

 ::=  [OR  [OR ]] 

 ::= INSERT | UPDATE | DELETE

 ::= 
    CONNECT | DISCONNECT 
  | TRANSACTION START | TRANSACTION COMMIT | TRANSACTION ROLLBACK 

 ::= ANY DDL STATEMENT |  [{OR } ...] 

 ::= 
    CREATE TABLE | ALTER TABLE | DROP TABLE 
  | CREATE PROCEDURE | ALTER PROCEDURE | DROP PROCEDURE 
  | CREATE FUNCTION | ALTER FUNCTION | DROP FUNCTION 
  | CREATE TRIGGER | ALTER TRIGGER | DROP TRIGGER 
  | CREATE EXCEPTION | ALTER EXCEPTION | DROP EXCEPTION 
  | CREATE VIEW | ALTER VIEW | DROP VIEW 
  | CREATE DOMAIN | ALTER DOMAIN | DROP DOMAIN 
  | CREATE ROLE | ALTER ROLE | DROP ROLE 
  | CREATE SEQUENCE | ALTER SEQUENCE | DROP SEQUENCE 
  | CREATE USER | ALTER USER | DROP USER 
  | CREATE INDEX | ALTER INDEX | DROP INDEX 
  | CREATE COLLATION | DROP COLLATION | ALTER CHARACTER SET 
  | CREATE PACKAGE | ALTER PACKAGE | DROP PACKAGE 
  | CREATE PACKAGE BODY | DROP PACKAGE BODY 
  | CREATE MAPPING | ALTER MAPPING | DROP MAPPING

Внешний триггер можно изменить с помощью оператора ALTER TRIGGER.

Синтаксис:

ALTER TRIGGER trigname { 
  [ACTIVE | INACTIVE] [ {BEFORE | AFTER}
  { | } | ON db_event ]
  [POSITION number] 
  [] 

 ::= 
  EXTERNAL NAME  ENGINE  
  [AS ] 

 ::= '![!]' 

 ::=  [OR  [OR ]]

 ::= { INSERT | UPDATE | DELETE }

Удалить внешний триггер можно с помощью оператора DROP TRIGGER.

Синтаксис:

DROP TRIGGER trigname

Приведём примеры объявления внешних триггеров с пояснениями.

create database 'c:\temp\slave.fdb';

create table persons (
    id integer not null,
    name varchar(60) not null,
    address varchar(60),
    info blob sub_type text
);

commit;

create database 'c:\temp\master.fdb';

create table persons (
    id integer not null,
    name varchar(60) not null,
    address varchar(60),
    info blob sub_type text
);

create table replicate_config (
    name varchar(31) not null,
    data_source varchar(255) not null
);

insert into replicate_config (name, data_source)
   values ('ds1', 'c:\temp\slave.fdb');

create trigger persons_replicate
    after insert on persons
    external name 'udrcpp_example!replicate!ds1'
    engine udr;

Реализация триггера находится в модуле udrcpp_example. Внутри этого модуля триггер зарегистрирован под именем replicate. Для работы внешнего триггера используется движок UDR.

В ссылке на внешний модуль используется дополнительный параметр ds1, по которому внутри внешнего триггера из таблицы replicate_config читается конфигурация для связи с внешней базой данных.


Структура UDR

Теперь настало время написать первую 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.


Замечание

Если вы разрабатываете вашу 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. Третьим аргументом передаётся экземпляр фабрики для создания функции (процедуры или триггера).


Реализация внешней функции

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


Исходный код модуля 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 значит и результат NULL
  xOutput^.resultNull := xInput^.n1Null or xInput^.n2Null or xInput^.n3Null;
  xOutput^.result := xInput^.n1 + xInput^.n2 + xInput^.n3;
end;

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

end.

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

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

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

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

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

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

Теперь перейдём к описанию экземпляра функции.

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

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

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

Входные и выходные сообщения имеют фиксированную ширину, которая зависит от типов данных декларируемых для входных и выходных переменных соответственно. Это позволяет использовать типизированные указатели на структуры фиксированный ширины, члены который должны соответствовать типам данных. Из примера видно, что для каждой переменной в структуре указывается член соответствующего типа, после чего идёт член, который является признаком специального значения NULL (далее Null флаг). Помимо работы с буферами входных и выходных сообщений через структуры, существует ещё один способ с использованием адресной арифметики на указателях с использованием смещениях, значения которых можно получить из интерфейса IMessageMetadata. Подробнее о работе с сообщениями мы поговорим далее, а сейчас просто поясним что делалось в методе execute.

Первым делом мы преобразовываем не типизированные указатели к типизированным. Для
выходного значения устанавливаем Null флаг равный логическому объединению Null флагов
у всех входных аргументов, если ни один из входных аргументов не равен NULL, то выходное
значение будет равно сумме значений аргументов


Реализация внешней процедуры

Пришло время добавить в наш UDR модуль хранимую процедуру. Как известно хранимые процедуры бывают двух видов: выполняемые хранимые процедуры и хранимые процедуры для выборки данных. Сначала добавим выполняемую хранимую процедуру, т.е. такую хранимую процедуру которая может быть вызвана с помощью оператора EXECUTE PROCEDURE и может вернуть не более одной записи.

Вернитесь в модуль UdrInit и измените функцию firebird_udr_plugin так чтобы она выглядела следующим образом.

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;


Замечание

Не забудьте добавить с список uses модуль SumArgsProc, в котором и будет расположена наша процедура.

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

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

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

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

Фабрику процедуры, а также саму процедуру расположим в модуле SumArgsProc.


Исходный код модуля SumArgsProc
unit SumArgsProc;

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

interface

uses
  Firebird;

  { **********************************************************

    create procedure sp_sum_args (
      n1 integer,
      n2 integer,
      n3 integer
    ) returns (result integer)
    external name 'myudr!sum_args_proc'
    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;

  // Фабрика для создания экземпляра внешней процедуры TSumArgsProcedure
  TSumArgsProcedureFactory = class(IUdrProcedureFactoryImpl)
    // Вызывается при уничтожении фабрики
    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;

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

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

  TSumArgsProcedure = class(IExternalProcedureImpl)
  public
    // Вызывается при уничтожении экземпляра процедуры
    procedure dispose(); override;

    { Этот метод вызывается непосредственно перед open и сообщает
      ядру наш запрошенный набор символов для обмена данными внутри
      этого метода. Во время этого вызова контекст использует набор символов,
      полученный из 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 Указатель на выходное сообщение)
      @returns(Набор данных для селективной процедуры или
               nil для процедур выполнения)
    }
    function open(AStatus: IStatus; AContext: IExternalContext; AInMsg: Pointer;
      AOutMsg: Pointer): IExternalResultSet; override;
  end;

implementation

{ TSumArgsProcedureFactory }

procedure TSumArgsProcedureFactory.dispose;
begin
  Destroy;
end;

function TSumArgsProcedureFactory.newItem(AStatus: IStatus;
  AContext: IExternalContext; AMetadata: IRoutineMetadata): IExternalProcedure;
begin
  Result := TSumArgsProcedure.create;
end;

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

end;

{ TSumArgsProcedure }

procedure TSumArgsProcedure.dispose;
begin
  Destroy;
end;

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

end;

function TSumArgsProcedure.open(AStatus: IStatus; AContext: IExternalContext;
  AInMsg, AOutMsg: Pointer): IExternalResultSet;
var
  xInput: PSumArgsInMsg;
  xOutput: PSumArgsOutMsg;
begin
  Result := nil;
  // преобразовываем указатели на вход и выход к типизированным
  xInput := PSumArgsInMsg(AInMsg);
  xOutput := PSumArgsOutMsg(AOutMsg);
  // если один из аргументов NULL значит и результат NULL
  xOutput^.resultNull := xInput^.n1Null or xInput^.n2Null or xInput^.n3Null;
  xOutput^.result := xInput^.n1 + xInput^.n2 + xInput^.n3;
end;

end.

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

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

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

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

© Habrahabr.ru