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:
{ 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:
| Field | Value | Meaning |
|---|---|---|
typlen | 1 | one byte on disk |
typbyval | t | passed by value, not by reference |
typalign | c | TYPALIGN_CHAR: no alignment padding |
typcategory | B | the Boolean category (sorts last in the built-in order) |
array_type_oid | 1000 | _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:
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:
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:
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:
{ 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):
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:
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:
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):
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:
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
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); -- NULLThe parser’s prefix behavior is easy to confirm:
SELECT 'ye'::bool, 'n'::bool, 'off'::bool, 'of'::bool;
-- t, f, f, fAnd the catalog entry is visible directly:
SELECT typname, typlen, typbyval, typalign, typcategory
FROM pg_type WHERE oid = 16;
-- bool, 1, t, c, BWhat it all means
- A
boolis one byte of storage and eight bytes of in-memoryDatum, withtypbyvalmeaning no pointer chasing. typalign => 'c'means it never forces padding, so aboolcolumn can sit at any byte offset in a tuple.booloutsayst/f;booltextsaystrue/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), withoas 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— theboolcatalog entry (OID 16).src/include/postgres.h—DatumGetBool,BoolGetDatum.src/include/catalog/pg_type.h—typalignsemantics.src/backend/access/nbtree/nbtcompare.c—btboolcmp.