bytea в postgres для хранения и извлечения байтов

Я пытаюсь понять, как работать с двоичными данными в postgresql (v 8.3). Скажем, у меня есть следующая таблица

Table "public.message"
Column  |  Type   | Modifiers 
---------+---------+-----------
id      | integer | 
message | bytea   | 

Я хотел бы сохранить пакет в поле сообщения в таком формате:

версия (1 байт), идентификатор (1 байт), эпоха (4 байта)

Я хотел бы упаковать эти данные в поле сообщения. Допустим, у меня есть версия = 1, идентификатор = 8 и эпоха = 123456. Как мне упаковать эти данные в поле сообщения? Как мне преобразовать мои целые значения в шестнадцатеричные или восьмеричные?

Мне также нужно вернуть сообщение и проанализировать его. Я смотрел на функцию get_byte, если нет другого способа проанализировать данные.

Спасибо!


person t0x13    schedule 03.04.2012    source источник
comment
Если вы можете использовать plperl, вам нужны pack() и unpack(). Но в встроенных функциях SQL PG нет эквивалента. . Или подумайте о том, чтобы сделать это на стороне клиента.   -  person Daniel Vérité    schedule 03.04.2012
comment
По какой-то особой причине вы хотите упаковать эти значения в столбец bytea? Почему не просто три числовых столбца? Если вы действительно хотите это сделать, вам лучше создать функцию на внешнем языке, таком как Python или Perl, как это было предложено @DanielVérité в его ответе.   -  person Clodoaldo Neto    schedule 03.04.2012
comment
Причина в том, что я имею дело с существующей архитектурой, которая имеет это поле сообщения как bytea, и это то, что я должен использовать. Меня действительно удивляет, что нет поддержки для этого на стр. Я знал о функции пакета, например, в python.   -  person t0x13    schedule 03.04.2012


Ответы (2)


Вот пример кода, показывающий, как это сделать с Perl на стороне сервера. Досадно, что пакет/распаковка считается ненадежными операциями PG, поэтому он должен быть создан с помощью plperlu суперпользователем, а затем доступ предоставлен с помощью GRANT EXECUTE для не суперпользователей.

С другой стороны, такой выбор языка упрощает работу с более сложными упакованными структурами, что является значительным преимуществом по сравнению с кодом, основанным на функциях SQL get_bytes()/set_bytes(). См. функции Perl pack().

1) первый шаг: определить составной тип SQL, представляющий неупакованную запись.

create type comp as (a smallint, b smallint, c int);

2) сделать функцию для упаковки значения записи в bytea:

create function pack_comp(comp) returns bytea
as $body$
 my $arg=shift;
 my $retval = pack("CCL", $arg->{a},$arg->{b},$arg->{c});
 # encode bytea according to PG doc. For PG>=9.0, use encode_bytea() instead
 $retval =~ s!(\\|[^ -~])!sprintf("\\%03o",ord($1))!ge; # from PG doc
 return $retval;
$body$ language plperlu;

3) сделать функцию распаковки bytea в составной тип:

create or replace function unpack_comp(bytea) returns comp
as $body$
 my $arg=shift;
 # decode bytea according to PG doc. For PG>=9.0, use decode_bytea() instead
 $arg =~ s!\\(?:\\|(\d{3}))!$1 ? chr(oct($1)) : "\\"!ge;
 my ($v,$i,$e)= unpack("CCL", $arg);
 return {"a"=>$v, "b"=>$i, "c"=>$e};
$body$ language plperlu;

4) использование:

# select encode(pack_comp((254,14,1000000)::comp), 'hex');
    encode    
--------------
 fe0e40420f00

# select unpack_comp(decode('fe0e40420f00','hex'));
   unpack_comp    
------------------
 (254,14,1000000)

 # select * from unpack_comp(decode('fe0e40420f00','hex'));
  a  | b  |    c    
-----+----+---------
 254 | 14 | 1000000
person Daniel Vérité    schedule 03.04.2012
comment
Спасибо, что нашли время написать это. Впечатляющий! Я просто не уверен, что это сработает для меня.. - person t0x13; 04.04.2012
comment
Я также пытаюсь понять, как работает вся бинарная упаковка. В вашем примере 40420f00 равно 1000000, однако, когда я использую онлайн-калькулятор, он показывает «f4240». Это связано с какой-то перестановкой байтов? - person t0x13; 04.04.2012
comment
Да, это с прямым порядком следования байтов, он родной на машинах x86. Big endian можно было получить с помощью шаблона CCL› для пакета вместо CCL - person Daniel Vérité; 04.04.2012

Так что я смог понять, как это сделать в plpg Вот код для упаковки

CREATE FUNCTION pack_numeric_bytes(i_values NUMERIC[], i_byte_sizes NUMERIC[],    i_big_endian BOOLEAN)
RETURNS BYTEA
    DECLARE
        v_bytes BYTEA := NULL;
        v_start INTEGER := 1;
        v_byte BYTEA;
        v_byte_size INTEGER;
        v_value NUMERIC;
        v_binary_value TEXT;
        v_num NUMERIC;
        i INTEGER;
        x INTEGER;
        v_sql TEXT;
    BEGIN
        IF array_upper(i_values, 1) != array_upper(i_byte_sizes, 1) THEN
            RETURN v_bytes;
        END IF;

        FOR x IN array_lower(i_values, 1) .. array_upper(i_values, 1) LOOP

            /* Given value and size at x position */
            v_byte_size := i_byte_sizes[x]::INTEGER;
            v_value := i_values[x];
            /* Convert number to binary form */
            v_sql := $$SELECT $$|| v_value ||$$::bit($$|| v_byte_size*8 ||$$);$$;
            EXECUTE v_sql INTO v_binary_value;
            IF i_big_endian IS TRUE THEN
                /* Convert each byte at a time */
                FOR i IN 1 .. v_byte_size LOOP
                    /* Extract byte from our binary value. 
                    Big endian starts at 1 and then increments of 8 */
                    v_byte := substring(v_binary_value, v_start, 8);
                    /* Convert binary 8 bits to an integer */
                    v_sql := $$SELECT B$$||quote_literal(v_byte)||$$::int8$$;
                    EXECUTE v_sql INTO v_num;
                    /* Build bytea of bytes */
                    v_bytes := COALESCE(v_bytes, '') || set_byte(E' '::BYTEA, 0, v_num::INTEGER);
                    v_start := v_start + 8;

                END LOOP;
            ELSE
                /* Small endian is extracted starting from last byte */
                v_start := (v_byte_size * 8) + 1;
                /* Convert each byte at a time */
                FOR i IN 1 .. v_byte_size LOOP
                    v_start := v_start - 8;
                    v_byte := substring(v_binary_value, v_start, 8);
                    /* Convert binary 8 bits to an integer */
                    v_sql := $$SELECT B$$||quote_literal(v_byte)||$$::int8$$;
                    EXECUTE v_sql INTO v_num;
                    /* Build bytea of bytes */
                    v_bytes := COALESCE(v_bytes, '') || set_byte(E' '::BYTEA, 0, v_num::INTEGER);
                END LOOP;

            END IF; /* END endian check */

            v_start := 1;

        END LOOP;
        RETURN v_bytes;
    END;

И вот код для распаковки:

CREATE OR REPLACE FUNCTION public.unpack_numeric_bytes(i_bytes bytea, i_byte_sizes INTEGER[], i_big_endian BOOLEAN)
RETURNS NUMERIC[]
SECURITY DEFINER AS
    DECLARE
        v_bytes BYTEA;
        v_start INTEGER := 1;
        v_byte_index INTEGER := 0;
        v_bit_shift INTEGER := 0;

        v_length INTEGER;
        v_size INTEGER;
        v_sum_byte_sizes INTEGER;

        v_vals NUMERIC[] := '{}';
        v_val BIGINT := 0;

        i INTEGER;
        x INTEGER;
        v_sql TEXT;
    BEGIN
       v_sql := $$SELECT $$|| array_to_string(i_byte_sizes, '+')||$$;$$;

        EXECUTE v_sql INTO v_sum_byte_sizes;

        IF length(i_bytes) != v_sum_byte_sizes::INTEGER THEN
            RETURN v_vals;
        END IF;

        /* Loop through values of bytea (split by their sizes) */
        FOR x IN array_lower(i_byte_sizes, 1) .. array_upper(i_byte_sizes, 1) LOOP

            v_size := i_byte_sizes[x];
            v_bytes := substring(i_bytes, v_start, v_size);
            v_length := length(v_bytes);

            IF i_big_endian IS TRUE THEN

                v_byte_index := v_length - 1;

                FOR i IN 1..v_length LOOP
                    v_val := v_val + (get_byte(v_bytes, v_byte_index) << v_bit_shift);
                    v_bit_shift := v_bit_shift + 8;
                    v_byte_index := v_byte_index - 1;
                END LOOP;
            ELSE

                FOR i IN 1..v_length LOOP
                    v_val := v_val + (get_byte(v_bytes, v_byte_index) << v_bit_shift);
                    v_bit_shift := v_bit_shift + 8;
                    v_byte_index := v_byte_index + 1;
                END LOOP;

            END IF;

            v_vals := array_append(v_vals, v_val::NUMERIC);
            /* Calculate next value start index */
            v_start := v_start + v_size;
            v_byte_index := 0;
            v_bit_shift := 0;
            v_val := 0;

        END LOOP;

        RETURN v_vals;
    END;

Я надеюсь, что это поможет кому-то.

person t0x13    schedule 17.04.2012
comment
Спасибо за это! можешь объяснить v_sql := $$SELECT B$$||quote_literal(v_byte)||$$::int8$$;? У меня возникла проблема с тем, чтобы заставить это работать из-за этой строки - person pyramation; 05.02.2020