Skip to content

feat(slice): partition a module into shared vs per-tenant changes - #1515

Merged
pyramation merged 2 commits into
mainfrom
feat/pgpm-partition-shared-objects
Jul 30, 2026
Merged

feat(slice): partition a module into shared vs per-tenant changes#1515
pyramation merged 2 commits into
mainfrom
feat/pgpm-partition-shared-objects

Conversation

@pyramation

@pyramation pyramation commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

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 slice package already builds from @pgsql/transform's classifyStatements: 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 would require.

partitionModule({
  moduleDir,
  seedObjects: [{ schema: 'catalog', name: 'products' }]  // the per-tenant table
})
// => {
//   perTenant: { …/tables/products, …/functions/product_slug },  // reads products
//   shared:    { …/schema, …/functions/slugify },                // pure helper
//   sharedDependencies: { '…/functions/product_slug' -> { '…/functions/slugify', '…/schema' } },
//   warnings: [ … ]
// }

Design

partition.ts in pgpm/slice, two layers:

  • partitionChanges({ graph, astEdges, seeds }) — pure, I/O-free core. Builds a dependents map (reverse of declared requires ∪ 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: parse pgpm.plan, index producers by extracting creates from each change's deploy SQL, resolve seed objects → the changes that create them, build AST edges (buildAstEdges), delegate to the core.

Reuses existing slice primitives (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 runs EXECUTE — 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 require the shared module with no back-edge.

Depends on the LANGUAGE sql classifier fix

Soundness requires seeing references inside LANGUAGE sql string 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/transform to ^18.5.0 and the on-disk test's tenant-reading function is a LANGUAGE sql function 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-disk partitionModule run (seed table → its LANGUAGE sql reader is per-tenant, the pure helper + schema are shared) plus a buildAstEdges cross-check. Full slice suite: 40 passed; transform suite 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

@pyramation pyramation self-assigned this Jul 29, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit d44074c into main Jul 30, 2026
16 checks passed
@pyramation
pyramation deleted the feat/pgpm-partition-shared-objects branch July 30, 2026 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant