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:

c
{ 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:

Typetyplentypbyvaltypaligntypstoragetypcategory
char1truec(plain)Z internal
nameNAMEDATALEN (64)falsec(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:

c
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:

c
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:

c
/*
 * 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:

c
/*
 * 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:

c
/*
 * 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:

c
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:

c
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:

c
	/* 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:

sql
SELECT typname, typlen, typbyval, typalign, typstorage, typelem
FROM pg_type
WHERE oid IN (18, 19);

name round-trips through SQL input and output:

sql
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:

sql
SELECT E'\200'::"char";
--  \200

What it all means

  • "char" is not SQL CHAR; it is an internal one-byte by-value type whose element relationship to name is the point.
  • name is a fixed 64-byte, NUL-padded, by-reference struct. NAMEDATALEN is an ABI constant shared by every loadable module and every catalog row.
  • name comparisons are plain strncmp under the C collation, and the padding is what makes that safe.
  • char compares 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 — name I/O, comparison, and helpers.
  • src/include/c.h — NameData, Name, NameStr, and the varlena typedefs.
  • 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.

PostgresInternalsCharName