ManagedRangePartitions for SQL Server: rolling time-window partitions (weasel#401) - #414
Merged
jeremydmiller merged 1 commit intoJul 30, 2026
Merged
Conversation
…-forward and TRUNCATE+MERGE retention (weasel#401) SQL Server parity for the PostgreSQL ManagedRangePartitions shipped in #413. RangePartitioning here takes a static list of boundary values, so rolling a new period forward (NEXT USED + SPLIT) and retiring an aged one was left to the application -- a hand-rolled T-SQL chore with published recipes and no standard tooling. Adds Weasel.SqlServer ManagedRangePartitions over the same RollingWindowPolicy, attached with Table.PartitionByRollingWindow. RollForwardAsync splits in the boundaries the window has gained; DropAgedPartitionsAsync retires everything below the retention floor; ApplyAsync does both idempotently. Retention is TRUNCATE TABLE ... WITH (PARTITIONS (n)) and THEN MERGE RANGE, in that order. MERGE RANGE against a partition that still holds rows reclaims nothing -- it moves those rows into the neighbouring partition, which is the opposite of the point. Truncating first deallocates the pages in O(1) and leaves the merge metadata-only. If the truncate fails the boundary is left in place rather than merging rows we failed to reclaim. Only boundaries landing exactly on a period start for the policy are retired, so a hand-added boundary is left alone; the underflow partition is reclaimed with the oldest boundary, since it holds late-arriving rows dated before the window ever started. The declared boundaries are the window's period starts PLUS the exclusive end of the newest provisioned period. Without that trailing boundary the top partition would hold the newest period's rows and every roll-forward would SPLIT a partition containing data -- a fully logged row movement. With it the top partition is always empty and SPLIT stays metadata-only. TableDelta now dispatches boundary migration on a new ISplittablePartitioning rather than the concrete RangePartitioning, so both declarative and rolling-window strategies migrate in place. ManagedTenantPartitions deliberately does not implement it and stays exempt, as before. Closes #401. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PefhUPhDZcmSUhfQFBqXxR
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.
Part two of #401 — SQL Server parity for the PostgreSQL
ManagedRangePartitionsthat shipped in #413. Closes the issue.The gap
Weasel.SqlServer.Tables.Partitioning.RangePartitioningtakes a static list of boundary values, so rolling a new period forward (NEXT USED+SPLIT) and retiring an aged one was left to the application. There is essentially no tooling for this on SQL Server — sliding-window management is a hand-rolled T-SQL chore whose published guidance amounts to "write a script that setsNEXT USEDandSPLITs, and another thatMERGEs".What this adds
ManagedRangePartitionsover the sameRollingWindowPolicythe Postgres half uses:RollForwardAsync—ALTER PARTITION SCHEME … NEXT USED+ALTER PARTITION FUNCTION … SPLIT RANGEfor boundaries the window has gained.DropAgedPartitionsAsync— retire everything below the retention floor.ApplyAsync— both, idempotent.CreateDeltanever returnsRebuildfor a boundary-set difference; a column or type change still does.Two decisions worth the review time
Retention is
TRUNCATEand thenMERGE RANGE, in that order. This is the point you flagged when we scoped it. BareMERGE RANGEagainst a partition that still holds rows reclaims nothing — it moves those rows into the neighbouring partition.TRUNCATE TABLE … WITH (PARTITIONS (n))(SQL Server 2016+) deallocates the pages in O(1), and the merge that follows is metadata-only against an empty partition. No staging table, so none ofSWITCH OUT's structural preconditions. If the truncate fails the boundary is deliberately left in place rather than merging rows we failed to reclaim.The declared boundaries are the window's period starts plus the exclusive end of the newest provisioned period. Without that trailing boundary the top partition is
[newest period start, +infinity)and holds the newest period's rows — so every roll-forward wouldSPLITa partition containing data, a fully logged row movement. With it the top partition is always beyond everything provisioned, therefore empty, andSPLITstays metadata-only.Smaller notes:
TableDeltanow dispatches boundary migration on a newISplittablePartitioningrather than the concreteRangePartitioning, so declarative and rolling-window strategies both migrate in place.ManagedTenantPartitionsdeliberately does not implement it and stays exempt, exactly as before.if, not a ternary:DateTimeconverts implicitly toDateTimeOffset, so a ternary over the two silently types itself asDateTimeOffsetand renders every boundary in the offset-carrying shape regardless of the column type. Caught by the round-trip test.Tests
14 new tests (6 pure-DDL/delta, 8 integration) covering the window boundaries, additive roll-forward, aged boundaries not reading as drift, column change still rebuilding, truncate+merge retention, underflow reclaim, leaving foreign boundaries alone, idempotent apply, and skipping a table whose partition function is absent.
Full SQL Server suite green: 337 passed, 0 failed, 8 skipped. PostgreSQL (819) and Core (50) suites re-run green as well.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PefhUPhDZcmSUhfQFBqXxR