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:
{ 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:
typedef unsigned int Oid;
#define InvalidOid ((Oid) 0)
#define OID_MAX UINT_MAXOid8 is a newer addition in src/include/c.h:
/* 8-byte Object ID */
typedef uint64 Oid8;
#define InvalidOid8 ((Oid8) 0)
#define OID8_MAX UINT64_MAXThe Datum conversions are the same cast-to-from-Datum shape as the
integers (src/include/postgres.h):
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:
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:
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:
/*
* 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:
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:
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:
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:
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):
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:
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:
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:
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
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:
SELECT '0b101'::oid; -- ERROR: invalid input syntax for type oid: "0b101"What it all means
Oidisunsigned int,Oid8isuint64; both aretypbyval, so the identifier lives inside theDatum.InvalidOid(0) is the sentinel, not a valid object id.- Input goes through
uint32in_subr/uint64in_subr(strtoul/strtou64with base 0), so hex and octal prefixes work but0b/0odo not, and a minus sign is accepted for backwards compatibility. oid8is the 64-bit sibling, and in this tree it is the identity type for TOASTchunk_idvalues.
Further reading
src/backend/utils/adt/oid.c—oidI/O,oid_cmp,oidvector.src/backend/utils/adt/oid8.c—oid8I/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—oid8as the TOAST chunk-id type.