Skip to content

Support binary protocol for hstore #1030

Description

@Zaczero

When using the binary protocol, hstore columns are not correctly decoded. Related code. This results in type errors because the data is received as raw bytes rather than a Python dictionary. I went ahead and implemented missing binary loader/dumper classes:

_U32_STRUCT = Struct('!I')
_I2B = {i: i.to_bytes(4) for i in range(256)}

class HstoreBinaryLoader(RecursiveLoader):
    format = Format.BINARY
    _encoding: str

    def load(self, data: Buffer) -> dict[str, str | None]:
        if len(data) < 12:  # Fast-path if too small to contain any data.
            return {}

        unpack_from = _U32_STRUCT.unpack_from
        encoding = self._encoding
        result = {}

        view = bytes(data)
        size, = unpack_from(view)
        pos = 4

        for _ in range(size):
            key_size, = unpack_from(view, pos)
            pos += 4

            key = view[pos : pos + key_size].decode(encoding)
            pos += key_size

            value_size, = unpack_from(view, pos)
            pos += 4

            if value_size == 0xFFFFFFFF:
                value = None
            else:
                value = view[pos : pos + value_size].decode(encoding)
                pos += value_size

            result[key] = value

        return result


class HstoreBinaryDumper(RecursiveDumper):
    format = Format.BINARY
    _encoding: str

    def dump(self, obj: dict[str, str | None]) -> Buffer:
        if not obj:
            return b'\x00\x00\x00\x00'

        pack = _U32_STRUCT.pack
        i2b = _I2B
        encoding = self._encoding
        buffer: list[bytes] = [i2b.get(l := len(obj)) or pack(l)]

        for key, value in obj.items():
            key_bytes = key.encode(encoding)
            buffer.append(i2b.get(l := len(key_bytes)) or pack(l))
            buffer.append(key_bytes)

            if value is None:
                buffer.append(b'\xFF\xFF\xFF\xFF')
            else:
                value_bytes = value.encode(encoding)
                buffer.append(i2b.get(l := len(value_bytes)) or pack(l))
                buffer.append(value_bytes)

        return b''.join(buffer)

_encoding is the current connection encoding (from psycopg._encodings import conn_encoding).

Note

If you're planning to use any part or all of the suggested code, please add me to the commit's Co-authored-by list. I enjoy collecting contributions to open source projects 🙂.

PS.

I saw #1028 (comment) but I can't guarantee I'll be able to submit a PR soon. I may or may not be able to - it depends on my other work. So feel free to integrate it early (or not) if you have spare capacity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions