feat(slice): partition a module into shared vs per-tenant changes - #1515
Merged
Conversation
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
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.
Summary
A standalone capability (no apply wiring yet — tool first, use-case later) that answers, systematically, "which of a module's objects can be deployed once and shared across tenants, and which must be materialized per tenant?" — the reuse question behind object-level apply routing (#1514, planning constructive-planning#1298).
It's a reachability pass over the dependency graph the
slicepackage already builds from@pgsql/transform'sclassifyStatements: given the objects that are inherently per-tenant (seeds — e.g. a tenant-owned table), any change that transitively reaches a seed is itself per-tenant; everything else is provably tenant-independent and safe to share. The cross-boundary edges are exactly the shared changes a per-tenant module wouldrequire.Design
partition.tsinpgpm/slice, two layers:partitionChanges({ graph, astEdges, seeds })— pure, I/O-free core. Builds adependentsmap (reverse of declaredrequires∪ AST-discovered edges) and BFS-propagates per-tenant status from the seed changes to all dependents.shared = allChanges − perTenant. Fully unit-testable with synthetic graphs.partitionModule({ moduleDir, seedObjects })— on-disk entry: parsepgpm.plan, index producers by extractingcreatesfrom each change's deploy SQL, resolve seed objects → the changes that create them, build AST edges (buildAstEdges), delegate to the core.Reuses existing
sliceprimitives (buildDependencyGraph,extractSqlFacts,buildAstEdges) — no new parsing.Soundness is explicit, not assumed
The partition is only as sound as the reference graph, so it surfaces what it cannot prove:
dynamic-sql: a change classified as shared runsEXECUTE— its real references are invisible, so sharing it can't be proven safe. Flagged, not silently trusted.unresolved-reference: a reference produced by no change in the plan (installed module / extension) — informational.unknown-seed: a requested seed change/object isn't in the module.By construction shared changes never depend on per-tenant ones (a test asserts the boundary is one-directional), so a per-tenant module can
requirethe shared module with no back-edge.Depends on the
LANGUAGE sqlclassifier fixSoundness requires seeing references inside
LANGUAGE sqlstring bodies (ubiquitous in pgpm), which the classifier previously treated as opaque. That's fixed upstream in constructive-io/pgsql-parser#320, published as@pgsql/transform@18.5.0; this PR bumps@pgpmjs/transformto^18.5.0and the on-disk test's tenant-reading function is aLANGUAGE sqlfunction that is correctly pulled per-tenant.Tests
pgpm/slice/__tests__/partition.test.ts— pure-core propagation,sharedDependencies/ one-directional boundary, dynamic-SQL and unknown-seed warnings, and an on-diskpartitionModulerun (seed table → itsLANGUAGE sqlreader is per-tenant, the pure helper + schema are shared) plus abuildAstEdgescross-check. Fullslicesuite: 40 passed;transformsuite 26 passed; typecheck + build clean.Follow-up
Wiring the partition into the apply/reuse path (emit the shared changes as their own generated module both instances
require) is the deferred use-case, tracked in constructive-planning#1298.Link to Devin session: https://app.devin.ai/sessions/025fb88043964fdbb335ac5e39df2478
Requested by: @pyramation