PostgreSQL's char and name Implementation in C
PostgreSQL's internal "char" is a single signed byte passed by value, while "name" is a 64-byte null-padded struct passed by reference — and both live in the system catalogs.
When you type char in SQL, you almost never get the type named "char". The
SQL standard’s blank-padded CHAR(n) is a varlena called bpchar. The type
actually named "char" is an internal 1-byte value that PostgreSQL uses for
system bookkeeping — and it is the element type of name, the 64-byte type
that holds every identifier in the catalogs.
The two are as different as two types can be. char is one byte, passed
by value inside a Datum. name is a fixed 64-byte struct passed by
reference. Neither has anything to do with the other except that one is
literally the other’s element type. This is a walk through both, based on the
PostgreSQL source (version 20devel, commit e75fcc9867f).
The catalog entries
Both types are declared in src/include/catalog/pg_type.dat:
{ oid => '18', array_type_oid => '1002', descr => 'single character',
typname => 'char', typlen => '1', typbyval => 't', typcategory => 'Z',
typinput => 'charin', typoutput => 'charout', typreceive => 'charrecv',
typsend => 'charsend', typalign => 'c' },
{ oid => '19', array_type_oid => '1003',
descr => '63-byte type for storing system identifiers',
typname => 'name', typlen => 'NAMEDATALEN', typbyval => 'f',
typcategory => 'S', typsubscript => 'raw_array_subscript_handler',
typelem => 'char', typinput => 'namein', typoutput => 'nameout',
typreceive => 'namerecv', typsend => 'namesend', typalign => 'c',
typcollation => 'C' },Read the flags carefully, because they tell the whole story:
| Type | typlen | typbyval | typalign | typstorage | typcategory |
|---|---|---|---|---|---|
char | 1 | true | c | (plain) | Z internal |
name | NAMEDATALEN (64) | false | c | (plain) | S string |
char is category Z — TYPCATEGORY_INTERNAL in src/include/catalog/pg_type.h.
It is not meant to be a user-facing type. name is a category S string type
with the C collation, and it names itself: typelem => 'char' means a name
is logically an array of "char"s, and name[] (oid 1003) is an array of
them.
name does not set typstorage, so it defaults to TYPSTORAGE_PLAIN — a
name is never toasted. It is exactly 64 bytes and always inline.
char: a signed byte in a Datum
There is no char struct. char is a Datum, which on a 64-bit build is an
unsigned long. The accessors are two inline functions in
src/include/postgres.h:
static inline char
DatumGetChar(Datum X)
{
return (char) X;
}
static inline Datum
CharGetDatum(char X)
{
return (Datum) X;
}Because typlen == 1, the value fits in the low byte of the Datum; the fmgr
macros in src/include/fmgr.h are just PG_GETARG_CHAR(n) DatumGetChar(...) and
PG_RETURN_CHAR(x) return CharGetDatum(x).
The I/O functions in src/backend/utils/adt/char.c are the interesting part.
charin accepts the octal escape that charout produces:
Datum
charin(PG_FUNCTION_ARGS)
{
char *ch = PG_GETARG_CSTRING(0);
if (strlen(ch) == 4 && ch[0] == '\\' &&
ISOCTAL(ch[1]) && ISOCTAL(ch[2]) && ISOCTAL(ch[3]))
PG_RETURN_CHAR((FROMOCTAL(ch[1]) << 6) +
(FROMOCTAL(ch[2]) << 3) +
FROMOCTAL(ch[3]));
/* This will do the right thing for a zero-length input string */
PG_RETURN_CHAR(ch[0]);
}So '\377' is decoded as one byte 0xFF, but any other input simply takes its
first byte and silently discards the rest. charout mirrors it: 0x00 becomes
an empty string, 0x01–0x7F become the raw ASCII byte, and 0x80–0xFF
become \ooo. The comment says this is deliberately “meant to match the
traditional escape format of bytea.” It is a one-byte string type, not a text
type — there is no encoding, and charrecv/charsend are a bare
pq_getmsgbyte/pq_sendbyte.
The unsigned/signed split
char.c has a comment worth reading twice:
/*
* NOTE: comparisons are done as though char is unsigned (uint8).
* Conversions to and from integer are done as though char is signed (int8).
*
* You wanted consistency?
*/The comparison functions cast to uint8 — charlt returns
(uint8) arg1 < (uint8) arg2 — while chartoi4 casts to int8 and i4tochar
range-checks against SCHAR_MIN/SCHAR_MAX. So "char" '200' = "char" '200'
orders bytes as 0–255, but 200::"char"::int is a negative number. Do not
build invariants on top of this.
name: a fixed 64-byte struct
The C representation is in src/include/c.h:
/*
* Representation of a Name: effectively just a C string, but null-padded to
* exactly NAMEDATALEN bytes. The use of a struct is historical.
*/
typedef struct nameData
{
char data[NAMEDATALEN];
} NameData;
typedef NameData *Name;
#define NameStr(name) ((name).data)NAMEDATALEN is 64, defined in src/include/pg_config_manual.h, with a comment
that “changing this requires an initdb.” It is also baked into the loadable
module ABI: Pg_abi_values in src/include/fmgr.h contains int namedatalen;
so a third-party extension compiled against a different NAMEDATALEN is
rejected at load time. name is the only built-in type whose size is a
compile-time constant that everyone must agree on.
Name is a pointer to a NameData, so PG_GETARG_NAME/PG_RETURN_NAME go
through DatumGetName/NameGetDatum, which just reinterpret the Datum as a
pointer. The accessor header is explicit that the caller owns the lifetime:
/*
* NameGetDatum
* Returns datum representation for a name.
*
* Note: Name is pass-by-reference; caller must ensure the pointed-to
* value has adequate lifetime.
*/Input, output, and comparison
namein always truncates on a multibyte boundary and always zero-pads:
Datum
namein(PG_FUNCTION_ARGS)
{
char *s = PG_GETARG_CSTRING(0);
Name result;
int len;
len = strlen(s);
/* Truncate oversize input */
if (len >= NAMEDATALEN)
len = pg_mbcliplen(s, len, NAMEDATALEN - 1);
/* We use palloc0 here to ensure result is zero-padded */
result = (Name) palloc0(NAMEDATALEN);
memcpy(NameStr(*result), s, len);
PG_RETURN_NAME(result);
}Every name is exactly NAMEDATALEN bytes, NUL-terminated, and zero-padded
after the terminator. That is what lets comparisons use strncmp up to the
full length safely. nameout is just pstrdup(NameStr(*s)).
namerecv is stricter than namein: a binary value of NAMEDATALEN or more
bytes raises identifier too long. namesend sends only the bytes up to the
terminator — the padding is not transmitted.
Comparison goes through one helper, namecmp:
static int
namecmp(Name arg1, Name arg2, Oid collid)
{
/* Fast path for common case used in system catalogs */
if (collid == C_COLLATION_OID)
return strncmp(NameStr(*arg1), NameStr(*arg2), NAMEDATALEN);
/* Else rely on the varstr infrastructure */
return varstr_cmp(NameStr(*arg1), strlen(NameStr(*arg1)),
NameStr(*arg2), strlen(NameStr(*arg2)),
collid);
}Because name is declared typcollation => 'C', the system-catalog fast path
is the norm: a plain strncmp over 64 bytes. Only if a query forces some other
collation does it fall back to the full varstr_cmp.
Two helpers complete the picture. namestrcpy copies a C string into a Name
and forces the last byte to '\0', zero-padding the rest. namestrcmp compares
a Name to a C string, assuming the C collation. And nameconcatoid — used by
information_schema to make per-schema-unique names — appends _oid and, if
that would overflow, truncates the name, not the suffix:
/* Truncate oversize input by truncating name part, not suffix */
if (namlen + suflen >= NAMEDATALEN)
namlen = pg_mbcliplen(NameStr(*nam), namlen, NAMEDATALEN - 1 - suflen);Why this layout
name is a fixed 64-byte array for the same reason ItemPointerData is six
bytes rather than eight: these structs are stored inline in system catalog
tuples, millions of times over, and typalign => 'c' means no padding is ever
inserted before them. The NOTE in pg_type.h makes the constraint explicit —
for types used in system tables, “it is critical that the size and alignment
defined in pg_type agree with the way that the compiler will lay out the field
in a struct representing a table row.” pg_class.relname, pg_type.typname,
pg_attribute.attname, and dozens more are NameData fields, so NAMEDATALEN
and the struct layout are one contract.
char, by contrast, is the cheapest possible thing: one byte, by value, with
no allocation and no length. It exists to give name an element type and to
give C code a way to move around raw single bytes.
Look at it yourself
pg_type exposes the flags directly:
SELECT typname, typlen, typbyval, typalign, typstorage, typelem
FROM pg_type
WHERE oid IN (18, 19);name round-trips through SQL input and output:
SELECT 'foo'::name, length('foo'::name);
-- foo | 3
-- name truncates on a character boundary
SELECT 'a' || repeat('b', 100) || 'é'::name;The internal "char" type prints with the octal escape. With
standard_conforming_strings on, E'\200' is a one-byte string containing
0x80, and casting it to "char" makes charout emit the escape:
SELECT E'\200'::"char";
-- \200What it all means
"char"is not SQLCHAR; it is an internal one-byte by-value type whose element relationship tonameis the point.nameis a fixed 64-byte, NUL-padded, by-reference struct.NAMEDATALENis an ABI constant shared by every loadable module and every catalog row.namecomparisons are plainstrncmpunder the C collation, and the padding is what makes that safe.charcompares as unsigned and converts as signed. That asymmetry is documented in the source, not a bug.
Further reading
src/backend/utils/adt/char.c—"char"I/O, comparison, and conversion.src/backend/utils/adt/name.c—nameI/O, comparison, and helpers.src/include/c.h—NameData,Name,NameStr, and thevarlenatypedefs.src/include/pg_config_manual.h—NAMEDATALEN.src/include/postgres.h—DatumGetChar,CharGetDatum,DatumGetName.src/include/fmgr.h—PG_GETARG_CHAR/PG_RETURN_CHAR,Pg_abi_values.