Polecat-owned rolling range partitions for time-series document tables (#386) - #387
Merged
Merged
Conversation
…386) PartitionOn(x => x.BucketEnd).ByRollingRange(PartitionPeriod.Month, ahead, behind) declares a rolling time window and Polecat owns every statement that moves it: NEXT USED + SPLIT RANGE at the leading edge, partition TRUNCATE + MERGE RANGE at the trailing one. ByExternallyManagedRange stays, now documented for what it is actually for — genuinely external ownership. Built on weasel#401, shipped in Weasel 9.21.0 (bumped here from 9.18.0, with the JasperFx family to 2.36.3 to match its floor). The window is a pure function of the policy and the clock, so a window that has rolled forward is never mistaken for drift: CreateDelta is additive, which means ordinary schema migration provisions the leading edge. Migration never removes data, so the retention half is driven separately — a PolecatActivator pass gated on the same ApplyAllDatabaseChangesOnStartup opt-in as the migration itself, plus Advanced.ApplyRollingPartitionsAsync / RollPartitionsForwardAsync / DropAgedRollingPartitionsAsync for hosts that outlive their periodsAhead. Managers are discovered from each database's own schema objects rather than tracked on StoreOptions, so a document type that grows a rolling window later is picked up and re-running the pass is always idempotent. Passing one manager to several document types rolls all of their tables forward in a single pass. ByRollingRange asserts at configuration time that the partition member is a DateTime or DateTimeOffset, turning what would be an opaque partition-function error during the first migration into a message that names the member. 11 tests in rolling_range_partitioning_tests.cs drive the window with a mutable TimeProvider rather than the calendar. Full suite 1623/0 on net10.0, EF Core 37/0 — also the regression gate on the Weasel bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #386. Depends on JasperFx/weasel#401, shipped in Weasel 9.21.0 (bumped here from 9.18.0). SQL Server counterpart to JasperFx/marten#5094.
Problem
PartitionOn(x => x.BucketEnd).ByExternallyManagedRange(boundaries)was the only workable option for a time-series document table: Polecat seeds the partition function/scheme once and the application owns everything after that. For a time-partitioned table "everything after that" is the whole lifecycle — rolling a period forward meansALTER PARTITION SCHEME … NEXT USEDfollowed byALTER PARTITION FUNCTION … SPLIT RANGE, and retiring an aged one meansMERGE RANGE, so applications end up hand-writing partition DDL on a schedule forever.Opting out of Weasel means opting out of its ordering and dependency management, not just its DDL generation. Field case in JasperFx/CritterWatch#886 (the Marten flavor of the same table): a hand-rolled rebuild re-created a partitioned table in a schema missing a helper function the generated index DDL depended on, and an ordinary computed index failed with a bare
42883. The index was correct; bypassing Weasel was the defect.What this adds
PartitionPeriodcoversHour,Day,Week,Month,Year. Overloads also take aRollingWindowPolicydirectly, or a pre-builtManagedRangePartitions— pass the same manager to several document types to roll all of their tables forward in one pass (and to setFilegroup). ATimeProvideris injectable so the window is testable without waiting on the calendar.How the two halves are driven
CreateDeltais purely additive, so a rolled-forward window is aSPLIT, never a rebuild of the partition function and every table on itPolecatActivatorstartup pass +Advanced.*The startup pass is gated on the same opt-in as the migration itself (
ApplyAllDatabaseChangesOnStartup) — that is how a host says "Polecat owns this schema", and retiring a partition is emphatically a schema change.For hosts whose processes outlive
periodsAhead— an hourly window especially —AdvancedOperationsgainsApplyRollingPartitionsAsync, plusRollPartitionsForwardAsync(additive only) andDropAgedRollingPartitionsAsync(retention only) for callers who want the halves separately. Each returns WeaselTablePartitionStatus[], so a partial failure surfaces per table.SQL Server specifics the implementation respects
NEXT USEDbefore everySPLIT, or the split fails.TRUNCATEbeforeMERGE RANGE, in that order.MERGE RANGEon a partition holding rows moves them into the neighbour rather than reclaiming anything — the opposite of the point. Truncating first deallocates the pages in O(1) and the merge is then metadata-only. A failed truncate deliberately leaves the boundary in place.RANGE RIGHTthroughout, so a boundary is the inclusive lower bound of its period, matching the half-open[From, To)intervals the policy produces.(-inf, +inf), so unlike PostgreSQL there is no overflow rejection to guard against and noDEFAULTpartition to declare.Design notes
StoreOptions. That stays correct for a document type that grows a rolling window later, and makes re-running the pass unconditionally idempotent. Reference identity is the key, exactly asManagedRangePartitions.ResolveManagedTablesmatches tables back to their manager.ByRollingRangeasserts at configuration time that the partition member is aDateTime/DateTimeOffset, and that a shared manager's column and SQL data type match what the member resolves to. Both turn what would be an opaque error during the first migration into a message that names the member.Duplicate(...)call to remember.RollingRangewith the window's current boundaries, since there is no declared list to report.Acceptance criteria from the issue
host_startup_rolls_forward_and_retires_under_ApplyAllDatabaseChangesOnStartuprolling_the_window_forward_is_additive_and_never_a_rebuild,the_apply_pass_is_idempotentconcurrent_apply_passes_do_not_throw_and_converge_on_the_windowDELETE—the_additive_and_retention_halves_can_be_run_separatelyDocs
docs/documents/partitioning.mdgains a strategy-selection table up front, a Rolling time windows section (policy, shared managers, how the two halves are driven, whyTRUNCATEprecedesMERGE, the retention warning, theAdvanced.*cadence escape hatch), and an Externally-managed range partitions section documenting that door for what it is actually for. The stale "dropping aged partitions is not yet wired into the document API" limitation is gone.Tests
11 new tests in
src/Polecat.Tests/Storage/rolling_range_partitioning_tests.cs, driving the window with a mutableTimeProviderrather than the calendar: window shape on first migration, documents landing per period, overflow rows, additive roll-forward with data survival, a shared manager rolling two tables, the two halves run separately, idempotency, concurrent passes, the full host-startup pass, and the two configuration-time rejections.Green on net10.0: Polecat.Tests 1623/0 (3 pre-existing skips), Polecat.EntityFrameworkCore.Tests 37/0 — the full suite also serving as the regression gate on the Weasel 9.18.0 → 9.21.0 bump, which picks up weasel#391 (managed tenant bucketing actually sharing a partition, #335's ordinal sharing) and weasel#399 along the way.
Known consumer: JasperFx/CritterWatch hand-writes exactly this for
pc_doc_metricssamplein its SQL Server flavor and can delete that code.🤖 Generated with Claude Code