Leaf backends¶
A leaf is the relation that stores rows: the deepest member of every branch. The scheme
decides how many leaves exist and what each owns; config.leaves decides what kind of
relation each one is.
from pg_partsmith import ForeignLeaves, LocalLeaves
TablePartitionConfig(..., leaves=LocalLeaves()) # the default
TablePartitionConfig(..., leaves=LocalLeaves(tablespace="fast_ssd", storage_parameters={"fillfactor": 70}))
TablePartitionConfig(..., leaves=ForeignLeaves(server="archive", options={"table_name": "{relname}"}))
Branches — partitions that partition further — are always local tables: PostgreSQL has no foreign partitioned tables.
Local leaves¶
LocalLeaves() is what every configuration gets: ordinary tables created
LIKE parent INCLUDING ALL EXCLUDING IDENTITY. Three things LIKE does not carry can be
declared:
| Field | Effect |
|---|---|
tablespace |
TABLESPACE on every created relation, leaves and branches alike. PostgreSQL refuses pg_default here; name a real tablespace. |
storage_parameters |
WITH (fillfactor = 70, autovacuum_enabled = false, toast.autovacuum_enabled = true) on every created leaf. Branches take none — PostgreSQL refuses storage parameters on a partitioned table. Values of any type are rendered as string literals, which PostgreSQL accepts for every parameter. |
inherit_privileges |
The parent's owner and grants are replayed onto every created relation, in the transaction that created it. LIKE copies neither. A role that reads through the parent needs no grant on a leaf, but one that addresses leaves directly — an export job, a pg_dump of one partition — does. A grant the maintenance role may not make rolls the creation back: nothing half-configured is left behind. |
leaves=LocalLeaves(tablespace="fast_ssd", storage_parameters={"fillfactor": 70}, inherit_privileges=True)
The classic pg_partman template table carried exactly these; LocalLeaves is the
declarative form.
Foreign leaves¶
ForeignLeaves(server, options) creates every leaf as
CREATE FOREIGN TABLE … (columns of the parent) SERVER server OPTIONS (…) and attaches it
like any other partition. The rows of a window then live wherever the foreign server keeps
them — a column store, an archive database — while the table is still queried through one
parent. This is the pg_clickhouse shape.
leaves=ForeignLeaves(server="clickhouse", options={"table_name": "{relname}", "engine": "MergeTree"})
Option values are templates:
| Placeholder | Value |
|---|---|
{relname} |
the leaf's own relation name (events__2026_08) |
{schema} |
the leaf's schema |
{parent} |
the relation it is attached to |
{root} |
the table the configuration is for |
A template with any other placeholder is refused at construction; literal values pass
through. The server and its user mapping exist before the first plan; pg-partsmith never
creates them. Whether the remote table exists is the foreign data wrapper's business —
postgres_fdw checks at query time, not at creation.
PostgreSQL's rule¶
A foreign table can be a partition only of a parent without a unique index or primary
key: PostgreSQL cannot enforce uniqueness across a foreign relation and refuses both
CREATE FOREIGN TABLE … PARTITION OF and ATTACH PARTITION (42809) when one exists.
Non-unique indexes are fine — they are skipped for the foreign member. The service checks
this against the catalog and refuses the configuration before any DDL, naming the
constraints in the way.
Ownership¶
Under a ForeignLeaves configuration a foreign partition is a lifecycle partition like any
other: created ahead, expired, detached with its COMMENT ON FOREIGN TABLE marker and
dropped with DROP FOREIGN TABLE — which removes the mapping and leaves the remote data
alone. Grace periods and DropNever apply unchanged.
Under a LocalLeaves configuration the very same foreign partition is not ours: it is
inspected, reported as foreign_partition (INFO) and never created, detached or dropped.
An archive someone attached behind postgres_fdw stays where it is.
In a nested scheme¶
Only the deepest level's members are leaves. In RANGE(month) → HASH(tenant) the month is
a local partitioned branch and its buckets are the leaves — foreign tables under
ForeignLeaves, tables WITH (…) under LocalLeaves. The branch itself takes the
tablespace but no storage parameters.
Serialization¶
leaves is discriminated on kind ("local" / "foreign") and round-trips through
config.model_dump(mode="json") with the rest of the configuration.
Worked examples: Tier cold data to a foreign server.