Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.5.0 (2026-08-28)¶
Features¶
- add boundary codecs for time-sortable partition keys (c10ffdb)
- add ensure_partitions for backfilling explicit periods (2ce6d10)
- add LIST subpartitioning alongside HASH (1267b5b)
- manage HASH and LIST roots that have no time dimension (59321d4)
- manage nested RANGE -> HASH partition trees (0d98360)
- partition trees, boundary codecs, static roots and composite keys (#23) (f150233)
- support composite partition keys (184c55c)
Bug Fixes¶
- answer "is it closed?" instead of raising, and refuse a key we cannot address (188285a)
- count buckets built while finishing a half-built branch (a9ed268)
- exclude identity columns when creating partitions (8f2b985)
- explain a partition that can never report as closed (1acf74a)
- keep doubled quotes from splitting a LIST bound (3f07273)
- keep partition_column in serialized configuration (39c7093)
- leave NULL-keyed rows where PostgreSQL puts them (55c9b75)
- never publish a branch that cannot route its whole keyspace (8b8b1b3)
- quoting hazards that only a hand-built statement can reach (f73dff8)
- read the whole key, every constraint, and the timezone that wrote the bound (5e4ea60)
- spell a partition key as one leading column plus a trailing tuple (a511b44)
- stop the planner abandoning subtrees and planning unusable names (b2bcb96)
- tell a lost race apart from a real conflict, and isolate each branch (cf77fb9)
- tell NULL from 'NULL', and a hidden child from a missing one (46af2e7)
Documentation¶
- add the new boundary and bounds names to the API reference (cfd92b1)
- correct three more claims, and test the one that was only written down (eb6388c)
- correct what the fact-checker falsified, and add what it found missing (5175daf)
- document subpartitioning, boundary codecs and the migration path (fdc4f28)
- name the difference between two fields called partition_type (5ed8863)
- say that issues now fills up on a successful run (d4f37f8)
- say what the database reported, not what was assumed (058801e)
0.4.0 (2026-08-27)¶
⚠ BREAKING CHANGES¶
PostgresPartitionRepository.partition_exists/.is_partition_attachedwere removed — use the identical methods onPostgresMetadataProvider(the repository protocol is write-only by design; the metadata provider is the read API). (#22)MaintenanceIssueStepnow contains only the members that are actually produced:CREATE,DETACH,DROP(theATTACHandHOOK_*members were never emitted).Period.to_date()no longer accepts adayargument.
Added¶
- End-to-end configurable timezone: every calculator accepts
tz(datetime.UTCdefault, or a keyedZoneInfo) — the current period, partition names, and naive boundary literals all follow it; pruning interprets naive catalog boundaries in the calculator's timezone;PartitionLifecycleServicerefuses a calculator/ddl_timezonemismatch so names and real bounds cannot silently drift apart;ddl_timezone=Nonewith a non-UTC calculator logs a warning.HourPeriodCalculatoris UTC-only (local hour names are ambiguous under DST). Defaults are bit-identical to the previous behavior. (#20) - Runtime-checkable
TimezoneAwareCalculator/DdlTimezoneAwareprotocols; new shared pure modulespg_partsmith.partition_bounds,pg_partsmith.pruning_rules,pg_partsmith.catalog_queries;PartitionType.from_partstrat; utils helperscoerce_str,elapsed_ms,describe_exception,is_default_partition_conflict,validate_timezone_alignment;get_period_calculatoris exported frompg_partsmith.strategies;PartitionTableSettings.get_period_calculator(tz=...)forwards the timezone. (#22)
Changed¶
- Library-wide quality pass (behavior-preserving beyond the breaking items above): the
aio/sync mirrors share the pure parsing/pruning/SQL logic instead of hand-maintaining
two copies; repository defaults and SQLSTATE sets live in
pg_partsmith.constants; timezone metadata is discovered via protocols instead ofgetattrsniffing; error/log wording no longer claims recovery where errors propagate;detach_single_partition/drop_single_partitionare documented extension points. (#22)
0.3.0 (2026-08-27)¶
Added¶
- Migration-ergonomics APIs (all mirrored in
aioandsync), extracted from a real migration of a hand-rolled partitioner: service.ensure_partition(config, period)— create and attach the partition for one specific period (idempotent, with DEFAULT reconciliation and attach-race handling); for writers that must guarantee a partition exists before an insert.repository.adopt_partition(table_name, partition_name)— stamp the orphan marker on a legacy detached table so safe-drop accepts it, instead of disabling the guard withdrop_allow_unmanaged.maintain_lifecycle(..., continue_on_error=True)(also on the maintainer andmaintain_partitions) — isolate create/detach/drop failures into the newMaintenanceResult.issues(MaintenanceIssue) instead of aborting the run.metadata.is_partition_closed(partition_name, *, settle_seconds=0)— server-side "the partition's upper bound has passed (+ settle buffer)" check for export pipelines.PartitionInfo.schema_name/PartitionInfo.relnameaccessors;qualify,split_qualified_name, andMaintenanceIssueare exported from the package root.- "Migrating an existing partitioner" documentation guide: retention count-vs-distance, adopting legacy partitions, schema-qualified names, lock ownership of granular calls, per-step error isolation, and export finalization.
0.2.0 (2026-08-26)¶
Added¶
pg_partsmith.sync— synchronous mirror ofpg_partsmith.aiowith the same class names and API, built on the sync SQLAlchemyEngine:PartitionLifecycleService,PartitionMaintainer,maintain_partitions,PostgresPartitionRepository,PostgresMetadataProvider,PostgresAdvisoryLockManager,RedisDistributedLockManager, syncPartitionLifecycleHooks/BasePartitionLifecycleHooks, and sync protocols. Differences from the async package:ddl_timeout_secondsis enforced server-side via PostgreSQLstatement_timeout(per statement), and the Redis lock renews its TTL from a background thread that logs (but cannot cancel maintenance) on renewal failure. (#13)- Hour and quarter partition granularities:
PartitionGranularity.HOUR/.QUARTER,HourPeriodCalculator(table__YYYY_MM_DD_HH, UTC boundaries with hour precision) andQuarterPeriodCalculator(table__YYYY_qN), plushour/quarterfields onPeriodwith validation, arithmetic, and ordering. (#15) Period.to_datetime()— period start as a timezone-aware UTC datetime preserving the hour component; the pruning fallback sort now uses it, so hourly partitions within one day order chronologically.
Changed¶
Periodinternals were restructured around a single per-granularity dispatch; behaviour is unchanged. Built-in calculators now derive names and boundaries fromPerioditself instead of duplicating the formatting and arithmetic. (#16)
Fixed¶
Hardening from a full-library audit (#16) and an external review (#17):
- Pruning fails closed:
infinityupper bounds are treated as unbounded (likeMAXVALUE), and an attached partition whose catalog boundary cannot be interpreted is skipped with a warning instead of being pruned by its name. list_partitionsalways returns schema-qualified partition names taken from the catalog — a partition living in a different schema than its parent can no longer be re-resolved viasearch_pathto an unrelated same-named table.drop_partitionrevalidates attachment and the orphan marker under anACCESS EXCLUSIVElock in the same transaction asDROP TABLE, closing the window where a concurrently reattached or replaced relation could be dropped.- Subpartitioned partitions (
relkind='p') are now recognised by existence checks and orphan discovery, so a detached partitioned child is dropped instead of being silently leaked. - Attach conflict SQLSTATEs (incl.
42809) are only treated as a lost race after verifying the partition is actually attached to the requested parent. - The compensating "return rows to DEFAULT" step now also runs when the attach is interrupted by cancellation (async, shielded) or KeyboardInterrupt (sync).
- A cancellation that lands while awaiting the Redis
SET NXresponse performs a token-checked release, so a server-side-applied SET no longer leaks the lock until TTL. - Detach: a partition left in
inhdetachpendingstate by a cancelledDETACH CONCURRENTLY(e.g. a DDL timeout) is now completed withDETACH PARTITION ... FINALIZEinstead of failing on every subsequent run. - Pruning: partitions with a
MAXVALUEupper bound are never pruned any more — previously the unparseable boundary fell back to name-based ageing, which could drop a catch-all partition holding current data. - DEFAULT reconciliation now runs under the same
SET LOCAL TIME ZONEasATTACH PARTITION, so a non-UTC server timezone no longer moves the wrong row range; if the attach still fails after rows were reconciled, they are moved back to the DEFAULT partition (best effort) instead of being stranded in a detached table. - Attach race handling: SQLSTATE
42809("already a partition", the code PostgreSQL actually raises when a concurrent worker wins the attach) is now tolerated, while55006(partition mid-detach) correctly propagates instead of being mislabelled as "already attached". - DDL statements no longer break when an identifier or literal contains
:(e.g. a pre-existing table comment) — colons are escaped before SQLAlchemytext()parses them as bind parameters. list_partitionsskips (with a warning) partitions whose schema or name contains a dot: such names cannot be addressed safely asschema.relnamestrings and previously produced DDL against the wrong relation.- Boundary parsing only applies to RANGE bound expressions; LIST/HASH bounds no longer yield fabricated from/to values.
- Config validation rejects quoted mixed-case partition columns up front instead of failing later inside reconciliation SQL.
- Lock managers: the per-table acquire rate limit no longer serializes unrelated tables (the delay is now slept outside the shared mutex) and no longer sleeps spuriously on the first acquire after host boot; the Redis lock is released even when the renewal watchdog fails to start; the async Redis lock no longer swallows an external task cancellation during watchdog teardown.
- Maintainer logging: operational
PartitionErrors (e.g. lock contention) are logged as warnings instead of "unexpected exception" errors with tracebacks.
0.1.0 - 2026-05-08¶
First public release of pg-partsmith.
- Time-based partition lifecycle management: create ahead, detach expired, drop orphans.
- Period calculators:
DayPeriodCalculator,WeekPeriodCalculator,MonthPeriodCalculator,YearPeriodCalculatorandBasePeriodCalculatorfor custom strategies. get_period_calculator()— factory function that returns the right calculator for a given granularity.PartitionLifecycleService— orchestrates the full create → detach → drop sequence.PartitionMaintainer— scheduler-friendly wrapper;run_maintenance_safe()never raises.maintain_partitions()— plain async function for APScheduler, Celery Beat, etc.- Lifecycle hooks:
before_create,after_create,before_detach,after_detach,before_drop,after_drop.before_*failures abort the operation;after_*failures are logged as warnings. PostgresPartitionRepository— PostgreSQL DDL implementation.PostgresMetadataProvider— PostgreSQL system catalog queries.PostgresAdvisoryLockManager— session-level advisory locks (no extra dependencies).RedisDistributedLockManager— Redis distributed locks (pg-partsmith[redis-locks]).PartitionTableSettings— pydantic-settings base class for env-driven configuration (pg-partsmith[pydantic-settings]).- Multi-schema support via
schemafield inTablePartitionConfig. - Orphan partition tracking via
COMMENTmarkers on detached tables. - DEFAULT partition reconciliation: moves conflicting rows and retries
ATTACH PARTITION. - TIMESTAMPTZ UTC boundary enforcement (
ddl_timezone="UTC"default). - Safe-drop protection:
UnmanagedPartitionDropErrorguards against dropping unmanaged tables. - All
Protocolclasses are@runtime_checkable— custom implementations can be validated viaisinstance(). - Python 3.11, 3.12, and 3.13 support.