Skip to content

The 50-row cross-schema page has no runtime caller — so the #1216/#1326/#1960 family's behavioural tests no longer demonstrate their claim #2048

Closed

Description

@rbuergi

Found while fixing #2011, and deliberately not fixed there — it is a different defect and the fix
is a decision, not a repair.

The paging fan-out has no src/ caller

PostgreSqlCrossSchemaQueryProvider has two QueryAcrossSchemasAsync shapes:

  • the search_across_schemas form, which clips at DefaultFanOutLimit (50) when the caller
    states no limit — the behaviour every "unpinned query answers with a PAGE" note describes; and
  • the table form (GenerateCrossSchemaSelectQuery), which applies no default limit at all.

PostgreSqlPartitionedMeshQuery.EnumerateFanOutAsync — the runtime path for every unpinned
query — always takes the table form. Grepping the whole tree, the paging form's only callers are
tests. The same split exists on Snowflake.

Why that matters

The 50-row page is the documented mechanism behind #1216, #1326 and #1960, and the remedy those
issues shipped is MeshQueryRequest.Complete(). If the runtime cannot reach the paging form, then:

  1. The behavioural tests for that family no longer demonstrate their claim. Measured while
    fixing The security fold's Role/GroupMembership queries are unpinned scope:subtree — a group-derived permission vanishes silently once the mesh outgrows a page #2011: removing .Complete() from UserIdentityCache.DirectoryQuery leaves
    UserDirectoryCompletenessTests.AUserOutsideTheMostRecentlyModifiedPage_StillResolvesTheirProfile
    passing — only the cheap property assertion (DirectoryQuery.Limit == NoLimit) fails. Its
    doc comment still explains, at length, a truncation the test can no longer produce.
    CompleteEnumerationFanOutTests is honest about calling the provider directly, but it too pins a
    form nothing runs.
  2. Every Complete() call site is now unfalsifiable — it may be load-bearing or it may be
    ceremony, and no test can tell the difference.
  3. The hazard is one routing change away from returning. The paging form is still on
    ICrossSchemaQueryProvider, still implemented on both backends, and the --- three cases ---
    comment that explains its limit handling reads as live documentation of the current runtime.

What would close it

A decision, then the code to match:

  • If the paging form is dead, retire it (and its Snowflake twin), and rewrite the family's tests
    so they pin what the runtime actually does — an unpinned query returning everything — rather than a
    truncation only a test can reach.
  • If it is meant to be live, wire it and say when: which shapes should be paged and which
    enumerated. The Complete() opt-in then becomes meaningful again.

Either way the tests should stop describing a mechanism the runtime does not execute.

Not affected

#2011's fix stands on its own reasoning: the security fold declares completeness so it is
indifferent to which form answers it, and that PR's paging assertion is made at the provider
seam
, where it is falsifiable, precisely because an end-to-end test would reproduce nothing today.

Related: #1216, #1326, #1960, #2011.

🤖 Generated with Claude Code

Activity

  1. rbuergi commented on Aug 25, 2026

    @rbuergi
    ContributorAuthor

    Confirmed on origin/main @ 201a16aa7 — the claim holds precisely as written.

    • The 50-row paging form is PostgreSqlCrossSchemaQueryProvider.cs:289.
    • DefaultFanOutLimit is referenced only at :373 and :467 — i.e. inside that method only.
    • The runtime path takes the table form instead: PostgreSqlPartitionedMeshQuery.cs:908 → :632 → GenerateCrossSchemaSelectQuery at :678, which applies no default limit.
    • The only src/ callers of any overload are the two table-form ones (PG :908, Snowflake :436).

    So the behavioural tests in the #1216/#1326/#1960 family do still exercise the paging code, but nothing in the running system reaches it — which is exactly this issue's point: the tests no longer demonstrate the claim they were written to defend.

    Worth adding, since it bears on the family: the same fan-out limit was a leading hypothesis for the #2031/#2185 flake family, and that hypothesis was weakened tonight — the identical UserIdentityCache.WhenDetermined timeout signature reproduces in MeshWeaver.Hosting.Monolith.Test, whose base uses AddInMemoryPersistence() and therefore has no PostgreSQL cross-schema path at all. (That reproduction was taken on a loaded machine, so treat it as suggestive rather than settled — see my note on #2185.)

  2. rbuergi commented on Aug 25, 2026

    @rbuergi
    ContributorAuthor

    Decision: retire the paging form. #2282 does it.

    Re-measured first, and the premise holds on ed4588222: the paging shape is PostgreSqlCrossSchemaQueryProvider.cs:289; DefaultFanOutLimit is referenced only at :373/:467, both inside it; the runtime path takes the table form (PostgreSqlPartitionedMeshQuery.cs:908 → the 6-arg overload → GenerateCrossSchemaSelectQuery, which emits LIMIT only for Limit is > 0). Same split on Snowflake. Grepping src, test, memex, samples, content and samples/*/Data: the paging form's only callers are tests.

    Why retire rather than wire

    1. The 50-row default is the defect, not a feature to restore. Batch bake's source discovery is truncated by the default query limit (50) — 169/237 types got zero sources in production #1216, GitSync reports "Skipped (0 nodes)" while a space is behind (_Activity/import-{hash} fingerprint), and force-prune eats Release records (AddIfUnderPrefix case-sensitivity) #1326 and fix: the portal read its user directory as a search page, so most viewers lost their time zone (#1936) #1960 are three filings of the same silent truncation. The runtime's actual behaviour — an unpinned query returns everything — is the one those issues' remedy argued for. Wiring the paging form would reintroduce, deliberately, the thing three issues were opened to remove.
    2. It was a second access-control implementation. The search_across_schemas stored function gates access in plpgsql; the table form gates it in the generated per-schema user_effective_permissions clause. Two implementations of one logical query, one of them unexercised, is where a security fix lands on the wrong copy — and the security-fold tests were already split across the two, so half of them exercised code no request could reach.
    3. If a bound is ever wanted, it belongs on the ONE shape the runtime executes and it has to be loud. That is a separate decision with Batch bake's source discovery is truncated by the default query limit (50) — 169/237 types got zero sources in production #1216/GitSync reports "Skipped (0 nodes)" while a space is behind (_Activity/import-{hash} fingerprint), and force-prune eats Release records (AddIfUnderPrefix case-sensitivity) #1326's evidence against it, not a reason to keep a dead second path warm.

    Deleting it surfaced three live problems — which is the argument in miniature

    • 🚨 Mesh-wide keyset paging had never worked in production. Mesh-wide (cross-partition) queries: sort:path errors 42703 and path:> keyset filter silently matches nothing #2186 fixed sort:path erroring 42703 column "path" does not exist — on the stored function. The runtime shape still 42703'd. CrossPartitionKeysetPagingTests went red the instant it was pointed at the overload the runtime takes. Both SQL generators now project path on every union branch.
    • The fan-out timing instrumentation had produced not one line in production — LogFanOutTiming's only production caller was the paging form. Moved onto the table form, in a finally so an abandoned enumeration still reports.
    • Complete() meant different things on the two backends — Snowflake dropped NoLimit (is > 0) where PG propagates it. Aligned.

    On Complete() and the family's tests

    Complete() stays, with its contract restated exactly rather than quietly overclaimed: it is the declaration that a read is an enumeration, and it overrides a limit that reaches the request (notably a limit:N carried in a query STRING, which is how a permission read actually gets truncated — SecurityQueries.Enumeration replaces such a token). It is no longer credited with defeating a truncation the runtime does not perform. UserDirectoryCompletenessTests now says outright that removing .Complete() leaves its behavioural test passing and only its property test failing, and why that is the correct division of labour.

    public.search_across_schemas stays in the schema — an older replica mid-rollout still calls it.

  3. added a commit that references this issue on Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions