fix: preserve inherited lens limits in narrowing and windows - #13
Merged
Merged
Conversation
agreenspan
marked this pull request as draft
September 6, 2026 23:01
The where validation added in the previous commit built a synthetic Lens
anchored at the where's own model, so a bare `path` comparison ref — which
check() resolves at the ROOT context and applyLens injects unchanged into a
to-many grant — was gated at the related model instead of the lens anchor.
A legal grant like `Order.where = { contactEmail equals path: 'email' }`
(Customer.email) was rejected, and a ref to a related-only column was
accepted although it resolves to nothing at the root.
checkRule now exposes an internal `checkConditionAtVisit` that starts the
existing visit at (mapName, modelName, relPath) on the REAL parent policy:
`field` and `$.` refs resolve at the visit, a bare `path` at the anchor.
narrowing.ts validates every where through it — relation nodes at their
relPath, root at [], model defaults at the off-path visit plus each declared
parent visit (OFF_PATH moves to policy.ts and is shared with exposedSurface).
applyLens: `filterFirst` uses `hasWindow`, the compilers' notion of a window,
so an empty `orderBy` keeps the AND injection that still compiles.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
agreenspan
marked this pull request as ready for review
September 6, 2026 23:05
`true` compiles to `{}`, which Prisma reads as match-all only at the top
level and under AND. Inside OR Prisma drops the arm — `OR: [x, {}]` is just
`x` — so `{ any: [customerId = mateo, true] }` returned zero rows for a
customer scoped to sofia (verified against Prisma SQLite) while check()
passed every row. `NOT: {}` is likewise not NOT(true).
The logical builders now fold constants on compiled output instead of
nesting them: a true arm absorbs an OR, a false arm absorbs an AND, either
drops out of the other, and NOT of a constant is the other constant. This
covers constants that arrive indirectly too — `all: []`, `any: []`,
`atLeast 0`, and the bridge over-fetch sentinel, which under `any` now
over-fetches the disjunction instead of being dropped and under-fetching.
Match-nothing is Prisma's documented `{ OR: [] }` everywhere; the
`{ id: null } AND { id: { not: null } }` self-contradiction sentinel is gone.
Arms are still compiled exactly once, in order, before folding, so groupBy
step refs stay positional and no step is duplicated; an arm that folds away
leaves its step behind, which is executed and ignored. The bridge
short-circuit in if/then/else is unchanged — NOT of the over-fetch sentinel
must stay unknown, not become match-nothing.
Co-Authored-By: Claude Fable 5.1 <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.
Delegated lens authors could reference ancestor-hidden fields or enum values from nested/model-default/source
whereclauses even though the equivalent root clause was rejected. The old model-local check also rejected valid nested relation conditions and accepted nonexistent dotted-path tails.Validate these clauses with the existing lens-aware rule checker, anchored at the declaring model and parent path. Model defaults also respect the parent's explicit visits. Bare comparison references retain the real lens root; current-element references are checked at the declaring visit. This removes the separate condition-tree walker and preserves the ability to filter on fields hidden by the declaring layer itself.
Relation grants now run before window selection for windowed conditions, as they already did for
all. An excluded row can no longer occupy atakeslot and change anany,noneor aggregate answer. Relations inside an existing window filter also retain their scope. Unsupported conditionless shapes keep their existing fail-closed behavior; Prisma/SQL compilation still rejects windows it cannot preserve.Validation:
bun run checkpasses, including 1,278 tests. Regressions cover hidden fields/values, related-model validation, comparison references, path-specific restrictions, window displacement and compiler rejection. No compiler semantics or public API were added. The first consumer is the template marketing workbench's delegated lens authoring and execution pipeline.Also in this branch:
fix(toPrisma): fold boolean constants through AND/OR/NOTtruecompiles to{}, which Prisma reads as match-all only at the top level and underAND. InsideORPrisma drops the arm, so{ any: [customerId = mateo, true] }compiled toOR: [customerId, {}]and returned zero rows for a customer scoped to sofia whilecheck()passed every row (reproduced against real Prisma SQLite;NOT: {}is likewise ignored, not negated).The logical builders now fold constants on compiled output instead of nesting them: a true arm absorbs an
OR, a false arm absorbs anAND, either drops out of the other, andNOTof a constant is the other constant. This also covers constants that arrive indirectly (all: [],any: [],atLeast 0, and the bridge over-fetch sentinel, which underanynow over-fetches the disjunction instead of being dropped and under-fetching). Match-nothing is Prisma's documented{ OR: [] }everywhere; the{ id: null } AND { id: { not: null } }sentinel is gone. Arms are compiled exactly once, in order, before folding, so groupBy step refs stay positional and no step is duplicated. The bridge short-circuit inif/then/elseis unchanged.Validation:
bun run checkpasses (1,375 tests). Newtest/toPrisma.booleanFold.test.tscovers the reproducer, every constant position underAND/OR/if/then/else, a no-nested-constant invariant across 80 generated shapes, bridge-under-anyover-fetch, and groupBy step-state coherence. Twolens.bridgeexpectations that pinned the under-fetchingOR: [x, {}]shape were updated. A 14-rule differential against real Prisma 6 SQLite (check()vs executedtoPrismawhere) agrees on every row.