Skip to content

Decode internal "char" type (OID 18) natively#168

Open
Dev-iL wants to merge 1 commit into
psqlpy-python:mainfrom
Dev-iL:2605/gchar_decode
Open

Decode internal "char" type (OID 18) natively#168
Dev-iL wants to merge 1 commit into
psqlpy-python:mainfrom
Dev-iL:2605/gchar_decode

Conversation

@Dev-iL
Copy link
Copy Markdown
Contributor

@Dev-iL Dev-iL commented May 14, 2026

Description

Adds a native decoder for PostgreSQL's internal "char" type (OID 18) and its array variant. A new InternalChar(u8) wrapper sits alongside the existing InternalUuid / InnerDecimal / InnerInterval helpers; two new arms in postgres_bytes_to_py (Type::CHAR, Type::CHAR_ARRAY) decode the single wire byte to a one-character Python str via char::from(u8) — Unicode code points 0..=255, Latin-1 round-trip, matching psycopg2/psycopg3.

custom_decoders keying is intentionally unchanged (still column-name-keyed, lowercase, per the existing documented contract).

Motivation and Context

Fixes #165.

Querying any column of the internal "char" type — distinct from character(n)/BPCHAR — raised RustToPyValueMappingError: Cannot convert _char into Python type, please look at the custom_decoders functionality. The type had no native decoder and fell through to other_postgres_bytes_to_py. The error message pointed at custom_decoders, but that path is keyed by column name, not type, so for users hitting this through pg_type / pg_class / pg_attribute / pg_proc (where "char" columns are pervasive — pg_type alone has five) it offered no workable escape.

How has this been tested?

  • cargo build --release
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo test --release internal_char — 2 passed (full 0..=255 byte round-trip + accepts() type guard rejects TEXT / VARCHAR / BPCHAR)
  • pytest python/tests/test_value_converter.py — 163 passed, including three new tests:
    • test_char_internal_type_pg_type_reproduction — the exact failing snippet from the issue
    • test_char_internal_type_byte_spectrum — round-trips bytes 0x20 / 0x41 / 0x61 / 0x7E plus NULL through a temp "char" column (SQL chr() rejects NUL and re-encodes >= 0x80 as multi-byte UTF-8 of which only the first byte is stored, so the full 0..=255 range is exercised by the Rust unit test instead)
    • test_char_internal_type_array — decodes ARRAY['a'::"char", 'b'::"char", 'c'::"char"] to ["a", "b", "c"]
  • Tautology check: with the Rust change stashed, the three new tests fail with the original RustToPyValueMappingError: Cannot convert _char into Python type error; with the fix in place, they pass.

Tested against PostgreSQL 14 (psqlpy's lowest supported major version) on Linux / Python 3.12.

Screenshots (if appropriate):

N/A.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.

Push the branch when you can, then I'll open the PR with this body.

@chandr-andr
Copy link
Copy Markdown
Member

@Dev-iL Could you please check lint stages?

@Dev-iL
Copy link
Copy Markdown
Contributor Author

Dev-iL commented May 14, 2026

Yeah, forgot to enable hooks locally after cloning. Will fix shortly...

Fixes psqlpy-python#165. Any query touching PostgreSQL system
catalogs (pg_type, pg_class, pg_attribute, pg_proc, ...) raised
RustToPyValueMappingError because the internal "char" type — distinct
from character(n)/BPCHAR — had no native decoder and fell through to
other_postgres_bytes_to_py.

Add an InternalChar(u8) wrapper next to the existing InternalUuid /
InnerDecimal / InnerInterval helpers and wire it into postgres_bytes_to_py
via two new match arms (Type::CHAR, Type::CHAR_ARRAY). The byte is read
through tokio-postgres' i8 FromSql impl, cast back to u8, and mapped to a
one-character Python str through char::from(u8) — i.e. Unicode code
points 0..=255 (Latin-1 round-trip), matching psycopg2/psycopg3.

The custom_decoders dispatch is intentionally unchanged: it stays keyed
by column name per the existing documented contract.

Tests:
- python/tests/test_value_converter.py:
  * test_char_internal_type_pg_type_reproduction — exact snippet from psqlpy-python#165
  * test_char_internal_type_byte_spectrum — reachable ASCII bytes 0x20,
    0x41, 0x61, 0x7E plus NULL (SQL chr() rejects NUL and re-encodes
    >=0x80 as multi-byte UTF-8)
  * test_char_internal_type_array — "char"[] decoded to list[str]
- src/value_converter/models/internal_char.rs:
  * from_sql_round_trips_full_byte_range — full 0..=255 byte mapping the
    SQL test cannot reach
  * accepts_only_char_type — type guard rejects TEXT/VARCHAR/BPCHAR
@Dev-iL Dev-iL force-pushed the 2605/gchar_decode branch from 0567cd9 to db83ad0 Compare May 14, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PostgreSQL "char" type (OID 18) cannot be decoded, and custom_decoders does not fix it

2 participants