Здравствуйте! Меня зовут Алексей Калинин. Это вторая моя статья, посвященная разработанному мною Интерпретатору. На просторах интернета мне встретилась вот эта интересная статья, в которой автор описал возможности расширения функциональности БД PostgreSQL. После ее прочтения я решил попробовать встроить разработанный мною интерпретатор в БД PostgreSQL (тем более сам автор статьи предлагал это попробовать). За одно и проверить две вещи: легко ли добавить новый язык (новое расширение) в PostgreSQL и хватит ли возможностей моего интерпретатора для работы внутри БД. И если эксперимент по встраиванию нового языка в PostgreSQL пройдет успешно, тогда код, написанные на нем, можно использовать в реальных проектах.   

Вот краткое описание возможностей языка, для которого разработан интерпретатор:

  • По синтаксису он похож на Oracle PL/SQL.

  • Поддерживает следующие типы данных: varchar2, integer, number, date, json, xml, html, record, boolean.

  • Поддерживает работу с курсорами, в нем реализованы операторы: for, while, open, if.

  • Имеются следующие встроенные пакеты:

    a)    TEXT_IO – для работы с локальными файлами.

    b)    HTTP_IO – для работы с http/https- запросами из БД.

    c)    JSON_IO – для работы с JSON.

    d)    RECORD_IO – для работы с переменными типа record.

    e)    XML_IO – для парсинга (разбора) XML-документов.

    f)     HTML_IO – для парсинга (разбора) HTML-документов.

  • Реализован оператор execute для выполнения произвольного текста.

  • Также реализована поддержка различных встроенных функций – instr, substr, trim, sysdate и т. д.

Более полное описание языка можно найти на github. (https://github.com/ank-75/pl_cpl_sql)

Встраивание интерпретатора в PostgreSQL.

Согласно указанной статьи, для реализации языка в PostgreSQL надо написать на C три функции:

  • CALL HANDLER - обработчик вызова, который будет исполнять процедуру/функцию на языке;

  • INLINE HANDLER - обработчик анонимных блоков;

  • VALIDATOR - функцию проверки кода при создании процедуры/функции.

Для того, чтобы внутри C-функций можно было бы обращаться к моему интерпретатору, я перенес код из Delphi в Lazarus и собрал там в виде динамической библиотеки (pl_cpl_sql_lib.dll для Windows, libpl_cpl_sql_lib.so для Linux). Бесплатный Lazarus оказался тем хорош, что позволил один и тот же код собрать для разных ОС.

Также мне пришлось доработать интерпретатор. Для описания процедур/функции пришлось добавить обработку введенных параметров и возврат значения.

Все указанные в этой статье действия проводились компьютере с ОС Alt Linux.

На github также есть версии библиотек для Windows, инструкция, как установить расширение в БД и каталог с примерами кода.

В качестве клиентского средства для работы с БД использовался DBeaver.

Далее я создал C-файл (pl_cpl_sql_ext.c) и написал в нем три функции. Возможно, код и не оптимален, так как опыта программирования на С у меня не было. Последний раз я программировал на С лет 20 назад. Так как в сети довольно мало информации на данную тему, мне пришлось изучать исходные коды расширений plpython и plpgsql. Также позже мне встретилась вот такая статья , в которой есть раздел подробного описания механизма создания расширений для PostgreSQL.

Новый язык в PostgreSQL будет называться - pl_cpl_sql. Полный код C-файла можно посмотреть на github. Здесь рассмотрим отдельные его элементы.

  • Функция для проверки кода перед созданием процедуры/функции - pl_cpl_sql_validator.

//-----------------------------------------------------------
//-----------------------------------------------------------
//-----------------------------------------------------------
// Реализация функции для выполнения проверки корректности кода
Datum pl_cpl_sql_validator(PG_FUNCTION_ARGS) {

    Oid             func_oid = PG_GETARG_OID(0);

    char* 			prosrc;
    int			    numargs;
    Oid* 			argtypes;
    char** 			argnames;
    char* 			argmodes;
    Form_pg_proc    pl_struct;
    char* 			proname;
    bool		    isnull;
    Datum		    ret;
    FmgrInfo* 		arg_out_func;
    Form_pg_type    type_struct;
    HeapTuple	    type_tuple;
    int             i;
    int             pos;
    Oid* 			types;
    Oid			    rettype;

    PG_TRY();
    {

        //-----------------------------------------
        //---Проинициализируем структуру, которую будем передавать в качестве входных параметров
        THandlerInputInfoRec* input_info_struct = (THandlerInputInfoRec*)palloc(sizeof(THandlerInputInfoRec));

        input_info_struct->Args = (char*)palloc(200 * sizeof(char)); // Выделяем память для строки
        input_info_struct->RetType = (char*)palloc(100 * sizeof(char)); // Выделяем память для строки

        //---Присвоим значения по умолчанию
        strcpy(input_info_struct->Args, "");
        strcpy(input_info_struct->RetType, "");

        //-----------------------------------------
        // Получаем кортеж функции по OID
        HeapTuple tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(func_oid));
        if (!HeapTupleIsValid(tuple)) {
            ereport(ERROR, (errmsg("Function with OID %u does not exist", func_oid)));
        }

        //--------------------------------------
        //---Получим текст функции для проверки и выполнения
        pl_struct = (Form_pg_proc)GETSTRUCT(tuple);
        proname = pstrdup(NameStr(pl_struct->proname));

        ret = SysCacheGetAttr(PROCOID, tuple, Anum_pg_proc_prosrc, &isnull);
        if (isnull)
            elog(ERROR, "could not find source text of function \"%s\"",
                proname);

        //--------------------------------------
        //---Текст исполняемой процедуры/функции
        prosrc = DatumGetCString(DirectFunctionCall1(textout, ret));

        size_t length = strlen(prosrc);
        input_info_struct->ProcSrc = (char*)palloc((length + 1) * sizeof(char)); // Выделяем память для строки

        //---Сохраним в структуру текст проверяемой функции
        strcpy(input_info_struct->ProcSrc, prosrc);

        //--------------------------------------
        //---Получим кол-во аргументов функции
        numargs = get_func_arg_info(tuple, &types, &argnames, &argmodes);

        //--------------------------------------
        //---Переберем все аргументы процедуры/функции
        for (i = pos = 0; i < numargs; i++)
        {
            HeapTuple	    argTypeTup;
            Form_pg_type    argTypeStruct;
            char* value;

            Assert(types[i] == pl_struct->proargtypes.values[pos]);

            argTypeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(types[i]));
            if (!HeapTupleIsValid(argTypeTup))
                elog(ERROR, "cache lookup failed for type %u", types[i]);

            argTypeStruct = (Form_pg_type)GETSTRUCT(argTypeTup);

            ReleaseSysCache(argTypeTup);

            //---Сохраним данные в структуру
            strcat(input_info_struct->Args, argnames[i]);
            strcat(input_info_struct->Args, ",");
        }

        //---Определим тип возвращаемого значения
        rettype = pl_struct->prorettype;

        //---Сохраним данные в структуру
        strcat(input_info_struct->RetType, format_type_be(rettype));

        //----------------------------------------------------
        //----------------------------------------------------
        //--Присоединяем библиотеку

		void* hlib = dlopen("libpl_cpl_sql_lib.so", RTLD_NOW | RTLD_GLOBAL);

        if (hlib) {
            Pl_Cpl_Sql_Validate_Func ProcAdd = (Pl_Cpl_Sql_Validate_Func)dlsym(hlib, "pl_cpl_sql_validator");

            if (ProcAdd) {
                THandlerResultRec* result = ProcAdd(input_info_struct); // Вызов функции из DLL

                //---Освободим память
				dlclose(hlib);

                //---если обранужилась ошибка
                if (result->OutType == 1) {
                    ereport(ERROR,
                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                            errmsg("%s", result->Errors)));
                }
            }
            else {
		        //---Освободим память
				dlclose(hlib);

                ereport(ERROR,
                    (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                        errmsg("ERR-01: Ошибка при выполнении валидации!")));
            }
        }
        else {
            ereport(ERROR,
                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                    errmsg("ERR-02: Библиотека не найдена!")));
        }

        ReleaseSysCache(tuple); // Освобождаем кэш
    }
    PG_CATCH();
    {
        PG_RE_THROW();
    }
    PG_END_TRY();

    // Если все проверки пройдены, просто возвращаем
    PG_RETURN_VOID();
}

Из переданных аргументов (PG_FUNCTION_ARGS)  получаем код создаваемой процедуры/функции. Подключаем библиотеку интерпретатора (libpl_cpl_sql_lib.so) и передаем ей текст на проверку. Если текст не корректный – возвращаем ошибку. Если ошибок не обнаружено - процедура/функция создается в БД.

  • Функция для выполнения кода процедуры/функции - pl_cpl_sql_call_handler.

//-----------------------------------------------------------
//-----------------------------------------------------------
//-----------------------------------------------------------
// Реализация функции для выполнения кода
Datum pl_cpl_sql_call_handler(PG_FUNCTION_ARGS) {

    HeapTuple	    pl_tuple;
    Datum		    ret;
    char* 			prosrc;
    bool		    isnull;
    FmgrInfo* 		arg_out_func;
    Form_pg_type    type_struct;
    HeapTuple	    type_tuple;
    Form_pg_proc    pl_struct;
    volatile MemoryContext proc_cxt = NULL;
    Oid* 			argtypes;
    char** 			argnames;
    char* 			argmodes;
    char* 			proname;
    Form_pg_type    pg_type_entry;
    Oid			    result_typioparam;
    Oid			    prorettype;
    FmgrInfo	    result_in_func;
    int			    numargs;
    size_t          length;

    PG_TRY();
    {
        //-----------------------------------------
        //---Проинициализируем структуру, которую будем передавать в качестве входных параметров
        THandlerInputInfoCallRec* input_info_struct = (THandlerInputInfoCallRec*)palloc(sizeof(THandlerInputInfoCallRec));

        //-----------------------------------------
        pl_tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(fcinfo->flinfo->fn_oid));

        //-----------------------------------------
        if (!HeapTupleIsValid(pl_tuple))
            elog(ERROR, "cache lookup failed for function %u",
                fcinfo->flinfo->fn_oid);

        //-----------------------------------------
        pl_struct = (Form_pg_proc)GETSTRUCT(pl_tuple);

        proname = pstrdup(NameStr(pl_struct->proname));

        ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &isnull);

        if (isnull)
            elog(ERROR, "could not find source text of function \"%s\"",
                proname);

        //---Текст исполняемой процедуры/функции
        prosrc = DatumGetCString(DirectFunctionCall1(textout, ret));

        //-------------------------------------------------
        //---Запишем текст процедуры функции в структуру---
        //-------------------------------------------------
        length = strlen(prosrc);
        input_info_struct->ProcSrc = (char*)palloc((length + 1) * sizeof(char)); // Выделяем память для строки
        strcpy(input_info_struct->ProcSrc, prosrc);

        //-------------------------------------------------
        proc_cxt = AllocSetContextCreate(TopMemoryContext, "PL_CPL_SQL function", ALLOCSET_SMALL_SIZES);

        //-------------------------------------------------
        arg_out_func = (FmgrInfo*)palloc0(fcinfo->nargs * sizeof(FmgrInfo));

        //---Получим аргументы функции
        numargs = get_func_arg_info(pl_tuple, &argtypes, &argnames, &argmodes);

        //-------------------------------------------------
        //---Массив с параметрами--------------------------
        TArgsArray args_arr;
        args_arr.count = numargs;
        args_arr.ArgName = palloc(numargs * sizeof(char*));
        args_arr.ArgValue = palloc(numargs * sizeof(char*));
        args_arr.ArgType = palloc(numargs * sizeof(char*));

        //-------------------------------------------------
        //---Обойдем все аргументы-------------------------
        int param_count = 0;
        int pos = 0;
        for (int i = 0; i < numargs; i++)
        {
            Oid			    argtype = pl_struct->proargtypes.values[i];
            char* value;

            Assert(argtypes[i] == pl_struct->proargtypes.values[pos]);

            type_tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(argtype));
            if (!HeapTupleIsValid(type_tuple))
                elog(ERROR, "cache lookup failed for type %u", argtype);

            type_struct = (Form_pg_type)GETSTRUCT(type_tuple);
            fmgr_info_cxt(type_struct->typoutput, &(arg_out_func[i]), proc_cxt);
            ReleaseSysCache(type_tuple);

            value = OutputFunctionCall(&arg_out_func[i], fcinfo->args[i].value);

            //----------------------------------
            //---Сохраним значения параметров
            args_arr.ArgName[i] = argnames[i];
            args_arr.ArgValue[i] = value;
            args_arr.ArgType[i] = format_type_be(argtypes[i]);

            param_count++;
        }

        /* Тип возвращаемого значения */
        prorettype = pl_struct->prorettype;
        ReleaseSysCache(pl_tuple);

        //---Запишем в структуру------------
        length = strlen(format_type_be(prorettype));
        input_info_struct->RetType = (char*)palloc((length + 1) * sizeof(char)); // Выделяем память для строки

        strcpy(input_info_struct->RetType, format_type_be(prorettype));

        char* exec_result;

        //-----------------------------------
        //--Присоединяем библиотеку
		void* hlib = dlopen("libpl_cpl_sql_lib.so", RTLD_NOW | RTLD_GLOBAL);

        if (hlib) {
            Pl_Cpl_Sql_Call_Func ProcAdd = (Pl_Cpl_Sql_Call_Func)dlsym(hlib, "pl_cpl_sql_call_handler");

            if (ProcAdd) {
                THandlerResultCallRec* result = ProcAdd(input_info_struct, &args_arr); // Вызов функции из DLL

		        //---Освободим память
				dlclose(hlib);

                //---Определим тип сообщения
                if (result->OutType == 1) {
                    //---Если была только ошибка
                    ereport(ERROR,
                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                            errmsg("%s", result->Errors)));
                }

                if (result->OutType == 2) {
                    //---Если было только сообщение (вывод сообщения уровня NOTICE)
                    elog(NOTICE, "%s", result->Messages);
                }

                if (result->OutType == 3) {
                    //---Если была ошибка и сообщение
                    elog(NOTICE, "%s", result->Messages);

                    ereport(ERROR,
                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                            errmsg("%s", result->Errors)));
                }

                //---Только если тип возвращаемого значения не void
                if (prorettype != VOIDOID) {
                    //---Обработаем полученный результат
                    length = strlen(result->Result);
                    exec_result = (char*)palloc((length + 1) * sizeof(char)); // Выделяем память для строки
                    strcpy(exec_result, result->Result);
                }
            }
            else {
                //---Освободим память
				dlclose(hlib);

                ereport(ERROR,
                    (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                        errmsg("ERR-01: Ошибка выполнения!")));
            }
        }
        else {
            ereport(ERROR,
                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                    errmsg("ERR-02: Библиотека не найдена!")));
        }


        //---Если нет возвращаемого значения
        if (prorettype == VOIDOID)
        {
            //---Выходим
            PG_RETURN_NULL();
        }
        else {
            type_tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(prorettype));

            if (!HeapTupleIsValid(type_tuple))
                elog(ERROR, "cache lookup failed for type %u", prorettype);

            pg_type_entry = (Form_pg_type)GETSTRUCT(type_tuple);

            result_typioparam = getTypeIOParam(type_tuple);

            fmgr_info_cxt(pg_type_entry->typinput, &result_in_func, proc_cxt);
            ReleaseSysCache(type_tuple);

            ret = InputFunctionCall(&result_in_func, exec_result, result_typioparam, -1);

        }
    }
    PG_CATCH();
    {
        PG_RE_THROW();
    }
    PG_END_TRY();

    //---Возвращаем значение
    PG_RETURN_DATUM(ret);
}

Из переданных аргументов (PG_FUNCTION_ARGS) получаем код выполняемой процедуры/функции, значения параметров и тип возвращаемого значения. Подключаем библиотеку интерпретатора (libpl_cpl_sql_lib.so) и передаем ей эти данные на выполнение. Если текст не корректный – возвращаем ошибку, если все нормально – возвращаем результат выполнения и текст сообщения (если оно было). Передавать в процедуру/функцию можно только параметры следующих типов (postgreSQL): numeric, bigint, varchar, text, json, jsonb. При выполнении кода данные типы транслируются во внутренние типы интерпретатора.

  • Функция для выполнения кода анонимного блока - pl_cpl_sql_inline_handler.

//-----------------------------------------------------------
//-----------------------------------------------------------
//-----------------------------------------------------------
// Реализация функции для выполнения анонимного блока do $$ begin end; $$
Datum pl_cpl_sql_inline_handler(PG_FUNCTION_ARGS) {

    LOCAL_FCINFO(fake_fcinfo, 0);
    InlineCodeBlock* codeblock = (InlineCodeBlock*)DatumGetPointer(PG_GETARG_DATUM(0));

    PG_TRY();
    {

		//--Присоединяем библиотеку
		void* hlib = dlopen("libpl_cpl_sql_lib.so", RTLD_NOW | RTLD_GLOBAL);

		if (hlib) {
		    Pl_Cpl_Sql_inline_Func ProcAdd = (Pl_Cpl_Sql_inline_Func)dlsym(hlib, "pl_cpl_sql_inline_handler");

		    if (ProcAdd) {

		        THandlerResultRec* result = ProcAdd(codeblock->source_text); // Вызов функции из DLL

		        //---Освободим память
				dlclose(hlib);

		        //---Определим тип сообщения
		        if (result->OutType == 1) {
		            //---Если была только ошибка
		            ereport(ERROR,
		                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
		                    errmsg("%s", result->Errors)));
		        }

		        if (result->OutType == 2) {
		            //---Если было только сообщение (вывод сообщения уровня NOTICE)
		            elog(NOTICE, "%s", result->Messages);
		        }

		        if (result->OutType == 3) {
		            //---Если была ошибка и сообщение
		            elog(NOTICE, "%s", result->Messages);

		            ereport(ERROR,
		                (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
		                    errmsg("%s", result->Errors)));
		        }

		    }
		    else {
		        //---Освободим память
				dlclose(hlib);

		        ereport(ERROR,
		            (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
		                errmsg("ERR-01: Ошибка выполнения!")));
		    }
		}
		else {
		    ereport(ERROR,
		        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
		            errmsg("ERR-02: Библиотека не найдена!")));
		}

	}
    PG_CATCH();
    {
        PG_RE_THROW();
    }
    PG_END_TRY();

    PG_RETURN_VOID();
}

Из аргументов функции получаем выполняемый код. Подключаем библиотеку интерпретатора (libpl_cpl_sql_lib.so) и передаем ей текст на выполнение. Если при выполнении кода возникли сообщения или ошибки - то выводим их.

Для работы с БД из C-кода имеется SPI (Server Programming Interface). Это набор C-функций для работы с данными. Так как мой интерпретатор реализован в виде внешней библиотеки, то такой способ выполнения операций с данным не подходит. Был выбран способ, когда в момент выполнения кода интерпретатором происходит создание отдельного соединение к БД (как будто работа происходит внутри клиентского приложения) (рис.1). Это способ чем-то похож на работу с БД из PL/Java.

рис. 1
рис. 1

В общем смысле, хотя интерпретатор и вызывается из локальной БД, он к ней не привязан. В каталог, где находиться библиотека интерпретатора, помещается файл с параметрами соединения к БД (conn_params.json) примерно следующего содержания:

[
	{"ConnType": "PSQL",
     "ConnName": "db_PG",
     "UserName": "username",
     "PassWord": "password",
     "HostName": "hostname",
     "DataBase": "database",
     "Port":     "port"
	},
	{"ConnType": "Oracle",
     "ConnName": "db_Oracle",
     "UserName": "username",
     "PassWord": "password",
     "HostName": "hostname",
     "DataBase": "database",
     "Port":     "port"
	}	
]

Используя данные параметры можно подсоединиться к текущей БД.

На следующем этапе создаем три файла следующего содержания:

  • pl_cpl_sql_ext.control – (управляющий файл, определяющий основные свойства нового языка).

comment = 'PL/сPLSQL procedural language'
default_version = '0.1'
module_pathname = '$libdir/pl_cpl_sql_ext'
relocatable = false
schema = pg_catalog
superuser = false
  • pl_cpl_sql_ext--1.0.sql – (файл SQL, который создает объекты нового языка)

-- handler
CREATE FUNCTION pl_cpl_sql_call_handler() RETURNS language_handler AS '$libdir/pl_cpl_sql_ext' LANGUAGE C;

-- inline
CREATE FUNCTION pl_cpl_sql_inline_handler(oid internal) RETURNS void AS '$libdir/pl_cpl_sql_ext', 'pl_cpl_sql_inline_handler' LANGUAGE C;

-- validator
CREATE FUNCTION pl_cpl_sql_validator(oid_ oid) RETURNS void AS '$libdir/pl_cpl_sql_ext', 'pl_cpl_sql_validator' LANGUAGE C;

CREATE TRUSTED LANGUAGE pl_cpl_sql HANDLER pl_cpl_sql_call_handler INLINE pl_cpl_sql_inline_handler validator pl_cpl_sql_validator;

COMMENT ON LANGUAGE pl_cpl_sql IS 'PL/cPLSQL procedural language';
  • makefile – (файл для сборки расширения)

MODULES = pl_cpl_sql_ext
EXTENSION = pl_cpl_sql_ext
DATA = pl_cpl_sql_ext--1.0.sql

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

Далее выполняем команды

Make

Make install.

Для Alt Linux происходит компиляция созданного C-файла и его регистрация в БД PostgreSQL.

Собранную библиотеку интерпретатора (pl_cpl_sql_lib.dll для Windows, libpl_cpl_sql_lib.so для Linux) выкладываем в каталог, где он будет виден БД PostgreSQL.

Далее в DBeaver подключаемся к БД выполняем файл pl_cpl_sql_ext--1.0.sql.

Выполняем запрос – select * from pg_language.

рис. 2
рис. 2

Видим, что в перечне языков появился новый язык - pl_cpl_sql. Так как язык создан как TRUSTED, то создавать процедуры/функции на нем могут любые пользователи.

Проверка работоспособности интерпретатора.

Будем выполнять код, написанный на языке – pl_cpl_sql.

В базе данных создаем схему – test. В ней будем проводить тестирование.

  • Пример 1. (выполним анонимный блок)

    Выполним анонимный блок, написанный на новом языке - pl_cpl_sql. Отправим post-запрос с помощью встроенного пакета http_io. В качестве языка указываем - LANGUAGE pl_cpl_sql.

do
LANGUAGE pl_cpl_sql
$$
declare 
    http_req http_io.req; 
    http_resp http_io.resp; 
begin 
    http_req := http_io.begin_request('https://httpbin.org/post', 'POST'); 
    http_io.set_header(http_req, 'Content-Type', 'application/json'); 
    http_io.set_resp_encoding(http_req, 'UTF8'); 
    http_io.write_text(http_req, '{"command":"FIND_INCOME_DOC", 
                                 "body":{"filter":{"start_date":"2024-06-26T00:00:00","end_date":"2024-06-26T23:59:59"},"start_from":0,"count":100} 
                                 }');     
    http_resp := http_io.get_response(http_req); 
    dbms_output.put_line('HTTP response status code: ' || http_resp.status_code); 
    dbms_output.put_line('HTTP response text: ' || http_resp.response_text); 
end;    
$$;

Выполняем код в DBeaver.

рис. 3
рис. 3

В окне «Вывод» видим полученный результат. Процедура dbms_output.put_line выводит результат в тот же буфер, в который выводит и стандартный оператор raise notice.

Если из кода уберем строку - LANGUAGE pl_cpl_sql – то получим сообщение об ошибке

рис. 4
рис. 4
  • Пример 2. Создание функции на языке pl_spl_sql.

CREATE OR REPLACE FUNCTION test.test_json(p_int bigint, p_name text, p_json json)
 RETURNS text
 LANGUAGE pl_cpl_sql
AS $function$
declare
    obj json := '{"main_obj": {"1": 123, "2": 456}}';
    new_obj json;
    arr json := '[]';
begin
    dbms_output.put_line('1. obj = '||obj);
    
	------------
	-- Добавим две пары ключ-значение
    obj := json_io.add(obj, 'id', '1000', 'name', p_name);
    dbms_output.put_line('2. obj = '||obj);
	
	------------
    --Добавим новый подобъект
    new_obj := '{"dddd": 890}';
    obj := json_io.add(obj, 'new_obj', new_obj);
    dbms_output.put_line('3. obj = '||obj);
	
    ------------
    --В массив arr добавим новый элемент
    arr := json_io.add(arr, '', new_obj);
    dbms_output.put_line('4. arr = '||arr);
	
    ------------
    --В массив arr добавим новый элемент
    arr := json_io.add(arr, '', '{"aaaa": 111}');
    dbms_output.put_line('5. arr = '||arr);
	
	------------
    --Добавим массив в объект - obj
    obj := json_io.add(obj, 'rows', arr);
    obj := json_io.add(obj, 'input_obj', p_json);
	
	------------
    --Выведем результат на экран 
    dbms_output.put_line('6. Результат: obj = '||json_io.format(obj));

	------------
    --Вернем результат как значение функции
    return obj;
END;
$function$

Выполняем код в DBeaver.

рис. 5
рис. 5

Видим, что функция успешно создана.

Далее выполним вызов функции с параметрами:

SELECT test.test_json(12345,'Привет!','{"ID": 123456789}');

рис. 6
рис. 6

Видим, что PostgreSQL успешно выполнил наше SQL-выражение. Был возвращен результат. Все выданные функцией сообщения отобразились в окне «Вывод».

  • Пример 3. Работа с данными БД PostgreSQL.

Как уже было сказано ранее, интерфейс SPI (Server Programming Interface) здесь не поддерживается. Для работы с данными БД в любой SQL-инструкции (курсора, select/insert/update/delete-выражения) в обязательном порядке должна быть указана метка /*##db=dbname##*/, в которой указано название БД, для которой будет выполнятся данная конструкция. Параметры соединения указаны в файле - conn_params.json.

В общем смысле из одного кода может быть сделано несколько соединения к различным БД (Oracle, PostgreSQL), а также несколько соединений к одной и той же БД.

В этом примере создадим таблицу в БД, заполним ее данными и выведем эти данные на экран.

do
LANGUAGE pl_cpl_sql
$$
declare
    sql_stmt    VARCHAR2;
  
    cursor cur_main_pg is 
		select * 
		    from /*##db=db_PG##*/ 
		        test.categories ct 
	    	order by ct.description;  
begin 
  dbms_output.put_line('--Create table');

  --Создадим тестовую таблицу 
  EXECUTE /*##db=db_PG##*/ 'CREATE TABLE if not exists test.categories (
    category_id smallint NOT NULL,
    category_name character varying(15) NOT NULL,
    description text
);';
  
  dbms_output.put_line('--Clear table');
 
  -- Очистим таблицу с категориями
  delete /*##db=db_PG##*/ from test.categories;

  dbms_output.put_line('--Insert'); 
 
  --Заполним таблицу данными 
  sql_stmt := '
do
$block$
begin
	INSERT INTO test.categories VALUES (1, ''Beverages'', ''Soft drinks, coffees, teas, beers, and ales'');
	INSERT INTO test.categories VALUES (2, ''Condiments'', ''Sweet and savory sauces, relishes, spreads, and seasonings'');
	INSERT INTO test.categories VALUES (3, ''Confections'', ''Desserts, candies, and sweet breads'');
	INSERT INTO test.categories VALUES (4, ''Dairy Products'', ''Cheeses'');
	INSERT INTO test.categories VALUES (5, ''Grains/Cereals'', ''Breads, crackers, pasta, and cereal'');
	INSERT INTO test.categories VALUES (6, ''Meat/Poultry'', ''Prepared meats'');
	INSERT INTO test.categories VALUES (7, ''Produce'', ''Dried fruit and bean curd'');
	INSERT INTO test.categories VALUES (8, ''Seafood'', ''Seaweed and fish'');
end;
$block$';
 
  	--выполним анонимный блок
  EXECUTE /*##db=db_PG##*/ sql_stmt;
  
  dbms_output.put_line('--Select data');
 
  --Выведем на экран содержимое созданной таблицы
  for rec in cur_main_pg loop
   	dbms_output.put_line(json_io.record_to_json(rec));
  end loop; 
 
end;
$$;

Выполним указанный код.

рис. 7
рис. 7

Видим, что код выполнен успешно. Таблица в БД создана и заполнена данными. Курсор по данным из таблицы вывел данные на экран в виде json.

  • Пример 4. Парсинг новостного сайта.

В данном примере мы, с помощью встроенного пакета http_io, загрузим содержимое сайта. Далее, с помощью другого пакета - html_io, распарсим его содержимое и выведем на экран перечень новостей.

do
LANGUAGE pl_cpl_sql
$$
declare 
    http_req http_io.req; 
    http_resp http_io.resp; 
   html_doc  html;
begin 
	dbms_output.put_line('Парсинг новостного сайта - https://www.yarnews.net/news/bymonth/2024/12/0/'||chr(10));

    http_req := http_io.begin_request('https://www.yarnews.net/news/bymonth/2024/12/0/', 'GET'); 
    http_io.set_header(http_req, 'Content-Type', 'html/text'); 
    http_resp := http_io.get_response(http_req); 
   
   	html_doc := http_resp.response_text;
  
   	for i in 1..html_io.get_node_count(html_doc) loop
   	  /*
	     dbms_output.put_line('level='||html_io.get_node_prop(html_doc, i, 'level')||                          
	     					  ', type='||html_io.get_node_prop(html_doc, i, 'type')||
	                          ', path='||html_io.get_node_prop(html_doc, i, 'path')||
	                          ', name='||html_io.get_node_prop(html_doc, i, 'name')||
							  ', val='||html_io.get_node_prop(html_doc, i, 'value')||	                          
	                          ', attrs='||html_io.get_node_all_attr(html_doc,i)
	                         );   	
   	  */
     if (html_io.get_node_prop(html_doc, i, 'level') = 12) and
        ((instr(html_io.get_node_prop(html_doc, i, 'path'),'/a') > 0) or
         (instr(html_io.get_node_prop(html_doc, i, 'path'),'/h3') > 0) or          
         (instr(html_io.get_node_prop(html_doc, i, 'path'),'/span') > 0)         
        ) 
        then
          if html_io.get_node_prop(html_doc, i, 'value') != '' then 
	        dbms_output.put_line(html_io.get_node_prop(html_doc, i, 'value'));
	      end if;
     end if;
   end loop;   
end;    
$$;

Выполним указанный код.

рис. 8
рис. 8

Итого:

  • Удалось успешно встроить интерпретатор в PostgreSQL. Это было нелегко для человека, не программирующего на Си. К тому же C-файл расширения для PostgreSQL должен быть написан по определенным канонам и поддерживать нужную структуру.

  • Интерпретатор показал себя хорошо. Его возможностей хватило для встраивания в PostgreSQL. Был получен новый язык программирования (pl_cpl_sql), на котором можно писать процедуры/функции внутри БД, в дальнейшем используя их в коде языков SQL и PLpgSQL. Язык содержит в себе функциональность для выполнения http/https-запросов (как в расширении http), доступ на уровне языка к другим базам данных (PostgreSQL и Oracle) (как в расширениях dblink, oracleFDW), позволяет производить разбор XML и HTML-документов.

Комментарии (6)


  1. rampler
    14.02.2025 12:01

    А какие преимущества по сравнению с существующим решением ?


    1. ank_75 Автор
      14.02.2025 12:01

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

      • Есть функционал для выполнения http/https-запросов.

      • Есть возможность создавать несколько соединений в одном коде.

      • При выполнении запросов в других БД (Oracle) сам запрос выполняется удаленно, а сюда перекачивается только его результат.

      • Есть возможность разбора xml/html-документов.


      1. plumqqz
        14.02.2025 12:01

        Возьмите питон. Там есть все это плюс прорва другого плюс сразу плюс штатно.


        1. ank_75 Автор
          14.02.2025 12:01

          Вы, несомненно, правы в отношении питона.

          Но и в языке pl_cpl_sql есть тоже свои преимущества.

          • Реализованная на уровне языка поддержка SQL-конструкций. В питоне для этого нужно использовать различные обертки.

          • И сам синтаксис языка, как мне кажется, проще питона. Нет этого обязательного форматирования текста.


  1. RekGRpth
    14.02.2025 12:01

    а есть исходники самого интерпретатора? (lib_date_util.so и lib_date_util.so)


  1. duke_alba
    14.02.2025 12:01

    Спасибо, что прошли весь путь! Была мысль добавить простенький язычок... Теперь вижу: как это сделать.