diff --git a/docs/stdlib/bytes.rst b/docs/stdlib/bytes.rst index 4256ab7eeaf..43f27bdb75c 100644 --- a/docs/stdlib/bytes.rst +++ b/docs/stdlib/bytes.rst @@ -41,6 +41,18 @@ Bytes * - :eql:func:`to_str` - :eql:func-desc:`to_str` + * - :eql:func:`to_int16` + - :eql:func-desc:`to_int16` + + * - :eql:func:`to_int32` + - :eql:func-desc:`to_int32` + + * - :eql:func:`to_int64` + - :eql:func-desc:`to_int64` + + * - :eql:func:`to_uuid` + - :eql:func-desc:`to_uuid` + * - :eql:func:`bytes_get_bit` - :eql:func-desc:`bytes_get_bit` @@ -136,19 +148,48 @@ Bytes --------- .. eql:function:: std::to_bytes(s: str) -> bytes + std::to_bytes(val: int16) -> bytes + std::to_bytes(val: int32) -> bytes + std::to_bytes(val: int64) -> bytes + std::to_bytes(val: int64) -> bytes + std::to_bytes(val: uuid) -> bytes :index: encode stringencoder .. versionadded:: 4.0 - Converts a :eql:type:`str` value to :eql:type:`bytes` using - UTF-8 encoding. + Converts a given value into binary representation as :eql:type:`bytes`. + + The strings get converted using UTF-8 encoding: .. code-block:: edgeql-repl db> select to_bytes('テキスト'); {b'\xe3\x83\x86\xe3\x82\xad\xe3\x82\xb9\xe3\x83\x88'} + The integer values are encoded as big-endian (most significant bit comes + first) byte strings: + + .. code-block:: edgeql-repl + + db> select to_bytes(31); + {b'\x00\x1f'} + db> select to_bytes(31); + {b'\x00\x00\x00\x1f'} + db> select to_bytes(123456789123456789); + {b'\x01\xb6\x9bK\xac\xd0_\x15'} + + The UUID values are converted to the underlying string of 16 bytes: + + .. code-block:: edgeql-repl + + db> select to_bytes('1d70c86e-cc92-11ee-b4c7-a7aa0a34e2ae'); + {b'\x1dp\xc8n\xcc\x92\x11\xee\xb4\xc7\xa7\xaa\n4\xe2\xae'} + + To perform the reverse conversion there are corresponding functions: + :eql:func:`to_str`, :eql:func:`to_int16`, :eql:func:`to_int32`, + :eql:func:`to_int64`, :eql:func:`to_uuid`. + --------- @@ -187,7 +228,7 @@ Bytes .. versionadded:: 4.0 Returns the :eql:type:`bytes` of a Base64-encoded :eql:type:`str`. - + Returns an InvalidValueError if input is not valid Base64. .. code-block:: edgeql-repl @@ -195,4 +236,4 @@ Bytes db> select enc::base64_decode('aGVsbG8='); {b'hello'} db> select enc::base64_decode('aGVsbG8'); - edgedb error: InvalidValueError: invalid base64 end sequence \ No newline at end of file + edgedb error: InvalidValueError: invalid base64 end sequence diff --git a/docs/stdlib/numbers.rst b/docs/stdlib/numbers.rst index 59d6af83bbf..ed28ac8bf8b 100644 --- a/docs/stdlib/numbers.rst +++ b/docs/stdlib/numbers.rst @@ -814,41 +814,90 @@ from :eql:type:`str` and :eql:type:`json`. .. eql:function:: std::to_int16(s: str, fmt: optional str={}) -> int16 + std::to_int16(val: bytes) -> int16 :index: parse int16 - Returns an :eql:type:`int16` value parsed from the given string. + Returns an :eql:type:`int16` value parsed from the given input. + + The string parsing function will use an optional format string passed as + *fmt*. See the :ref:`number formatting options + ` for help writing a format string. + + .. code-block:: edgeql-repl + + db> select to_int16('23'); + {23} + db> select to_int16('23%', '99%'); + {23} + + The bytes conversion function expects exactly 2 bytes using big-endian + representation. + + .. code-block:: edgeql-repl + + db> select to_int16(b'\x00\x07'); + {7} - The function will use an optional format string passed as *fmt*. See the - :ref:`number formatting options ` for help - writing a format string. ------------ .. eql:function:: std::to_int32(s: str, fmt: optional str={}) -> int32 + std::to_int32(val: bytes) -> int32 :index: parse int32 - Returns an :eql:type:`int32` value parsed from the given string. + Returns an :eql:type:`int32` value parsed from the given input. - The function will use an optional format string passed as *fmt*. See the - :ref:`number formatting options ` for help - writing a format string. + The string parsin function will use an optional format string passed as + *fmt*. See the :ref:`number formatting options + ` for help writing a format string. + + .. code-block:: edgeql-repl + + db> select to_int32('1000023'); + {1000023} + db> select to_int32('1000023%', '9999999%'); + {1000023} + + The bytes conversion function expects exactly 4 bytes using big-endian + representation. + + .. code-block:: edgeql-repl + + db> select to_int32(b'\x01\x02\x00\x07'); + {16908295} ------------ .. eql:function:: std::to_int64(s: str, fmt: optional str={}) -> int64 + std::to_int64(val: bytes) -> int64 :index: parse int64 - Returns an :eql:type:`int64` value parsed from the given string. + Returns an :eql:type:`int64` value parsed from the given input. - The function will use an optional format string passed as *fmt*. See the - :ref:`number formatting options ` for help - writing a format string. + The string parsing function will use an optional format string passed as + *fmt*. See the :ref:`number formatting options + ` for help writing a format string. + + .. code-block:: edgeql-repl + + db> select to_int64('10000234567'); + {10000234567} + db> select to_int64('10000234567%', '99999999999%'); + {10000234567} + + The bytes conversion function expects exactly 8 bytes using big-endian + representation. + + .. code-block:: edgeql-repl + + db> select to_int64(b'\x01\x02\x00\x07\x11\x22\x33\x44'); + {72620574343574340} ------------ diff --git a/docs/stdlib/uuid.rst b/docs/stdlib/uuid.rst index 41af3a56cd4..f83ece2dcbd 100644 --- a/docs/stdlib/uuid.rst +++ b/docs/stdlib/uuid.rst @@ -21,6 +21,9 @@ UUIDs * - :eql:func:`uuid_generate_v4` - :eql:func-desc:`uuid_generate_v4` + * - :eql:func:`to_uuid` + - :eql:func-desc:`to_uuid` + --------- @@ -87,3 +90,26 @@ UUIDs db> select uuid_generate_v4(); {92673afc-9c4f-42b3-8273-afe0053f0f48} + + +--------- + + +.. eql:function:: std::to_uuid(val: bytes) -> uuid + + :index: parse uuid + + Returns a :eql:type:`uuid` value parsed from 128-bit input. + + The :eql:type:`bytes` string has to be a valid 128-bit UUID + representation. + + .. code-block:: edgeql-repl + + db> select to_uuid( + ... b'\x92\x67\x3a\xfc\ + ... \x9c\x4f\ + ... \x42\xb3\ + ... \x82\x73\ + ... \xaf\xe0\x05\x3f\x0f\x48'); + {92673afc-9c4f-42b3-8273-afe0053f0f48} diff --git a/edb/buildmeta.py b/edb/buildmeta.py index fb1a8f0a617..19f3cf9d827 100644 --- a/edb/buildmeta.py +++ b/edb/buildmeta.py @@ -44,7 +44,7 @@ # Increment this whenever the database layout or stdlib changes. -EDGEDB_CATALOG_VERSION = 2024_02_16_00_00 +EDGEDB_CATALOG_VERSION = 2024_02_16_14_00 EDGEDB_MAJOR_VERSION = 5 diff --git a/edb/lib/std/70-converters.edgeql b/edb/lib/std/70-converters.edgeql index 3348d10ac7a..9477fca6fb7 100644 --- a/edb/lib/std/70-converters.edgeql +++ b/edb/lib/std/70-converters.edgeql @@ -270,6 +270,54 @@ std::to_bytes(s: std::str) -> std::bytes { }; +CREATE FUNCTION +std::to_bytes(val: std::int16) -> std::bytes +{ + CREATE ANNOTATION std::description := + 'Convert an int16 to big-endian binary format'; + SET volatility := 'Immutable'; + USING SQL $$ + SELECT int2send(val); + $$; +}; + + +CREATE FUNCTION +std::to_bytes(val: std::int32) -> std::bytes +{ + CREATE ANNOTATION std::description := + 'Convert an int32 to big-endian binary format'; + SET volatility := 'Immutable'; + USING SQL $$ + SELECT int4send(val); + $$; +}; + + +CREATE FUNCTION +std::to_bytes(val: std::int64) -> std::bytes +{ + CREATE ANNOTATION std::description := + 'Convert an int64 to big-endian binary format'; + SET volatility := 'Immutable'; + USING SQL $$ + SELECT int8send(val); + $$; +}; + + +CREATE FUNCTION +std::to_bytes(val: std::uuid) -> std::bytes +{ + CREATE ANNOTATION std::description := + 'Convert an UUID to binary format'; + SET volatility := 'Immutable'; + USING SQL $$ + SELECT uuid_send(val); + $$; +}; + + CREATE FUNCTION std::to_json(str: std::str) -> std::json { @@ -477,6 +525,29 @@ std::to_int64(s: std::str, fmt: OPTIONAL str={}) -> std::int64 }; +CREATE FUNCTION +std::to_int64(val: std::bytes) -> std::int64 +{ + CREATE ANNOTATION std::description := + 'Convert bytes into `int64` value using big-endian format.'; + SET volatility := 'Immutable'; + USING SQL $$ + SELECT + CASE WHEN (length(val) = 8) THEN + ('x' || right(val::bytea::text, 16))::bit(64)::bigint + ELSE + edgedb.raise( + 0::int8, + 'invalid_parameter_value', + msg => ( + 'to_int64(): the argument must be exactly 8 bytes long' + ) + ) + END + $$; +}; + + CREATE FUNCTION std::to_int32(s: std::str, fmt: OPTIONAL str={}) -> std::int32 { @@ -506,6 +577,29 @@ std::to_int32(s: std::str, fmt: OPTIONAL str={}) -> std::int32 }; +CREATE FUNCTION +std::to_int32(val: std::bytes) -> std::int32 +{ + CREATE ANNOTATION std::description := + 'Convert bytes into `int32` value using big-endian format.'; + SET volatility := 'Immutable'; + USING SQL $$ + SELECT + CASE WHEN (length(val) = 4) THEN + ('x' || right(val::bytea::text, 8))::bit(32)::int + ELSE + edgedb.raise( + 0::int4, + 'invalid_parameter_value', + msg => ( + 'to_int32(): the argument must be exactly 4 bytes long' + ) + ) + END + $$; +}; + + CREATE FUNCTION std::to_int16(s: std::str, fmt: OPTIONAL str={}) -> std::int16 { @@ -535,6 +629,29 @@ std::to_int16(s: std::str, fmt: OPTIONAL str={}) -> std::int16 }; +CREATE FUNCTION +std::to_int16(val: std::bytes) -> std::int16 +{ + CREATE ANNOTATION std::description := + 'Convert bytes into `int16` value using big-endian format.'; + SET volatility := 'Immutable'; + USING SQL $$ + SELECT + CASE WHEN (length(val) = 2) THEN + ('x' || right(val::bytea::text, 4))::bit(16)::int::smallint + ELSE + edgedb.raise( + 0::int2, + 'invalid_parameter_value', + msg => ( + 'to_int16(): the argument must be exactly 2 bytes long' + ) + ) + END + $$; +}; + + CREATE FUNCTION std::to_float64(s: std::str, fmt: OPTIONAL str={}) -> std::float64 { @@ -591,3 +708,26 @@ std::to_float32(s: std::str, fmt: OPTIONAL str={}) -> std::float32 ) $$; }; + + +CREATE FUNCTION +std::to_uuid(val: std::bytes) -> std::uuid +{ + CREATE ANNOTATION std::description := + 'Convert binary representation into UUID value.'; + SET volatility := 'Immutable'; + USING SQL $$ + SELECT + CASE WHEN (length(val) = 16) THEN + ENCODE(val, 'hex')::uuid + ELSE + edgedb.raise( + NULL::uuid, + 'invalid_parameter_value', + msg => ( + 'to_uuid(): the argument must be exactly 16 bytes long' + ) + ) + END + $$; +}; diff --git a/tests/test_edgeql_functions.py b/tests/test_edgeql_functions.py index 524592a4790..9e8afe778ac 100644 --- a/tests/test_edgeql_functions.py +++ b/tests/test_edgeql_functions.py @@ -22,6 +22,7 @@ import json import os.path import random +import uuid import edgedb @@ -3116,6 +3117,141 @@ async def test_edgeql_functions_string_bytes_conversion_error(self): ''', ) + async def test_edgeql_functions_int_bytes_conversion_01(self): + # Make sure we can convert the bytes to ints and back + twobytes = b'\x7f\x3a' + for numbytes in [2, 4, 8]: + raw = twobytes * (numbytes // 2) + typename = f'int{numbytes * 8}' + await self.assert_query_result( + f''' + WITH + val := <{typename}>$val, + bin := $bin, + SELECT ( + val = to_{typename}(bin), + bin = to_bytes(val), + ) + ''', + {(True, True)}, + variables={ + "val": int.from_bytes(raw, 'big'), + "bin": raw, + }, + ) + + async def test_edgeql_functions_int_bytes_conversion_02(self): + with self.assertRaisesRegex( + edgedb.InvalidValueError, + r'to_int16.*the argument must be exactly 2 bytes long', + ): + async with self.con.transaction(): + await self.con.execute( + r''' + SELECT to_int16(b'\x01') + ''', + ) + + with self.assertRaisesRegex( + edgedb.InvalidValueError, + r'to_int16.*the argument must be exactly 2 bytes long', + ): + async with self.con.transaction(): + await self.con.execute( + r''' + SELECT to_int16(to_bytes(123)) + ''', + ) + + async def test_edgeql_functions_int_bytes_conversion_03(self): + with self.assertRaisesRegex( + edgedb.InvalidValueError, + r'to_int32.*the argument must be exactly 4 bytes long', + ): + async with self.con.transaction(): + await self.con.execute( + r''' + SELECT to_int32(to_bytes(23)) + ''', + ) + + with self.assertRaisesRegex( + edgedb.InvalidValueError, + r'to_int32.*the argument must be exactly 4 bytes long', + ): + async with self.con.transaction(): + await self.con.execute( + r''' + SELECT to_int32(to_bytes(16908295)) + ''', + ) + + async def test_edgeql_functions_int_bytes_conversion_04(self): + with self.assertRaisesRegex( + edgedb.InvalidValueError, + r'to_int64.*the argument must be exactly 8 bytes long', + ): + async with self.con.transaction(): + await self.con.execute( + r''' + SELECT to_int64(to_bytes(23)) + ''', + ) + + with self.assertRaisesRegex( + edgedb.InvalidValueError, + r'to_int64.*the argument must be exactly 8 bytes long', + ): + async with self.con.transaction(): + await self.con.execute( + r''' + SELECT to_int64(b'\x00\x00' ++ to_bytes(62620574343574340)) + ''', + ) + + async def test_edgeql_functions_uuid_bytes_conversion_01(self): + uuid_val = uuid.uuid4() + + await self.assert_query_result( + r''' + WITH + uuid_input := $uuid_input, + bin_input := $bin_input, + SELECT ( + bin_input = to_bytes(uuid_input), + uuid_input = to_uuid(bin_input), + ) + ''', + {(True, True)}, + variables={ + "uuid_input": uuid_val, + "bin_input": uuid_val.bytes, + }, + ) + + async def test_edgeql_functions_uuid_bytes_conversion_02(self): + with self.assertRaisesRegex( + edgedb.InvalidValueError, + r'to_uuid.*the argument must be exactly 16 bytes long', + ): + async with self.con.transaction(): + await self.con.execute( + r''' + SELECT to_uuid(to_bytes(uuid_generate_v4())[:10]) + ''', + ) + + with self.assertRaisesRegex( + edgedb.InvalidValueError, + r'to_uuid.*the argument must be exactly 16 bytes long', + ): + async with self.con.transaction(): + await self.con.execute( + r''' + SELECT to_uuid(b'\xff\xff' ++ to_bytes(uuid_generate_v4())) + ''', + ) + async def test_edgeql_functions_array_join_01(self): await self.assert_query_result( r'''SELECT array_join(['one', 'two', 'three'], ', ');''',