Configure a table¶
TablePartitionConfig describes one partitioned table. This guide shows the two ways to
write one, how to load it from the environment or JSON, and what is checked when.
The flat spelling¶
For the ordinary time-partitioned table — a RANGE root over a timestamp, one partition
per period:
from pg_partsmith import PartitionGranularity, TablePartitionConfig
config = TablePartitionConfig(
schema="public",
table_name="events",
partition_column="created_at",
granularity=PartitionGranularity.MONTH,
create_ahead_count=3,
retention_count=12,
)
| Field | Meaning |
|---|---|
schema |
Schema of the parent table. Optional but recommended: everything is then schema-qualified and independent of search_path. |
table_name |
The parent table, lowercase. |
partition_column |
Leading column of the partition key — the time dimension. |
trailing_partition_columns |
The rest of a composite key, in key order. |
granularity |
HOUR, DAY, WEEK, MONTH, QUARTER, YEAR. |
tz |
Timezone the calendar is computed in (IANA name or ZoneInfo); UTC by default. |
boundary_codec |
Physical encoding of the key: "uuidv7", "epoch_seconds", "epoch_milliseconds", or a codec instance. |
subpartition |
A level below the root: HashPartitioning(...) or ListPartitioning(...). |
create_ahead_count |
Periods that must exist, the current one included. |
retention_count |
Newest periods kept, the current one included — a count, not a distance. |
partition_type, partition_strategy |
Optional; checked against the scheme. |
The composed spelling¶
Everything else — an integer axis, a hash or list root, a sliding list, a policy beyond "N ahead, M kept", leaves that are not plain tables — is spelled out:
from datetime import timedelta
from pg_partsmith import (
CreateAhead,
DropAfter,
HashPartitioning,
KeepNewest,
LifecyclePolicy,
LocalLeaves,
PartitionGranularity,
RangePartitioning,
TablePartitionConfig,
TimeBoundaries,
)
config = TablePartitionConfig(
schema="public",
table_name="events",
scheme=RangePartitioning(
key="created_at",
boundaries=TimeBoundaries(granularity=PartitionGranularity.MONTH, tz="Europe/Helsinki"),
child=HashPartitioning(key="tenant_id", modulus=4),
),
lifecycle=LifecyclePolicy(
creation=CreateAhead(count=3),
retention=KeepNewest(count=12),
drop=DropAfter(grace=timedelta(days=7)),
),
leaves=LocalLeaves(storage_parameters={"fillfactor": 90}),
)
| Field | See |
|---|---|
scheme |
Partition schemes |
lifecycle |
Lifecycle policies |
leaves |
Leaf backends |
Passing scheme together with the flat scheme fields, or lifecycle together with
create_ahead_count / retention_count, is an error: one spelling per config.
Whichever spelling you used, both views are there:
config.scheme # the root level
config.lifecycle # the policy
config.leaves # what kind of relation the leaves are
config.partition_type # PartitionType.RANGE / LIST / HASH — the root's method
config.partition_strategy # TIME_BASED / NUMERIC_BASED / VALUE_BASED / HASH_BASED
config.partition_columns # the root's whole key
config.levels # every level, root first
config.qualified_name # "public.events"
From the environment¶
PartitionTableSettings (pip install "pg-partsmith[pydantic-settings]") reads the flat
fields from environment variables, and SCHEME / LIFECYCLE / LEAVES as JSON for the
composed form:
from pydantic_settings import SettingsConfigDict
from pg_partsmith.settings import PartitionTableSettings
class EventsSettings(PartitionTableSettings):
model_config = SettingsConfigDict(env_prefix="EVENTS_")
config = EventsSettings().to_config()
EVENTS_TABLE_NAME=events
EVENTS_SCHEMA_NAME=public
EVENTS_PARTITION_COLUMN=created_at
EVENTS_GRANULARITY=month
EVENTS_CREATE_AHEAD_COUNT=3
EVENTS_RETENTION_COUNT=12
Every variable is listed in Environment settings.
From JSON¶
Configs are frozen Pydantic models. model_dump(mode="json") produces a document —
timezones as IANA names, built-in codecs by name, policies discriminated on kind — and
model_validate reads it back, nested schemes, numeric boundaries and combinator
policies included:
payload = config.model_dump(mode="json", by_alias=True)
same = TablePartitionConfig.model_validate(payload)
Custom calculators, custom codecs and Callback predicates are Python objects and are
left out of the dump; a config using them is built in code.
Several tables¶
One config per table; the service is stateless across them:
CONFIGS = [events_config, audit_config, outbox_config]
for table_config in CONFIGS:
result = await maintainer.run_maintenance_safe(table_config)
Tables are locked and maintained independently — a problem with one never blocks another.
What is checked, and when¶
At construction — no database involved: an unknown or misspelled field is refused
rather than silently dropped (a retention_cout typo must not shrink retention to its
default), identifiers are lowercased and checked, a LIST key is one column, every level
partitions on a fresh column, a sliding list has no CreateAhead, a retention age is
never negative, every generated name fits 63 bytes at every level:
table_name 'a_very_long_table_name_for_an_events_store_that_goes_on_and_on' is too long for
this scheme: table_name (62) + the longest generated suffix (9) = 71 > 63 bytes. PostgreSQL
truncates identifiers silently, which would collapse two partitions onto one name.
At plan time — against the catalog: the table exists and is partitioned by the root's
method on the root's key (composite keys compared in key order; mixed-case and expression
keys refused); the calendar's timezone and the repository's ddl_timezone agree; every
nested level's key columns appear in every UNIQUE / PRIMARY KEY constraint; foreign
leaves are not asked of a parent with a unique index. Each failure is an
InvalidPartitionConfigError naming the fix — the timezone mismatch a ValueError —
raised before any DDL.