Convert hex in text representation to decimal number - postgresql

I am trying to convert hex to decimal using PostgreSQL 9.1
with this query:
SELECT to_number('DEADBEEF', 'FMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
I get the following error:
ERROR: invalid input syntax for type numeric: " "
What am I doing wrong?

Ways without dynamic SQL
There is no cast from hex numbers in text representation to a numeric type, but we can use bit(n) as waypoint. There are undocumented casts from bit strings (bit(n)) to integer types (int2, int4, int8) - the internal representation is binary compatible. Quoting Tom Lane:
This is relying on some undocumented behavior of the bit-type input
converter, but I see no reason to expect that would break. A possibly
bigger issue is that it requires PG >= 8.3 since there wasn't a text
to bit cast before that.
integer for max. 8 hex digits
Up to 8 hex digits can be converted to bit(32) and then coerced to integer (standard 4-byte integer):
SELECT ('x' || lpad(hex, 8, '0'))::bit(32)::int AS int_val
FROM (
VALUES
('1'::text)
, ('f')
, ('100')
, ('7fffffff')
, ('80000000') -- overflow into negative number
, ('deadbeef')
, ('ffffffff')
, ('ffffffff123') -- too long
) AS t(hex);
int_val
------------
1
15
256
2147483647
-2147483648
-559038737
-1
Postgres uses a signed integer type, so hex numbers above '7fffffff' overflow into negative integer numbers. This is still a valid, unique representation but the meaning is different. If that matters, switch to bigint; see below.
For more than 8 hex digits the least significant characters (excess to the right) get truncated.
4 bits in a bit string encode 1 hex digit. Hex numbers of known length can be cast to the respective bit(n) directly. Alternatively, pad hex numbers of unknown length with leading zeros (0) as demonstrated and cast to bit(32). Example with 7 hex digits and int or 8 digits and bigint:
SELECT ('x'|| 'deafbee')::bit(28)::int
, ('x'|| 'deadbeef')::bit(32)::bigint;
int4 | int8
-----------+------------
233503726 | 3735928559
bigint for max. 16 hex digits
Up to 16 hex digits can be converted to bit(64) and then coerced to bigint (int8, 8-byte integer) - overflowing into negative numbers in the upper half again:
SELECT ('x' || lpad(hex, 16, '0'))::bit(64)::bigint AS int8_val
FROM (
VALUES
('ff'::text)
, ('7fffffff')
, ('80000000')
, ('deadbeef')
, ('7fffffffffffffff')
, ('8000000000000000') -- overflow into negative number
, ('ffffffffffffffff')
, ('ffffffffffffffff123') -- too long
) t(hex);
int8_val
---------------------
255
2147483647
2147483648
3735928559
9223372036854775807
-9223372036854775808
-1
-1
uuid for max. 32 hex digits
The Postgres uuid data type is not a numeric type. But it's the most efficient type in standard Postgres to store up to 32 hex digits, only occupying 16 bytes of storage. There is a direct cast from text to uuid (no need for bit(n) as waypoint), but exactly 32 hex digits are required.
SELECT lpad(hex, 32, '0')::uuid AS uuid_val
FROM (
VALUES ('ff'::text)
, ('deadbeef')
, ('ffffffffffffffff')
, ('ffffffffffffffffffffffffffffffff')
, ('ffffffffffffffffffffffffffffffff123') -- too long
) t(hex);
uuid_val
--------------------------------------
00000000-0000-0000-0000-0000000000ff
00000000-0000-0000-0000-0000deadbeef
00000000-0000-0000-ffff-ffffffffffff
ffffffff-ffff-ffff-ffff-ffffffffffff
ffffffff-ffff-ffff-ffff-ffffffffffff
As you can see, standard output is a string of hex digits with typical separators for UUID.
md5 hash
This is particularly useful to store md5 hashes:
SELECT md5('Store hash for long string, maybe for index?')::uuid AS md5_hash;
md5_hash
--------------------------------------
02e10e94-e895-616e-8e23-bb7f8025da42
See:
What is the optimal data type for an MD5 field?

You have two immediate problems:
to_number doesn't understand hexadecimal.
X doesn't have any meaning in a to_number format string and anything without a meaning apparently means "skip a character".
I don't have an authoritative justification for (2), just empirical evidence:
=> SELECT to_number('123', 'X999');
to_number
-----------
23
(1 row)
=> SELECT to_number('123', 'XX999');
to_number
-----------
3
The documentation mentions how double quoted patterns are supposed to behave:
In to_date, to_number, and to_timestamp, double-quoted strings skip the number of input characters contained in the string, e.g. "XX" skips two input characters.
but the behavior of non-quoted characters that are not formatting characters appears to be unspecified.
In any case, to_number isn't the right tool for converting hex to numbers, you want to say something like this:
select x'deadbeef'::int;
so perhaps this function will work better for you:
CREATE OR REPLACE FUNCTION hex_to_int(hexval varchar) RETURNS integer AS $$
DECLARE
result int;
BEGIN
EXECUTE 'SELECT x' || quote_literal(hexval) || '::int' INTO result;
RETURN result;
END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;
Then:
=> select hex_to_int('DEADBEEF');
hex_to_int
------------
-559038737 **
(1 row)
** To avoid negative numbers like this from integer overflow error, use bigint instead of int to accommodate larger hex numbers (like IP addresses).

pg-bignum
Internally, pg-bignum uses the SSL library for big numbers. This method has none of the drawbacks mentioned in the other answers with numeric. Nor is it slowed down by plpgsql. It's fast and it works with a number of any size. Test case taken from Erwin's answer for comparison,
CREATE EXTENSION bignum;
SELECT hex, bn_in_hex(hex::cstring)
FROM (
VALUES ('ff'::text)
, ('7fffffff')
, ('80000000')
, ('deadbeef')
, ('7fffffffffffffff')
, ('8000000000000000')
, ('ffffffffffffffff')
, ('ffffffffffffffff123')
) t(hex);
hex | bn_in_hex
---------------------+-------------------------
ff | 255
7fffffff | 2147483647
80000000 | 2147483648
deadbeef | 3735928559
7fffffffffffffff | 9223372036854775807
8000000000000000 | 9223372036854775808
ffffffffffffffff | 18446744073709551615
ffffffffffffffff123 | 75557863725914323415331
(8 rows)
You can get the type to numeric using bn_in_hex('deadbeef')::text::numeric.

If anybody else is stuck with PG8.2, here is another way to do it.
bigint version:
create or replace function hex_to_bigint(hexval text) returns bigint as $$
select
(get_byte(x,0)::int8<<(7*8)) |
(get_byte(x,1)::int8<<(6*8)) |
(get_byte(x,2)::int8<<(5*8)) |
(get_byte(x,3)::int8<<(4*8)) |
(get_byte(x,4)::int8<<(3*8)) |
(get_byte(x,5)::int8<<(2*8)) |
(get_byte(x,6)::int8<<(1*8)) |
(get_byte(x,7)::int8)
from (
select decode(lpad($1, 16, '0'), 'hex') as x
) as a;
$$
language sql strict immutable;
int version:
create or replace function hex_to_int(hexval text) returns int as $$
select
(get_byte(x,0)::int<<(3*8)) |
(get_byte(x,1)::int<<(2*8)) |
(get_byte(x,2)::int<<(1*8)) |
(get_byte(x,3)::int)
from (
select decode(lpad($1, 8, '0'), 'hex') as x
) as a;
$$
language sql strict immutable;

Here is a version which uses numeric, so it can handle arbitrarily large hex strings:
create function hex_to_decimal(hex_string text)
returns text
language plpgsql immutable as $pgsql$
declare
bits bit varying;
result numeric := 0;
exponent numeric := 0;
chunk_size integer := 31;
start integer;
begin
execute 'SELECT x' || quote_literal(hex_string) INTO bits;
while length(bits) > 0 loop
start := greatest(1, length(bits) - chunk_size);
result := result + (substring(bits from start for chunk_size)::bigint)::numeric * pow(2::numeric, exponent);
exponent := exponent + chunk_size;
bits := substring(bits from 1 for greatest(0, length(bits) - chunk_size));
end loop;
return trunc(result, 0);
end
$pgsql$;
For example:
=# select hex_to_decimal('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
32592575621351777380295131014550050576823494298654980010178247189670100796213387298934358015

Here is a poper way to convert hex to string... then you can check whether it's a numric type or not
SELECT convert_from('\x7468697320697320612076657279206C6F6E672068657820737472696E67','utf8')
returns
this is a very long hex string

Here is a other version which uses numeric, so it can handle arbitrarily large hex strings:
create OR REPLACE function hex_to_decimal2(hex_string text)
returns text
language plpgsql immutable as $pgsql$
declare
bits bit varying;
result numeric := 0;
begin
execute 'SELECT x' || quote_literal(hex_string) INTO bits;
while length(bits) > 0 loop
result := result + (substring(bits from 1 for 1)::bigint)::numeric * pow(2::numeric, length(bits) - 1);
bits := substring(bits from 2 for length(bits) - 1);
end loop;
return trunc(result, 0);
end
$pgsql$;
For example:
=# select hex_to_decimal('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
32592575621351777380295131014550050576823494298654980010178247189670100796213387298934358015
For example:
=# select hex_to_decimal('5f68e8131ecf80000');
110000000000000000000

Here is another implementation:
CREATE OR REPLACE FUNCTION hex_to_decimal3(hex_string text)
RETURNS numeric
LANGUAGE plpgsql
IMMUTABLE
AS $function$
declare
hex_string_lower text := lower(hex_string);
i int;
digit int;
s numeric := 0;
begin
for i in 1 .. length(hex_string) loop
digit := position(substr(hex_string_lower, i, 1) in '0123456789abcdef') - 1;
if digit < 0 then
raise '"%" is not a valid hexadecimal digit', substr(hex_string_lower, i, 1) using errcode = '22P02';
end if;
s := s * 16 + digit;
end loop;
return s;
end
$function$;
It is a straightforward one that works digit by digit, using the position() function to compute the numeric value of each character in the input string. Its benefit over hex_to_decimal2() is that it seems to be much faster (4x or so for md5()-generated hex strings).

CREATE OR REPLACE FUNCTION numeric_from_bytes(bytea)
RETURNS numeric
LANGUAGE plpgsql
AS $$
declare
bits bit varying;
result numeric := 0;
exponent numeric := 0;
bit_pos integer;
begin
execute 'SELECT x' || quote_literal(substr($1::text,3)) into bits;
bit_pos := length(bits) + 1;
exponent := 0;
while bit_pos >= 56 loop
bit_pos := bit_pos - 56;
result := result + substring(bits from bit_pos for 56)::bigint::numeric * pow(2::numeric, exponent);
exponent := exponent + 56;
end loop;
while bit_pos >= 8 loop
bit_pos := bit_pos - 8;
result := result + substring(bits from bit_pos for 8)::bigint::numeric * pow(2::numeric, exponent);
exponent := exponent + 8;
end loop;
return trunc(result);
end;
$$;
In a future PostgreSQL version, when/if Dean Rasheed's patch 0001-Add-non-decimal-integer-support-to-type-numeric.patch gets committed, this can be simplified:
CREATE OR REPLACE FUNCTION numeric_from_bytes(bytea)
RETURNS numeric
LANGUAGE sql
AS $$
SELECT ('0'||right($1::text,-1))::numeric
$$;

Related

Limit the output range of Skip32

Skip32 encrypts or decrypts a single int4 (32 bits) value with a 10 bytes (80 bits) key of bytea type.
Is it possible to limit the output to 100,000,000 - 999,999,999 so we always get 9 digits positive integers?
There is pseudo encrypt constrained to an arbitrary range example in the wiki but it doesn't work with a key and only support setting the upper limit.
Do I need to modify the following lines in the original implementation?
...
wl := (val & -65536) >> 16;
wr := val & 65535;
...
RETURN (wr << 16) | (wl & 65535);
...
and wrap in a loop that ensures the result is between the min,max range?
CREATE FUNCTION bounded_skip32(VALUE int4, CR_KEY bytea, ENCRYPT bool, MIN int, MAX int) returns int AS $$
BEGIN
LOOP
VALUE := skip32(VALUE, CR_KEY, ENCRYPT);
EXIT WHEN VALUE <= MAX AND VALUE >= MIN;
END LOOP;
RETURN VALUE;
END
$$ LANGUAGE plpgsql strict immutable;

How to generate a random 6-digit integer that's cryptographically strong?

We want a function that given an argument that is three bytes of type bytea (generated by the function gen_random_bytes of the pgcrypto extension), the function returns a random 6-digit integer (between 0 & 999999 inclusive). The 6-digit integer should preserve the randomness given by the argument passed to the function.
I might be overcomplicating, but I need to make sure that I have exactly n numeric digits in the string. Repetition of numbers is allowed.
SELECT string_agg(shuffle('0123456789')::char, '')
FROM generate_series(1, 6);
With the shuffle function provided in another answer that I copied here for convenience
create or replace function shuffle(text)
returns text language sql as $$
select string_agg(ch, '')
from (
select substr($1, i, 1) ch
from generate_series(1, length($1)) i
order by random()
) s
$$;
In case of 3 bytes, take 6 last chars, prepend 'x', convert to bitstring and then to int:
select ('x' || right(gen_random_bytes(3)::text, 6))::bit(24)::int;
More details are in similar question: What's the easiest way to represent a bytea as a single integer in PostgreSQL?

Syntax for passing values to a PL/pgSQL function

I'm new to writing triggers and functions in Postgres.
I have written a function that changes prices to .99 or .00 whenever a price is put into the database.
CREATE OR REPLACE FUNCTION public.cents(price numeric)
RETURNS numeric
LANGUAGE plpgsql
LEAKPROOF
AS $function$
DECLARE
dollars text;
cents text;
new_price numeric;
BEGIN
dollars := split_part(price::text, '.', 1);
cents := split_part(price::text, '.', 2);
IF cents != '00' THEN cents := '99';
ENDIF;
new_price := dollars || '.' || cents;
RETURN new_price;
END;
$function$
I've read the doc on triggers and these examples seem more complex.
I'm not sure I understand how to create a trigger that will run this function specifically whenever a record in the price column is updated.
Does this look correct? The price column isn't mentioned in the trigger..
CREATE OR REPLACE FUNCTION public.cents()
RETURNS trigger
LANGUAGE plpgsql
LEAKPROOF
AS $tr_cents$
DECLARE
dollars text;
cents text;
new_price numeric;
BEGIN
dollars := split_part(OLD::text, '.', 1);
cents := split_part(OLD::text, '.', 2);
IF cents != '00' THEN cents := '99';
ENDIF;
new_price := dollars || cents;
RETURN new_price;
END;
$tr_cents$;
CREATE TRIGGER tr_cents BEFORE INSERT OR UPDATE ON microwaves
FOR EACH ROW EXECUTE PROCEDURE cents();
The examples in the docs also use RETURN NEW but I'm not exactly sure how that would work with my code, or if it's necessary?
Assuming missing information:
price is defined numeric NOT NULL.
There is a CHECK constraint enforcing positive prices.
Postgres 9.5. (Solution should work for Postgres 9.0+.)
I read your objective like this:
Leave numbers without (significant) fractional digits (.00) and change all others to .99.
See below about "without (significant) fractional digits" or .00 ...
If that's all the trigger does, the most efficient way is to place the condition in a WHEN clause to the trigger itself. The manual:
In a BEFORE trigger, the WHEN condition is evaluated just before the
function is or would be executed, so using WHEN is not materially
different from testing the same condition at the beginning of the trigger function.
(There is more, read the manual.)
This way, the trigger function is not even called if not needed. The logic can be radically simplified:
CREATE OR REPLACE FUNCTION tr_cents()
RETURNS trigger AS
$tr_cents$
BEGIN
-- only called WHEN (NEW.price % 1 IN (.00, .99)
NEW.price := trunc(NEW.price) + .99;
RETURN NEW;
END
$tr_cents$ LANGUAGE plpgsql LEAKPROOF;
CREATE TRIGGER microwaves_cents
BEFORE INSERT OR UPDATE ON microwaves
FOR EACH ROW
WHEN ((NEW.price % 1) <> ALL ('{.00,.99}'::numeric[]))
EXECUTE PROCEDURE tr_cents();
Note that the trigger kicks in for INSERT and UPDATE with illegal price values. Not just
whenever a record in the price column is updated.
You need RETURN NEW; at the end of the trigger function or the operation on the row will be cancelled. The manual:
A trigger function must return either NULL or a record/row value having exactly the structure of the table the trigger was fired for.
You don't need the function public.cents() at all for this.
Test case
CREATE TABLE microwaves (m_id serial PRIMARY KEY, price numeric);
INSERT INTO microwaves (m_id, price) VALUES
(1, 0.00)
, (2, 0.01)
, (3, 0.02)
, (4, 0.99)
, (5, 1.00)
, (6, 1.01)
, (7, 1.02)
, (8, 1.99)
, (9, 12.34);
UPDATE microwaves SET price = 2.0 WHERE m_id = 7;
UPDATE microwaves SET price = 2.5 WHERE m_id = 8;
UPDATE microwaves SET price = 5.99 WHERE m_id = 9;
TABLE microwaves;
Result:
m_id | price
------+-------
1 | 0.00
2 | 0.99
3 | 0.99
4 | 0.99
5 | 1.00
6 | 1.99
7 | 2.0
8 | 2.99
9 | 5.99
Data type numeric and scale
.. and why your function public.cents(price numeric) is a trap.
Scale being the number of decimal fractional digits.
numeric is an arbitrary precision type. It preserves literal digits exactly as entered - unless you specify precision and scale for the type. Like: numeric(10,2). The manual:
Specifying:
NUMERIC
without any precision or scale creates a column in which numeric
values of any precision and scale can be stored, up to the
implementation limit on precision. A column of this kind (numeric
without precision and scale) will not coerce input values to any
particular scale, whereas numeric columns with a declared scale will
coerce input values to that scale.
Leading zeroes are never stored, but trailing zeroes in the fractional part are kept this way, even if insignificant. The manual can easily be misread in this respect, further down:
Numeric values are physically stored without any extra leading or trailing zeroes.
Note the word "extra". Meaning, Postgres will not add trailing zeros, but it will keep the ones you added - even if those are completely insignificant for the numeric value.
You need to be aware of this when converting numeric to text. A check for "00" in the fractional part will work for numeric with a configured scale like numeric (9,2). But it is unreliable for plain numeric like you use in your function. Consider:
SELECT (numeric(9,2) '1')::numeric AS num_cast_from_num_with_scale
, numeric '1.00' AS num_with_scale
, numeric '1' AS num_without_scale;
num_cast_from_num_with_scale | num_with_scale | num_without_scale
------------------------------+----------------+-------------------
1.00 | 1.00 | 1
This way, insignificant trailing zeros become significant. And I seriously doubt that's how it's supposed to be. The test IF cents != '00' ... in your function public.cents(price numeric) is a loaded footgun. It may work as expected while you pass values from a numeric(9,2) column, but "suddenly" break once you use values from other sources.
You described return value as numeric, but return a string by fact. Also several type conversions not a good point. There is more easy way. F. ex.
CREATE OR REPLACE FUNCTION cents(price numeric) RETURNS numeric AS
$BODY$
begin
IF price IS NOT NULL then
IF price % 1 != 0 then
price := floor(price) + 0.99;
end IF;
END IF;
RETURN price;
end;
$BODY$ LANGUAGE plpgsql;
To execute such update on any insert/update need:
CREATE TABLE test (
price numeric
);
CREATE FUNCTION price_update() RETURNS trigger AS $price_update$
BEGIN
IF NEW.price IS NOT NULL THEN
NEW.price = public.cents(NEW.price);
END IF;
RETURN NEW;
END;
$price_update$ LANGUAGE plpgsql;
CREATE TRIGGER on_price_update BEFORE INSERT OR UPDATE ON test
FOR EACH ROW EXECUTE PROCEDURE price_update();
Lets check:
=# insert into test (price) values (2), (1.1), (5);
INSERT 0 3
=# select * from test;
price
-------
2
1.99
5
(3 rows)
=# update test set price = 5.01 where price = 5;
UPDATE 1
=# select * from test;
price
-------
2
1.99
5.99
(3 rows)
=# update test set price = 3 where price = 1.99;
UPDATE 1
=# select * from test;
price
-------
2
5.99
3
(3 rows)

Convert value from string representation in base N to numeric

On my database i keep a numbers in specific notation as varchar values. Is there any way to typecast this values into decimals with selected notation?
What i basically looking here should look like this:
SELECT to_integer_with_notation('d', '20')
int4
---------
13
One more example:
SELECT to_integer_with_notation('d3', '23')
int4
---------
302
Unfortunately, there is no built-in function for that in PostgreSQL, but can be written fairly easy:
CREATE OR REPLACE FUNCTION number_from_base(num TEXT, base INTEGER)
RETURNS NUMERIC
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT sum(exp * cn)
FROM (
SELECT base::NUMERIC ^ (row_number() OVER () - 1) exp,
CASE
WHEN ch BETWEEN '0' AND '9' THEN ascii(ch) - ascii('0')
WHEN ch BETWEEN 'a' AND 'z' THEN 10 + ascii(ch) - ascii('a')
END cn
FROM regexp_split_to_table(reverse(lower(num)), '') ch(ch)
) sub
$function$;
Note: I used numeric as a return type, as int4 is not enough in many cases (with longer string input).
Edit: Here is a sample reverse function, which can convert a bigint to its text representation within a custom base:
CREATE OR REPLACE FUNCTION number_to_base(num BIGINT, base INTEGER)
RETURNS TEXT
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
WITH RECURSIVE n(i, n, r) AS (
SELECT -1, num, 0
UNION ALL
SELECT i + 1, n / base, (n % base)::INT
FROM n
WHERE n > 0
)
SELECT string_agg(ch, '')
FROM (
SELECT CASE
WHEN r BETWEEN 0 AND 9 THEN r::TEXT
WHEN r BETWEEN 10 AND 35 THEN chr(ascii('a') + r - 10)
ELSE '%'
END ch
FROM n
WHERE i >= 0
ORDER BY i DESC
) ch
$function$;
Example usage:
SELECT number_to_base(1248, 36);
-- +----------------+
-- | number_to_base |
-- +----------------+
-- | yo |
-- +----------------+

Convert bigint to bytea, but swap the byte order

I have a PostgreSQL table that I want to alter a column from bigint to bytea byte to hold more data. I am thinking using the following sequence:
alter table mytable add new_column
update mytable set new_column = int8send(old_column)
alter table drop old_column
alter table rename new_column to old_column
The above sequence works, the only problem is that I want the byte sequence in the bytea to be reversed. For example, if a value in old_column is 0x1234567890abcdef, the above sequence would generate \0224Vx\220\253\315\357, but I want it to be
\357\315\253\220xV4\022. Seems like the resulting bytea uses the big-endian order from originating bigint.
Is there an easy way to do that without writing a program? I was looking for a swap64() sort of function in PostgreSQL but failed to find one.
Here's a pure-SQL function I wrote to reverse the byte-order of a bytea-type value:
CREATE OR REPLACE FUNCTION reverse_bytes_iter(bytes bytea, length int, midpoint int, index int)
RETURNS bytea AS
$$
SELECT CASE WHEN index >= midpoint THEN bytes ELSE
reverse_bytes_iter(
set_byte(
set_byte(bytes, index, get_byte(bytes, length-index)),
length-index, get_byte(bytes, index)
),
length, midpoint, index + 1
)
END;
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION reverse_bytes(bytes bytea) RETURNS bytea AS
'SELECT reverse_bytes_iter(bytes, octet_length(bytes)-1, octet_length(bytes)/2, 0)'
LANGUAGE SQL IMMUTABLE;
I just wrote it yesterday, so it's not particularly well-tested nor optimized, but it seems to work, at least on byte strings up to 1k in length.
It is possible to byte-swap without plpgsql code using regexp extractions on the hexadecimal representation.
Here's an example to swap a bigint constant, assuming SET standard_conforming_strings to ON (the default with PG 9.1)
select regexp_replace( lpad(to_hex(x'123456789abcd'::bigint),16,'0'),
'(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)',
'\8\7\6\5\4\3\2\1');
It returns cdab896745230100. Then apply decode(value, 'hex') to convert that to a bytea.
The whole type conversion could actually be done in a single SQL statement:
ALTER TABLE mytable ALTER COLUMN old_column TYPE bytea
USING decode(
regexp_replace( lpad(to_hex(old_column), 16,'0'),
'(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)',
'\8\7\6\5\4\3\2\1')
, 'hex');
I am playing with pageinspect module now, and I was also curios how to change byte order of an existing bytea value, which pretty much matches your case.
I've come up with the following function:
CREATE OR REPLACE FUNCTION reverse(bytea) RETURNS bytea AS $reverse$
SELECT string_agg(byte,''::bytea)
FROM (
SELECT substr($1,i,1) byte
FROM generate_series(length($1),1,-1) i) s
$reverse$ LANGUAGE sql;
It's pretty straightforward and works similar to textual reverse() function:
WITH v(val) AS (
VALUES ('\xaabbccdd'::bytea),('\x0123456789abcd'::bytea)
)
SELECT val, reverse(val)
FROM v;
This function, while not exactly what you're looking for, should help you get on your way.
The source code from that function is reproduced verbatim below.
CREATE OR REPLACE FUNCTION utils.int_littleendian(v_number integer)
RETURNS bytea AS
$BODY$
DECLARE
v_textresult bytea;
v_temp int;
v_int int;
v_i int = 0;
BEGIN
v_int = v_number;
v_textresult = '1234';
WHILE(v_i < 4) LOOP
raise notice 'loop %',v_int;
v_temp := v_int%256;
v_int := v_int - v_temp;
v_int := v_int / 256;
SELECT set_byte(v_textresult,v_i,v_temp) INTO v_textresult;
v_i := v_i + 1;
END LOOP;
return v_textresult;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE
COST 100;