Repository navigation
feat: partition_create_ahead keeps a time-partitioned parent ahead of now (#1297) - #1308
Conversation
jdatcmd
left a comment
There was a problem hiding this comment.
Review of 0d8e3f7. The lock design is right: a standalone CREATE then ATTACH, with SHARE UPDATE EXCLUSIVE on the parent. The timestamp-zone fix and its arms are good work. Four inputs break the function, though, each measured.
How I measured
I ran the function body from 0d8e3f7 on PG18 as public.pca, with USING pgcolumnar replaced by USING heap. The DDL paths are core's, so the access method doesn't change these answers. SET TimeZone = 'UTC'.
| parent | call | result |
|---|---|---|
a partition FROM ('2026-10-12') TO (MAXVALUE) |
pca('m1','1 day',2) |
ERROR: partition "m1_20261012T000000" would overlap partition "m1_tail" |
w int GENERATED ALWAYS AS (v * 2) STORED |
pca('g1','1 day',1) |
ERROR: column "w" in child table must be a generated column |
| a 63-character table name | pca(<name>,'1 day',2) |
NOTICE: identifier ... will be truncated, then ERROR: relation ... already exists |
| empty, timestamptz | pca('s1','1 second',20000) |
6 min 37 s, then ERROR: out of shared memory, 0 partitions |
| an identity column | pca('i1','1 day',1) |
fine: 2 created, and an insert gets id 1 |
Blocking
-
Generated columns.
LIKE ... INCLUDING DEFAULTS INCLUDING CONSTRAINTScopies a stored generated column as a plain column, and ATTACH refuses that. Any parent with a generated column can't use the function. AddINCLUDING GENERATED, and an arm with a generated column on the parent. Identity is fine as it is (row 5). -
Long names. The suffix
_YYYYMMDDTHH24MISSis 16 characters, so any parent name over 47 characters truncates, and the second partition collides with the first. Either truncate the prefix to63 - 16characters yourself, so the date part always survives, or refuse with a clear SQLSTATE. The arm: a 63-character parent withahead = 2. -
No bound on the work. One call with a small width, or a parent whose newest partition is far in the past, tries to create everything in one transaction. It runs for minutes holding locks, then fails on the lock table with nothing created. Refuse up front when the count would exceed a limit, with
22023and a HINT naming the count, before any DDL. You can compute the count exactly fromlo,targetandwidth, except for month widths, which can use an upper estimate. A fixed cap such as 1000 is simple; tying it tomax_locks_per_transactionis more exact. The arm: the refusal fires, nothing is created, and it returns quickly.
Should fix
- MAXVALUE (and DEFAULT-like tails). The regex
TO \('([^']+)'\)doesn't matchTO (MAXVALUE), so that partition is ignored, and the first new one overlaps it. Today that surfaces as an overlap error and nothing is created, so it fails safe, but the message doesn't say why. Refuse with55000("has a partition bounded by MAXVALUE"), as you do for DEFAULT, and add an arm.
Checked, no change
- Invoker rights,
SET search_path = pg_catalog, pg_temp, andparentrendered through regclass: safe.%Iis used for the new names. - Day widths across DST: contiguity follows from adding to the highest existing bound, and your west/east arms cover it.
- The drop half staying out: agreed. It belongs with the daemon (#1307), once both have landed.
|
All four are taken, and each has an arm I've seen red against 0d8e3f7 on PG17 for the reason you measured: The 20,000-partition call took 11 minutes here, then ran out of shared memory, which confirms your 6 min 37 s. The fixes:
Both twins are 27/27 on PG15–19. Mutation runs are going now: the seven earlier mutants, plus one per fix, each red where aimed, in both twins for the four new ones. I'll push once they and the ledger replay are done. 🤖 Generated with Claude Code |
…nt ahead of now (commandprompt#1297) Creates columnar partitions, each width wide and contiguous with the highest existing upper bound, until they reach now plus ahead widths. Standalone CREATE then ATTACH, so the parent is held SHARE UPDATE EXCLUSIVE, never ACCESS EXCLUSIVE. A DEFAULT partition is refused (55000). Found by new arms against the prototype: a timestamp key's bounds were read as timestamptz in the session's zone and written converted from UTC, so in America/New_York partitions had a four-hour gap (a row at the boundary had nowhere to go), and daily partitions made from Tokyo began at 15:00. A timestamp key is now reasoned about in its own type, with localtimestamp as now. partition_lifecycle.sh and its independent pytest twin: 23/23 on PG15-19 each. Five mutants red in both (the prototype's zone handling, PARTITION OF, no DEFAULT refusal, starting from now, heap partitions). Upgrades from every released version converge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pytest twin covers every shell property (compare_to_bash: 17 literal, 3 template, 0 missing). Ledger: 23 rows from a five-major run, 17 dated by seven mutations merged one log at a time with --mutation and --target; the six 'never' rows are premises. One check a mutation reddened is no longer named a premise (commandprompt#1236). Census 1674 -> 1680. TESTS.md section 92; cluster_tests 499 by collection. Guard half 413 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…work bound and MAXVALUE (commandprompt#1308 review) Four inputs @jdatcmd measured breaking the function, each an arm red against 0d8e3f7 on PG17 for the reason measured: - a stored generated column: LIKE now INCLUDING GENERATED (was 42804 at ATTACH); - a 63-character parent: the prefix is shortened by whole characters until prefix + the 16-byte date fits in 63 bytes (was a truncated name colliding); - no bound on the work: a dry run counts the partitions before any DDL and refuses more than 1000 with 22023 (one call ran 11 minutes, then 53200); - a MAXVALUE upper bound: refused with 55000 (was an overlap, 42P17). The pytest twin's long name is 31 two-byte characters, so truncation must never split one. Both twins 27/27 on PG15-19; one mutant per fix, red in both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ith the commandprompt#1308 review's arms 27 rows from a five-major run, 21 dated by eleven mutations merged one log at a time with --mutation and --target; the six 'never' rows are premises. Census 1694 -> 1700. TESTS.md section 93; cluster_tests 501 by collection. Docs and CHANGELOG name the MAXVALUE and 1000-partition refusals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
All four are fixed at 9dcbd59, rebased onto d7c9951 (after #1312). The fixes (dba7f5b): Red, then green, in both twins:
Removal proofs: eleven mutants on PG17, each red on its arm: the seven from the first round, plus one per fix ( Registries:
One honest note on the ledger: the logs ran before #1306 and #1312 merged. Those touched only 🤖 Generated with Claude Code |
0d8e3f7 to
9dcbd59
Compare
jdatcmd
left a comment
There was a problem hiding this comment.
Approved at 9dcbd59. I re-ran my original probe against this head's function body, without relying on the PR text. It's the same harness as before: PG18, USING heap substituted, TimeZone = UTC.
| case | 0d8e3f7 | 9dcbd59 |
|---|---|---|
a partition TO (MAXVALUE) |
overlap error | ERROR: public.m1 has a partition bounded by MAXVALUE, refused before any DDL |
| a stored generated column | column "w" in child table must be a generated column |
2 partitions; INSERT (ts, v) = (now(), 21) gives w = 42 |
a 63-character parent, ahead = 2 |
truncation, then already exists |
3 partitions, ..._abc_20261013T000000 through ..._20261015T000000, with the date suffix intact |
'1 second', ahead = 20000 |
6 min 37 s, then out of shared memory |
refused in 1.2 ms, 0 partitions |
'1 minute', ahead = 1001 (1002 partitions) |
refused in 0.7 ms, 0 partitions |
I accept your ledger note: #1306 and #1312 touched only the TID range and cursor paths, which this suite never reaches. I'll merge once CI is green.
|
Merging now: approved at 9dcbd59, 15/15 green. |
pgcolumnar.partition_create_ahead: the create half of #1297partition_create_ahead(parent regclass, width interval, ahead int) returns intcreates columnar partitions, eachwidthwide and contiguous with the highest existing upper bound, until they reach now plusaheadwidths. A second call creates none. It goes intopgcolumnar--1.0-alpha5.sqland the alpha4→alpha5 upgrade script, per the gate change on #1283.CREATE TABLEand thenATTACH, which takes SHARE UPDATE EXCLUSIVE.CREATE TABLE ... PARTITION OFwould take ACCESS EXCLUSIVE (Native partition lifecycle for time-partitioned columnar tables: create ahead, drop past retention #1297 note 1, measured on PG15–19).55000), because it rules out DETACH CONCURRENTLY for the drop half.The bug the new arms found in my prototype
For a
timestampkey, which has no zone, the prototype read existing bounds astimestamptzin the session's zone and wrote new ones converted from UTC.2026-10-11 00:00:00followed by04:00:00. A row at that boundary had nowhere to go.A
timestampkey is now handled in its own type: bounds are read astimestamp, "now" is the session'slocaltimestamp, and nothing is converted.timestamptzwas already right.Tests (written first; the original 14 arms were red on main)
test/partition_lifecycle.sh: 23/23 on PG15–19.test/pytest/test_partition_lifecycle.py, independent of the shell suite: 23/23 on PG15–19. It uses six-hour widths, America/Los_Angeles, and Asia/Kolkata for a half-hour offset.compare_to_bash: 17 literal and 3 template matches, 0 missing.native_upgrade_converge: 21/21. Every released version reaches the same catalog as a fresh install.Removal proofs, each red in both twins: the prototype's zone handling;
CREATE TABLE ... PARTITION OF(the lock arm); no DEFAULT refusal; starting from now instead of the highest bound (the second call then fails); heap partitions; stopping one width short (the coverage arms); and a one-minute gap between partitions (the contiguity and boundary arms).Registries
--mutationand--target. The sixneverrows are all premises.🤖 Generated with Claude Code