How PostgreSQL Implements bool in C

A bool is one byte of storage and a pass-by-value Datum, but it has two different text spellings, an input parser that accepts unique prefixes of eight words, and an aggregate state struct behind bool_and/bool_or.

bool looks like the easiest type in PostgreSQL. One bit of meaning, one byte of storage, done. It is not. The value has two different text spellings depending on which code path you go through, the input parser accepts eight different words plus unique prefixes, the binary wire format is not the text format, and the aggregate machinery behind bool_and/bool_or is a small state machine with its own struct. All of it lives in src/backend/utils/adt/bool.c, a little over 400 lines.

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

The catalog entry

The type’s identity is one line in src/include/catalog/pg_type.dat:

text
{ oid => '16', array_type_oid => '1000',
  descr => 'boolean, format \'t\'/\'f\'',
  typname => 'bool', typlen => '1', typbyval => 't', typcategory => 'B',
  typispreferred => 't', typinput => 'boolin', typoutput => 'boolout',
  typreceive => 'boolrecv', typsend => 'boolsend', typalign => 'c' },

Five of those fields matter here:

FieldValueMeaning
typlen1one byte on disk
typbyvaltpassed by value, not by reference
typaligncTYPALIGN_CHAR: no alignment padding
typcategoryBthe Boolean category (sorts last in the built-in order)
array_type_oid1000_bool is generated automatically from this entry

typalign => 'c' is TYPALIGN_CHAR, documented in src/include/catalog/pg_type.h as “char alignment (ie no alignment needed)”. So a bool column never forces a padding byte before it. typstorage is not listed, so it takes the default 'p' (TYPSTORAGE_PLAIN): a bool is never TOASTed.

The C representation

In memory, a bool is a C99 bool (from <stdbool.h>, pulled in by src/include/c.h), which is one byte holding 0 or 1. But the function-call convention never hands you a bare bool. Every value crosses a function boundary as a Datum — a pointer-sized integer — and the conversion is a pair of trivial inline functions in src/include/postgres.h:

c
static inline bool
DatumGetBool(Datum X)
{
	return (X != 0);
}

static inline Datum
BoolGetDatum(bool X)
{
	return (Datum) (X ? 1 : 0);
}

Notice the asymmetry in the doc comments: “any nonzero value will be considered true.” The representation is 8 bytes in memory but 1 byte on disk. PG_GETARG_BOOL(n) and PG_RETURN_BOOL(x) in src/include/fmgr.h are just these two functions.

Two text spellings

boolout is the type’s official output function, and it emits a single character:

c
Datum
boolout(PG_FUNCTION_ARGS)
{
	bool		b = PG_GETARG_BOOL(0);
	char	   *result = (char *) palloc(2);

	result[0] = (b) ? 't' : 'f';
	result[1] = '\0';
	PG_RETURN_CSTRING(result);
}

That is why SELECT true; prints t and SELECT false; prints f — the type’s own output function is what a bare SELECT invokes. But the cast to text uses a different function, booltext, which follows the SQL standard instead:

c
Datum
booltext(PG_FUNCTION_ARGS)
{
	bool		arg1 = PG_GETARG_BOOL(0);
	const char *str;

	if (arg1)
		str = "true";
	else
		str = "false";

	PG_RETURN_TEXT_P(cstring_to_text(str));
}

src/include/catalog/pg_cast.dat wires that function up for all three string targets:

text
{ castsource => 'bool', casttarget => 'text', castfunc => 'text(bool)', ... }
{ castsource => 'bool', casttarget => 'varchar', castfunc => 'text(bool)', ... }
{ castsource => 'bool', casttarget => 'bpchar', castfunc => 'text(bool)', ... }

So true::text is true, and so is true::varchar and true::char(n). One value, two spellings, chosen by whether you go through boolout or through a cast to a string type.

The permissive input parser

boolin trims leading and trailing whitespace, then calls parse_bool_with_len. That function is a switch on the first character (src/backend/utils/adt/bool.c):

c
bool
parse_bool_with_len(const char *value, size_t len, bool *result)
{
	/* Check the most-used possibilities first. */
	switch (*value)
	{
		case 't':
		case 'T':
			if (pg_strncasecmp(value, "true", len) == 0)
			{
				if (result)
					*result = true;
				return true;
			}
			break;
		...
		case 'o':
		case 'O':
			/* 'o' is not unique enough */
			if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0)
			...

The comment above the function claims it accepts “unique prefixes thereof” — and the implementation really does, because pg_strncasecmp only compares len characters. 't', 'tr', 'tru', and 'true' all parse as true; the same for f/fa/fal/fals/false, y/ye/yes, and n/no. The one exception is o, which the code calls out as “not unique enough”: it clamps the comparison length to at least 2, so 'o' alone is rejected while 'on' and 'of' are accepted.

The full set of accepted inputs is true, false, yes, no, on, off, 1, 0, each case-insensitive and optionally prefixed (except o, which needs two characters).

Binary format

boolrecv and boolsend handle the binary protocol (COPY ... WITH (FORMAT binary)). They do not reuse the text representation:

c
Datum
boolrecv(PG_FUNCTION_ARGS)
{
	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
	int			ext;

	ext = pq_getmsgbyte(buf);
	PG_RETURN_BOOL(ext != 0);
}

Datum
boolsend(PG_FUNCTION_ARGS)
{
	bool		arg1 = PG_GETARG_BOOL(0);
	StringInfoData buf;

	pq_begintypsend(&buf);
	pq_sendbyte(&buf, arg1 ? 1 : 0);
	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
}

One byte on the wire, 0 or 1 on the way out, and any nonzero byte counts as true on the way in. That is the same “any nonzero is true” rule as DatumGetBool, which is what lets a boolean survive a round trip even if a client sends 0x02.

Hashing, comparison, and aggregates

Equality is trivial (arg1 == arg2), and hashing widens the byte to an int32 and reuses the integer hash:

c
Datum
hashbool(PG_FUNCTION_ARGS)
{
	return hash_uint32((int32) PG_GETARG_BOOL(0));
}

The btree comparison is a plain subtraction (src/backend/access/nbtree/nbtcompare.c):

c
Datum
btboolcmp(PG_FUNCTION_ARGS)
{
	bool		a = PG_GETARG_BOOL(0);
	bool		b = PG_GETARG_BOOL(1);

	PG_RETURN_INT32((int32) a - (int32) b);
}

For int4 and wider, btint4cmp cannot subtract (it could overflow) and switches to > / <. For one-byte bools the subtraction is safe.

bool_and and bool_or are where the interesting struct appears. They are aggregates, so they need per-group state. BoolAggState keeps two counters:

c
typedef struct BoolAggState
{
	int64		aggcount;		/* number of non-null values aggregated */
	int64		aggtrue;		/* number of values aggregated that are true */
} BoolAggState;

The transition function bool_accum increments both counters, skipping NULLs, and bool_alltrue / bool_anytrue answer from the counters at the end. That is why bool_and of an all-NULL group returns NULL rather than an arbitrary default. The whole thing is one more example of the general PostgreSQL aggregate pattern: a state struct, a transition function, an inverse (bool_accum_inv, for moving-aggregate mode), and a final function.

Look at it yourself

sql
SELECT 't'::bool, 'tru'::bool, 'o'::bool;      -- t, t, ERROR
SELECT true::text, true::varchar;              -- true, true
SELECT true;                                   -- t
SELECT bool_and(x) FROM (VALUES (true),(false)) v(x);  -- false
SELECT bool_and(x) FROM (VALUES (NULL),(NULL)) v(x);   -- NULL

The parser’s prefix behavior is easy to confirm:

sql
SELECT 'ye'::bool, 'n'::bool, 'off'::bool, 'of'::bool;
-- t, f, f, f

And the catalog entry is visible directly:

sql
SELECT typname, typlen, typbyval, typalign, typcategory
FROM pg_type WHERE oid = 16;
-- bool, 1, t, c, B

What it all means

  • A bool is one byte of storage and eight bytes of in-memory Datum, with typbyval meaning no pointer chasing.
  • typalign => 'c' means it never forces padding, so a bool column can sit at any byte offset in a tuple.
  • boolout says t/f; booltext says true/false. Which one you see depends on the cast.
  • The input parser accepts case-insensitive prefixes of eight words (true/false/yes/no/on/off/1/0), with o as the only minimum-two-character special case.
  • The binary protocol uses one byte and treats any nonzero value as true.

Further reading

  • src/backend/utils/adt/bool.c — all of the above.
  • src/include/catalog/pg_type.dat — the bool catalog entry (OID 16).
  • src/include/postgres.h — DatumGetBool, BoolGetDatum.
  • src/include/catalog/pg_type.h — typalign semantics.
  • src/backend/access/nbtree/nbtcompare.c — btboolcmp.

PostgresInternalsBool