PostgreSQL's oid Implementation in C

oid is a 4-byte unsigned int and oid8 is an 8-byte one; both are pass-by-value Datums parsed by a permissive unsigned parser that accepts hex and octal (and even a minus sign), and in this tree oid8 is the identity type for TOAST chunk ids.

oid is the type of every system identifier in PostgreSQL: tables, indexes, functions, operators, types, namespaces. It looks like a narrower int4, and that is almost right. It is a 4-byte unsigned integer, carried by value inside a Datum, with its own input/output functions and its own operator set. In this source tree there is also a 64-bit sibling, oid8, that is quietly becoming the identity type for out-of-line TOAST values.

This is based on reading the PostgreSQL source (version 20devel, commit e75fcc9867f).

The catalog entries

Two entries in src/include/catalog/pg_type.dat — OID 26 and the newer OID 6437:

text
{ oid => '26', array_type_oid => '1028',
  descr => 'object identifier(oid), maximum 4 billion',
  typname => 'oid', typlen => '4', typbyval => 't', typcategory => 'N',
  typispreferred => 't', typinput => 'oidin', typoutput => 'oidout',
  typreceive => 'oidrecv', typsend => 'oidsend', typalign => 'i' },
{ oid => '6437', array_type_oid => '6442',
  descr => 'object identifier(oid8), 8 bytes',
  typname => 'oid8', typlen => '8', typbyval => 't', typcategory => 'N',
  typinput => 'oid8in', typoutput => 'oid8out', typreceive => 'oid8recv',
  typsend => 'oid8send', typalign => 'd' },

oid is 4 bytes, typbyval, typalign => 'i' (TYPALIGN_INT). oid8 is 8 bytes, typbyval, typalign => 'd' (TYPALIGN_DOUBLE). Both are plain storage, so neither is ever TOASTed. oid is marked typispreferred => 't' for its category.

The C representation

Oid is defined in the client-visible src/include/postgres_ext.h:

c
typedef unsigned int Oid;

#define InvalidOid		((Oid) 0)
#define OID_MAX  UINT_MAX

Oid8 is a newer addition in src/include/c.h:

c
/* 8-byte Object ID */
typedef uint64 Oid8;

#define InvalidOid8		((Oid8) 0)
#define OID8_MAX	UINT64_MAX

The Datum conversions are the same cast-to-from-Datum shape as the integers (src/include/postgres.h):

c
static inline Oid
DatumGetObjectId(Datum X) { return (Oid) X; }

static inline Oid8
DatumGetObjectId8(Datum X) { return (Oid8) X; }

Because both are typbyval, an Oid or Oid8 lives inside the Datum itself. InvalidOid (zero) is the sentinel meaning “no object”; it is not a valid identifier.

Input: the permissive unsigned parser

oidin and oid8in do not use the pg_strtoint* parser. They use two unsigned helpers in src/backend/utils/adt/numutils.c:

c
Datum
oidin(PG_FUNCTION_ARGS)
{
	char	   *s = PG_GETARG_CSTRING(0);
	Oid			result;

	result = uint32in_subr(s, NULL, "oid", fcinfo->context);
	PG_RETURN_OID(result);
}

uint32in_subr wraps strtoul with base 0:

c
	errno = 0;
	cvt = strtoul(s, &endptr, 0);

Base 0 is the detail worth noticing: it accepts 0x/0X hexadecimal and a leading 0 octal form, but not the 0o/0b prefixes that the signed integer parser accepts. So SELECT '0x10'::oid is 16 and SELECT '010'::oid is 8 (octal), while 0b101 is not valid oid input. That asymmetry is a consequence of reusing strtoul rather than the custom signed parser.

The same function also accepts a leading minus sign, which the comment says is for backwards compatibility:

c
	/*
	 * For backwards compatibility, we want to accept inputs that are given
	 * with a minus sign, so allow the input value if it matches after either
	 * signed or unsigned extension to long.
	 */

So SELECT '-1'::oid yields 4294967295, not an error. oid8in uses uint64in_subr, which goes through the strtou64 macro (src/include/c.h) and has the same behavior at 8 bytes.

Output

oidout is a single snprintf with %u:

c
Datum
oidout(PG_FUNCTION_ARGS)
{
	Oid			o = PG_GETARG_OID(0);
	char	   *result = (char *) palloc(12);

	snprintf(result, 12, "%u", o);
	PG_RETURN_CSTRING(result);
}

The 12-byte buffer holds a leading sign, up to 10 digits, and the NUL. oid8out is more careful about avoiding a redundant strlen; it formats into a stack buffer with pg_ulltoa_n (unsigned, since Oid8 is uint64) and then pallocs and memcpys the known length:

c
Datum
oid8out(PG_FUNCTION_ARGS)
{
	Oid8		val = PG_GETARG_OID8(0);
	char		buf[MAXOID8LEN + 1];
	char	   *result;
	int			len;

	len = pg_ulltoa_n(val, buf) + 1;
	buf[len - 1] = '\0';

	result = palloc(len);
	memcpy(result, buf, len);
	PG_RETURN_CSTRING(result);
}

Binary format

oidrecv and oidsend use the width-aware pq_getmsgint / pq_sendint32:

c
Datum
oidrecv(PG_FUNCTION_ARGS)
{
	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);

	PG_RETURN_OID((Oid) pq_getmsgint(buf, sizeof(Oid)));
}

Datum
oidsend(PG_FUNCTION_ARGS)
{
	Oid			arg1 = PG_GETARG_OID(0);
	StringInfoData buf;

	pq_begintypsend(&buf);
	pq_sendint32(&buf, arg1);
	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
}

The oid8 pair is identical in shape but uses pq_getmsgint64 / pq_sendint64 (src/backend/utils/adt/oid8.c). Four bytes on the wire for oid, eight for oid8.

Operators and the qsort comparator

The comparison operators are direct C comparisons, e.g. oideq:

c
Datum
oideq(PG_FUNCTION_ARGS)
{
	Oid			arg1 = PG_GETARG_OID(0);
	Oid			arg2 = PG_GETARG_OID(1);

	PG_RETURN_BOOL(arg1 == arg2);
}

Because Oid is unsigned, the oid_cmp helper used by qsort compares unsigned (src/backend/utils/adt/oid.c):

c
int
oid_cmp(const void *p1, const void *p2)
{
	Oid			v1 = *((const Oid *) p1);
	Oid			v2 = *((const Oid *) p2);

	return pg_cmp_u32(v1, v2);
}

pg_cmp_u32 in src/include/common/int.h returns (a > b) - (a < b) rather than a - b, so it cannot overflow. The btree comparator btoidcmp follows the same three-way pattern, and btoid8cmp does the same for the 64-bit type. oid8 also has a sort-support hook that points at ssup_datum_uint64_cmp, the unsigned 64-bit comparator.

The oidvector type

oidvector (OID 30) is the variable-length array of Oid that system catalogs use. It is not just oid[]; it has its own struct and a validator. buildoidvector shows the layout and one deliberate quirk:

c
	result = (oidvector *) palloc0(OidVectorSize(n));

	if (n > 0 && oids)
		memcpy(result->values, oids, n * sizeof(Oid));

	/*
	 * Attach standard array header.  For historical reasons, we set the index
	 * lower bound to 0 not 1.
	 */
	SET_VARSIZE(result, OidVectorSize(n));
	result->ndim = 1;
	result->dataoffset = 0;		/* never any nulls */
	result->elemtype = OIDOID;
	result->dim1 = n;
	result->lbound1 = 0;

lbound1 = 0 is the historical 0-based indexing. Because a general oid[] can be cast to oidvector, check_valid_oidvector insists on ndim == 1, dataoffset == 0 (no nulls), and elemtype == OIDOID. The btree comparator btoidvectorcmp sorts first by length, then lexicographically.

oid8 in the wild: TOAST chunk IDs

oid8 is not (only) a user-facing curiosity. In src/backend/catalog/toasting.c the TOAST table’s chunk_id column can be either an Oid or an Oid8, chosen by the table’s toast_value_type reloption:

c
			case STDRD_OPTION_TOAST_VALUE_TYPE_OID:
				toast_chunkid_typid = OIDOID;
				break;
			case STDRD_OPTION_TOAST_VALUE_TYPE_OID8:
				toast_chunkid_typid = OID8OID;
				break;

And when building the index on that column, the opclass is chosen to match:

c
	if (toast_chunkid_typid == OID8OID)
		opclassIds[0] = OID8_BTREE_OPS_OID;
	else
		opclassIds[0] = OID_BTREE_OPS_OID;

The earlier article on row storage noted that TOAST pointers identify a value by (toast table, value id). The value id here is exactly this chunk_id, and oid8 exists to give those identifiers a wider namespace without abandoning the Oid-style by-value, unsigned representation.

Look at it yourself

sql
SELECT '0x10'::oid, '010'::oid;            -- 16, 8  (hex, octal)
SELECT '-1'::oid;                          -- 4294967295
SELECT 4294967295::oid8;                   -- 4294967295
SELECT typname, typlen, typbyval, typalign FROM pg_type
WHERE typname IN ('oid','oid8');

To see the parser reject what the integer parser accepts:

sql
SELECT '0b101'::oid;   -- ERROR: invalid input syntax for type oid: "0b101"

What it all means

  • Oid is unsigned int, Oid8 is uint64; both are typbyval, so the identifier lives inside the Datum.
  • InvalidOid (0) is the sentinel, not a valid object id.
  • Input goes through uint32in_subr / uint64in_subr (strtoul/strtou64 with base 0), so hex and octal prefixes work but 0b/0o do not, and a minus sign is accepted for backwards compatibility.
  • oid8 is the 64-bit sibling, and in this tree it is the identity type for TOAST chunk_id values.

Further reading

  • src/backend/utils/adt/oid.c — oid I/O, oid_cmp, oidvector.
  • src/backend/utils/adt/oid8.c — oid8 I/O and operators.
  • src/include/postgres_ext.h — typedef unsigned int Oid.
  • src/include/c.h — typedef uint64 Oid8.
  • src/backend/utils/adt/numutils.c — uint32in_subr, uint64in_subr.
  • src/backend/catalog/toasting.c — oid8 as the TOAST chunk-id type.

PostgresInternalsOidOid8