Skip to content

perf(storage): conditional MATERIALIZED CTE fence for ListTransactions#1409

Draft
sylr wants to merge 4 commits into
mainfrom
perf/materialized-cte-fence-listtransactions
Draft

perf(storage): conditional MATERIALIZED CTE fence for ListTransactions#1409
sylr wants to merge 4 commits into
mainfrom
perf/materialized-cte-fence-listtransactions

Conversation

@sylr

@sylr sylr commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Problem

A selective wallet-history ListTransactions attached ORDER BY id DESC LIMIT n to the same select as a JSONB @> filter, so the planner chose an abort-early transactions_id_desc index walk that scanned ~2.3M rows (16.1s) to return 16. Statistics can't fix this (JSONB @> selectivity over-estimate + a value↔id correlation no stats object can express).

Fix

Wrap the filtered dataset in a MATERIALIZED CTE (optimization fence) and move ORDER BY id DESC LIMIT n to the outer select. The planner then uses the GIN BitmapOr over the filtered set and the LIMIT becomes a trivial top-N — ~7.7ms (~2,000×), verified on a prod read-replica. No schema change; the required GIN indexes already exist (migrations 43/51/52).

The fence is conditional — applied only when the filter contains a "needle" containment predicate (account/source/destination/metadata), via a new opt-in DatasetFencer interface implemented only by the transactions handler. Unfiltered/range-only lists keep the historical non-materialized shape, where abort-early is the faster plan.

How

  • DatasetFencer[Opts] opt-in interface; transactionsResourceHandler.ShouldFenceDataset implements the static needle rule.
  • Paginator split into ApplyCursorPredicate (keyset/offset predicate stays inside the fenced CTE) and ApplyWindow (LIMIT/OFFSET on the outer select, no ORDER BY). The existing outer ORDER BY remains the single order source and is applied before the window. Non-fence SQL is unchanged.
  • expand() gains a materialized bool (uses bun.NewWithQuery(...).Materialized()); GetOne/Count are unaffected (pass false).

Tests

  • Unit: table-driven ShouldFenceDataset (needle → fence; id/timestamp/reference/reverted → no fence).
  • Integration: SQL-shape via a bun.QueryHook asserting AS MATERIALIZED present for account/metadata filters and absent for unfiltered/timestamp-only; multi-page next/previous traversal under the fence; effectiveVolumes under the fence. Existing TestTransactionsList (every filtered case now runs through the fence) still passes.

Known limitation (correctness-safe)

A negated containment (NOT metadata @> {x}) or a non-selective needle value (account="world") may fence without benefit, but never changes the result. Deliberately deferred per a static-vs-cost-probe trade-off.

A selective wallet-history ListTransactions attached ORDER BY id DESC
LIMIT n to the same select as a JSONB @> filter, so the planner chose an
abort-early transactions_id_desc walk that scanned ~2.3M rows (16.1s) to
return 16. Wrapping the filtered dataset in a MATERIALIZED CTE and moving
ORDER BY + LIMIT to the outer select lets the planner pick the GIN
BitmapOr over the filtered set (~7.7ms, verified on prod read-replica).

The fence is applied conditionally, only when the filter contains a
"needle" containment predicate (account/source/destination/metadata),
via the new opt-in DatasetFencer interface implemented by the
transactions handler. Unfiltered/range-only lists keep the historical
non-materialized shape, where abort-early is the faster plan.

The Paginator contract is split into ApplyCursorPredicate (keyset/offset
predicate that stays inside the fence) and ApplyWindow (LIMIT/OFFSET that
move to the outer select); the existing outer ORDER BY remains the single
order source and is applied before the window. Non-fence SQL is unchanged.

Constraint: JSONB @> selectivity + value<->id correlation can't be fixed by statistics or extended stats; pg_hint_plan not installed
Constraint: GIN indexes on sources/destinations/metadata are schema migrations (43/51/52), present on all migrated clusters
Rejected: Blanket-wrap every list in a MATERIALIZED CTE | regresses broad/unfiltered lists (materializes millions then sorts)
Rejected: Runtime cost probe to detect selectivity | static filter-shape decision is simpler and sufficient per findings doc
Confidence: high
Scope-risk: moderate
Directive: ApplyWindow must NOT emit ORDER BY — the caller applies the qualified outer ORDER BY first; do not fold ordering back in
Directive: The fenced dataset CTE must never carry an inner LIMIT/OFFSET, or the abort-early walk re-triggers
Directive: Do not extend the moved-LIMIT pattern to a fan-out (non-1:1) expand without re-checking; effectiveVolumes is 1:1 per tx
Not-tested: negated-only metadata filter (NOT metadata @>) and non-selective needle values (account="world") fence without benefit but never change results
@sylr sylr requested a review from a team as a code owner June 11, 2026 14:35
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Separates cursor predicate application from LIMIT/OFFSET in the paginator contract, adds a DatasetFencer interface to optionally emit a MATERIALIZED dataset CTE, rewires repository pagination to choose fenced vs unfenced SQL shapes, implements transaction fencing heuristics, and adds tests and mocks validating behavior.

Changes

Dataset Fencing for Paginated Queries

Layer / File(s) Summary
Paginator contract and implementations
internal/storage/common/paginator.go, internal/storage/common/paginator_column.go, internal/storage/common/paginator_offset.go
Paginator adds ApplyCursorPredicate and ApplyWindow. columnPaginator extracts cursor boundary logic into ApplyCursorPredicate and simplifies ApplyWindow to apply LIMIT. OffsetPaginator adds a no-op ApplyCursorPredicate and delegates windowing.
Repository fencing infrastructure and query paths
internal/storage/common/resource.go
Adds DatasetFencer and resolveBuildContext, refactors buildFilteredDataset/expand to accept a build context, and rewrites GetOne, Count, and Paginate to choose fenced (cursor predicate inside MATERIALIZED CTE, window after outer ORDER BY) vs unfenced paths while emitting a single final qualified ORDER BY.
Transaction fencing implementation
internal/storage/ledger/resource_transactions.go
transactionsResourceHandler implements ShouldFenceDataset via helper logic that fences when filters target needle fields (account, source, destination, metadata).
Fencing behavior tests and mocks
internal/storage/ledger/resource_transactions_fence_test.go, internal/storage/ledger/resource_transactions_test.go, internal/controller/ledger/mocks_test.go
Adds sqlRecorder and integration tests asserting MATERIALIZED CTE emission and fenced cursor pagination behavior, unit tests for the fencing decision, generated MockDatasetFencer, and supporting test helpers.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Paginate
    participant resolveBuildContext
    participant DatasetFencer
    participant buildFilteredDataset
    participant expand
    participant Paginator
    
    Caller->>Paginate: Paginate(query)
    Paginate->>resolveBuildContext: resolve and validate filters
    resolveBuildContext-->>Paginate: BuildContext
    Paginate->>DatasetFencer: ShouldFenceDataset(context)?
    alt Fenced Path
        DatasetFencer-->>Paginate: true
        Paginate->>buildFilteredDataset: build with context
        buildFilteredDataset-->>Paginate: filtered dataset query
        Paginate->>expand: expand(WITH MATERIALIZED)
        expand-->>Paginate: materialized CTE
        Paginate->>Paginator: ApplyCursorPredicate
        Paginator-->>Paginate: cursor-filtered query
        Paginate->>Paginator: ApplyWindow
        Paginator-->>Paginate: windowed query
        Paginate->>Paginate: Final qualified ORDER BY
    else Unfenced Path
        DatasetFencer-->>Paginate: false
        Paginate->>buildFilteredDataset: build with context
        buildFilteredDataset-->>Paginate: filtered dataset query
        Paginate->>expand: expand(non-materialized)
        expand-->>Paginate: CTE
        Paginate->>Paginator: Paginate (ORDER BY + ApplyWindow)
        Paginator-->>Paginate: ordered, windowed query
        Paginate->>Paginate: Final qualified ORDER BY
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • gfyrag

Poem

🐰 Hopping through queries, I nibble the fence,
Cursor and window now split, quite intense.
MATERIALIZED hedges keep needle-fields tight,
Pages step forward, then backward in light.
A rabbit applauds this SQL delight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing a conditional MATERIALIZED CTE optimization for ListTransactions queries.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the problem, solution, implementation details, tests, and known limitations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/materialized-cte-fence-listtransactions

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 53.57143% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.79%. Comparing base (140ed9b) to head (94ccd01).

Files with missing lines Patch % Lines
internal/storage/common/resource.go 54.54% 8 Missing and 12 partials ⚠️
internal/storage/common/paginator_column.go 51.85% 3 Missing and 10 partials ⚠️
internal/storage/ledger/resource_transactions.go 55.55% 2 Missing and 2 partials ⚠️
internal/storage/common/paginator_offset.go 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1409      +/-   ##
==========================================
- Coverage   80.55%   78.79%   -1.77%     
==========================================
  Files         206      206              
  Lines       11293    11382      +89     
==========================================
- Hits         9097     8968     -129     
- Misses       1646     1696      +50     
- Partials      550      718     +168     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/storage/common/resource.go (1)

411-444: 💤 Low value

Clarify the dual ORDER BY application in the unfenced path.

In the unfenced path (lines 419-426), paginator.Paginate applies cursor predicate + ORDER BY + LIMIT inside the dataset, then line 433-435 applies ORDER BY again on the outer select after expand joins. While this is intentional (per the comment on lines 431-432), the double ordering could be confusing or wasteful if the inner ORDER BY is redundant.

The comment states "This is the single source of the final ORDER BY for both paths," but in the unfenced path there are actually two ORDER BY clauses. Consider clarifying whether the inner ORDER BY is required for abort-early optimization or if the outer one alone would suffice.

💡 Suggested clarification

Add a comment explaining why both ORDER BYs are needed in the unfenced path:

 	// cannot introduce an ambiguous column name (e.g. a future expand that also
 	// selects "id"). No expand today conflicts, but this makes it safe by construction.
+	// NOTE: In the unfenced path, ORDER BY is applied twice: once inside the dataset CTE
+	// (by Paginate, combined with LIMIT for abort-early index walk) and once here on the
+	// outer select (to ensure correct ordering after any expand LEFT JOINs). The inner
+	// ORDER BY is required for the query planner's abort-early optimization.
 	// This is the single source of the final ORDER BY for both paths; in the fenced path it
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/storage/common/resource.go` around lines 411 - 444, The comment is
unclear about why the unfenced path has two ORDER BYs (paginator.Paginate
applies an inner ORDER BY and later finalQuery.Order applies an outer ORDER BY
using paginator.OrderExpression); update the code comments to explicitly state
why the inner ORDER BY from paginator.Paginate is necessary (e.g., for correct
cursor predicate evaluation or abort-early optimization inside the dataset) and
that the outer finalQuery.Order (using paginator.OrderExpression and applied
after r.expand) is the canonical final ordering; reference the symbols
paginator.Paginate, paginator.OrderExpression, finalQuery.Order,
paginator.ApplyWindow and r.expand so reviewers can find the logic and make it
clear that the inner ORDER BY is intentional and not redundant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/storage/ledger/resource_transactions_test.go`:
- Around line 37-41: The test case named "metadata needle" uses a map value for
the "metadata" filter but ResolveFilter expects a string key for metadata (see
ResolveFilter and the metadata property check). Update the test's filters entry
from map[string]any{"wallet_id": "x"} to just the string "wallet_id" so the
filter value matches the expected semantics (i.e., filters:
map[string][]any{"metadata": {"wallet_id"}}) and keep the rest of the test logic
unchanged.

---

Nitpick comments:
In `@internal/storage/common/resource.go`:
- Around line 411-444: The comment is unclear about why the unfenced path has
two ORDER BYs (paginator.Paginate applies an inner ORDER BY and later
finalQuery.Order applies an outer ORDER BY using paginator.OrderExpression);
update the code comments to explicitly state why the inner ORDER BY from
paginator.Paginate is necessary (e.g., for correct cursor predicate evaluation
or abort-early optimization inside the dataset) and that the outer
finalQuery.Order (using paginator.OrderExpression and applied after r.expand) is
the canonical final ordering; reference the symbols paginator.Paginate,
paginator.OrderExpression, finalQuery.Order, paginator.ApplyWindow and r.expand
so reviewers can find the logic and make it clear that the inner ORDER BY is
intentional and not redundant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3766c71-10aa-448e-9b16-bf8936cd0b9f

📥 Commits

Reviewing files that changed from the base of the PR and between 140ed9b and 801a561.

📒 Files selected for processing (7)
  • internal/storage/common/paginator.go
  • internal/storage/common/paginator_column.go
  • internal/storage/common/paginator_offset.go
  • internal/storage/common/resource.go
  • internal/storage/ledger/resource_transactions.go
  • internal/storage/ledger/resource_transactions_fence_test.go
  • internal/storage/ledger/resource_transactions_test.go

Comment thread internal/storage/ledger/resource_transactions_test.go
The new DatasetFencer interface in storage/common/resource.go is picked up
by `go generate` (mocks_test.go is generated from resource.go), so the
generated mock must be committed or the `Dirty` CI check fails.

Directive: mocks_test.go is generated from internal/storage/common/resource.go — re-run `just generate` (mockgen) after changing interfaces there, do not hand-edit
…case

Addresses a CodeRabbit review note: the metadata filter value should be a
string key, not a map. The fence decision only checks filter presence, so
behavior is unchanged — this is for semantic accuracy of the test fixture.
@sylr

sylr commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Example: how a selective ListTransactions is rewritten

Take a wallet-history lookup filtered by account — GET /v2/.../transactions?query={"$match":{"account":"users:alice"}} with pageSize=15. The account filter compiles to a GIN containment ("needle") predicate, so ShouldFenceDataset returns true.

Before this patch (current main shape — ORDER BY … LIMIT sits inside the dataset CTE):

WITH "dataset" AS (
  SELECT *
  FROM (
    SELECT ledger, id, timestamp, reference, inserted_at, updated_at, postings,
           sources, destinations, sources_arrays, destinations_arrays, template,
           metadata, transactions.reverted_at AS reverted_at
    FROM "_default".transactions
    WHERE (ledger = 'ledger0')
  ) dataset
  WHERE (sources @> '["users:alice"]' OR destinations @> '["users:alice"]')
  ORDER BY id DESC          -- ← attached to the same select as the filter
  LIMIT 16                  -- ← pageSize + 1 overfetch; triggers abort-early
)
SELECT * FROM "dataset" ORDER BY "dataset"."id" DESC;

→ the planner assumes ORDER BY id DESC LIMIT 16 will short-circuit and picks an abort-early Index Scan using transactions_id_desc, applying the @> filter per row. When the matching rows are sparse and old, it walks most of the table (Rows Removed by Filter in the millions).

With this patch (account is a needle → fenced):

WITH "dataset" AS MATERIALIZED (          -- ← (1) optimization fence
  SELECT *
  FROM (
    SELECT ledger, id, timestamp, reference, inserted_at, updated_at, postings,
           sources, destinations, sources_arrays, destinations_arrays, template,
           metadata, transactions.reverted_at AS reverted_at
    FROM "_default".transactions
    WHERE (ledger = 'ledger0')
  ) dataset
  WHERE (sources @> '["users:alice"]' OR destinations @> '["users:alice"]')
)                                          -- ← (2) no ORDER BY / LIMIT inside
SELECT * FROM "dataset"
ORDER BY "dataset"."id" DESC
LIMIT 16;                                  -- ← (3) ORDER BY + LIMIT on the outer select

→ fenced, the CTE is "the filtered set", so the planner uses the GIN BitmapOr over sources/destinations and the outer LIMIT becomes a trivial top-N heapsort over the (small) result. On the prod repro this took the same query from ~16.1 s → ~7.7 ms.

The only two mechanical changes are: AS MATERIALIZED on the CTE, and moving ORDER BY … LIMIT pageSize+1 from inside the CTE to the outer SELECT.

A few details preserved:

  • PIT (timestamp <= $pit) and the keyset cursor (AND id <= $cursor on next/previous pages) stay inside the fenced CTE — they're part of the filtered set. Only ORDER BY + LIMIT move out.
  • A metadata[...]/source=/destination= filter is rewritten identically (it's also a needle → metadata @> {…} / sources @> …).
  • An unfiltered or range-only (id/timestamp) list is not fenced — it keeps the Before shape exactly, where abort-early is the faster plan.

Comment thread internal/storage/common/resource.go Outdated
Addresses @flemzord's review on #1409: in the fenced path the `dataset` CTE
held the entire materialized filtered set, and the `effectiveVolumes` expand
(`transactions_id in (select id from dataset)`) therefore aggregated over every
matching transaction to return a single page — linear in account history, even
for a selective (but busy) account, not just the documented account="world" case.

Restructure the fenced shape so the MATERIALIZED fence and the page window are
separate, nested CTEs:

  WITH dataset AS (
    WITH filtered AS MATERIALIZED (<filter + PIT + keyset>)
    SELECT * FROM filtered ORDER BY id <dir> LIMIT pageSize+1
  )
  SELECT * FROM dataset LEFT JOIN <expands on "select id from dataset"> ...
  ORDER BY dataset.id <dir>

The LIMIT stays outside the *materialized* CTE (so the planner still evaluates
`filtered` once via the GIN BitmapOr and the page is a cheap top-N over it), but
now lives inside the `dataset` CTE the expands reference — so expand work is
bounded to the page again, restoring the implicit "dataset = rows returned"
contract. The expand() helper drops its now-unused `materialized` parameter.

Constraint: effectiveVolumes expand references "select id from dataset" — the page LIMIT must be inside that CTE or the expand scans the full filtered set
Rejected: Keep LIMIT on the outermost select | expands then aggregate over the whole filtered set (the bug this fixes)
Confidence: high
Scope-risk: moderate
Directive: do not move the page ORDER BY/LIMIT back to the outer select — it must stay inside the "dataset" CTE so expands see only the page
sylr added a commit that referenced this pull request Jun 11, 2026
Addresses @flemzord's review on #1409: in the fenced path the `dataset` CTE
held the entire materialized filtered set, and the `effectiveVolumes` expand
(`transactions_id in (select id from dataset)`) therefore aggregated over every
matching transaction to return a single page — linear in account history, even
for a selective (but busy) account, not just the documented account="world" case.

Restructure the fenced shape so the MATERIALIZED fence and the page window are
separate, nested CTEs:

  WITH dataset AS (
    WITH filtered AS MATERIALIZED (<filter + PIT + keyset>)
    SELECT * FROM filtered ORDER BY id <dir> LIMIT pageSize+1
  )
  SELECT * FROM dataset LEFT JOIN <expands on "select id from dataset"> ...
  ORDER BY dataset.id <dir>

The LIMIT stays outside the *materialized* CTE (so the planner still evaluates
`filtered` once via the GIN BitmapOr and the page is a cheap top-N over it), but
now lives inside the `dataset` CTE the expands reference — so expand work is
bounded to the page again, restoring the implicit "dataset = rows returned"
contract. The expand() helper drops its now-unused `materialized` parameter.

Constraint: effectiveVolumes expand references "select id from dataset" — the page LIMIT must be inside that CTE or the expand scans the full filtered set
Rejected: Keep LIMIT on the outermost select | expands then aggregate over the whole filtered set (the bug this fixes)
Confidence: high
Scope-risk: moderate
Directive: do not move the page ORDER BY/LIMIT back to the outer select — it must stay inside the "dataset" CTE so expands see only the page

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/storage/common/resource.go (1)

116-127: ⚡ Quick win

Update the fencing docs to match the nested-CTE shape.

Lines 120-126 and Lines 393-395 still describe the older “ORDER BY + LIMIT on the outer select” plan. The code now pages in the intermediate dataset select after filtered AS MATERIALIZED and before expand(...), so these comments no longer match the contract that keeps expands page-bounded.

Also applies to: 393-395

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/storage/common/resource.go` around lines 116 - 127, Update the
DatasetFencer docs to reflect the new nested-CTE paging shape: change the
description around DatasetFencer and ShouldFenceDataset (currently describing
"ORDER BY + LIMIT on the outer select") to explain that the code now
materializes an inner filtered CTE (e.g., filtered AS MATERIALIZED), then pages
from the intermediate dataset select before expands, so the ORDER BY + LIMIT
apply inside the nested CTE that bounds the dataset prior to expand(...) rather
than on the outermost select; update the explanatory example SQL and the text at
the DatasetFencer comment (and the similar text near lines referenced for the
other occurrence) to describe the nested-CTE form and that expands remain
page-bounded.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/storage/common/resource.go`:
- Around line 116-127: Update the DatasetFencer docs to reflect the new
nested-CTE paging shape: change the description around DatasetFencer and
ShouldFenceDataset (currently describing "ORDER BY + LIMIT on the outer select")
to explain that the code now materializes an inner filtered CTE (e.g., filtered
AS MATERIALIZED), then pages from the intermediate dataset select before
expands, so the ORDER BY + LIMIT apply inside the nested CTE that bounds the
dataset prior to expand(...) rather than on the outermost select; update the
explanatory example SQL and the text at the DatasetFencer comment (and the
similar text near lines referenced for the other occurrence) to describe the
nested-CTE form and that expands remain page-bounded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 78846a6d-dcd9-4239-8414-41062bc1cdc4

📥 Commits

Reviewing files that changed from the base of the PR and between 4e32a4f and 94ccd01.

📒 Files selected for processing (2)
  • internal/storage/common/resource.go
  • internal/storage/ledger/resource_transactions_fence_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/storage/ledger/resource_transactions_fence_test.go

@sylr sylr marked this pull request as draft June 11, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants