Skip to content

ft. ENG-26 — Implement sequence counter helpers for monotonic gap-free numbering#84

Merged
Connorbelez merged 3 commits intomainfrom
ENG-26
Mar 16, 2026
Merged

ft. ENG-26 — Implement sequence counter helpers for monotonic gap-free numbering#84
Connorbelez merged 3 commits intomainfrom
ENG-26

Conversation

@Connorbelez
Copy link
Copy Markdown
Owner

@Connorbelez Connorbelez commented Mar 16, 2026

Replace query-last-entry sequence generation with a singleton counter
document pattern. Every journal write now touches a dedicated counter
doc, creating an explicit OCC serialization point per the spec.

  • Add ledger_sequence_counters table to schema
  • Create convex/ledger/sequenceCounter.ts with initializeSequenceCounter
    (idempotent ledgerMutation) and getNextSequenceNumber (ConvexError on
    uninitialized)
  • Remove old nextSequenceNumber from internal.ts
  • Update mutations.ts and demo/ledger.ts to use new helper
  • Add 5 unit tests (init, idempotency, monotonic, gap-free, error)
  • Update all 45 existing ledger tests to initialize counter

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

Summary by Sourcery

Introduce a dedicated ledger sequence counter backed by a singleton table and wire all ledger writes to use it for monotonic, gap-free journal numbering.

New Features:

  • Add a ledger_sequence_counters table and sequenceCounter module to manage a singleton ledger sequence counter with explicit initialization and next-number helper.

Bug Fixes:

  • Prevent race conditions and gaps in ledger journal sequence numbers by replacing query-last-entry sequencing with a centralized counter document.

Tests:

  • Add focused unit tests for sequence counter initialization, idempotency, monotonicity, gap-freedom, and error-on-uninitialized, and update existing ledger tests to initialize the counter before mutations.

Summary by CodeRabbit

  • Tests

    • Added comprehensive test suite for ledger sequence counter initialization and monotonic number generation, ensuring sequence number consistency across operations.
  • Chores

    • Refactored internal sequence number generation mechanism for improved maintainability and reliability.
    • Added new sequence counter table to data schema.

…e numbering

Replace query-last-entry sequence generation with a singleton counter
document pattern. Every journal write now touches a dedicated counter
doc, creating an explicit OCC serialization point per the spec.

- Add ledger_sequence_counters table to schema
- Create convex/ledger/sequenceCounter.ts with initializeSequenceCounter
  (idempotent ledgerMutation) and getNextSequenceNumber (ConvexError on
  uninitialized)
- Remove old nextSequenceNumber from internal.ts
- Update mutations.ts and demo/ledger.ts to use new helper
- Add 5 unit tests (init, idempotency, monotonic, gap-free, error)
- Update all 45 existing ledger tests to initialize counter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@linear
Copy link
Copy Markdown

linear bot commented Mar 16, 2026

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 16, 2026

Warning

Rate limit exceeded

@Connorbelez has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 1 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cbfb7620-3d5a-44a0-9852-78241f52ba7d

📥 Commits

Reviewing files that changed from the base of the PR and between 5a5b53b and 79036e7.

📒 Files selected for processing (5)
  • convex/demo/ledger.ts
  • convex/ledger/__tests__/ledger.test.ts
  • convex/ledger/__tests__/sequenceCounter.test.ts
  • convex/ledger/migrations.ts
  • convex/ledger/sequenceCounter.ts
📝 Walkthrough

Walkthrough

The changes refactor ledger sequence number generation from a query-based approach to a dedicated singleton counter table. A new sequenceCounter.ts module is introduced with initialization and retrieval functions, the old nextSequenceNumber function is removed, and all call sites are updated to use the new implementation. Schema and test coverage are extended accordingly.

Changes

Cohort / File(s) Summary
Sequence Counter Module
convex/ledger/sequenceCounter.ts, convex/ledger/internal.ts (removed)
Introduces singleton counter in new module with initializeSequenceCounter mutation and getNextSequenceNumber function; removes legacy nextSequenceNumber query function.
API Migration
convex/demo/ledger.ts, convex/ledger/mutations.ts
Updates imports and function calls from nextSequenceNumber to getNextSequenceNumber from new sequenceCounter module.
Test Coverage
convex/ledger/__tests__/ledger.test.ts, convex/ledger/__tests__/sequenceCounter.test.ts
Adds initCounter helper to existing tests; introduces comprehensive new test suite validating counter initialization, idempotency, error handling, and monotonic sequence generation.
Schema & Infrastructure
convex/schema.ts, convex/seed/seedHelpers.ts
Adds ledger_sequence_counters table with name and value fields; minor formatting cleanup in seed helpers callback.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A counter now stands, clean and bright,
Where sequence numbers flow just right,
No more journals to hunt and peek,
A singleton strong, not frail or weak,
The ledger hops forward with all its might! 🌱

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: implementing sequence counter helpers for monotonic gap-free numbering, which aligns with the core objective of replacing query-based sequence generation with a singleton counter pattern.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ENG-26
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 16, 2026

Reviewer's Guide

Replaces query-based journal sequencing with a singleton ledger_sequence_counters table and helper module that provides an explicitly initialized, mutation-scoped, monotonic, gap-free sequence counter used by all ledger journal writes, with comprehensive tests and test harness updates to require counter initialization.

Sequence diagram for initializing the ledger sequence counter

sequenceDiagram
  actor Admin
  participant InitializeSequenceCounterMutation
  participant Db as ledger_sequence_counters

  Admin->>InitializeSequenceCounterMutation: call initializeSequenceCounter(ctx)
  InitializeSequenceCounterMutation->>Db: query by_name name=ledger_sequence
  Db-->>InitializeSequenceCounterMutation: existing or null
  alt counter_exists
    InitializeSequenceCounterMutation-->>Admin: return existing._id
  else counter_missing
    InitializeSequenceCounterMutation->>Db: insert name=ledger_sequence, value=0
    Db-->>InitializeSequenceCounterMutation: new_id
    InitializeSequenceCounterMutation-->>Admin: return new_id
  end
Loading

Sequence diagram for posting a ledger entry with monotonic gap-free numbering

sequenceDiagram
  actor Client
  participant LedgerMutation as postEntryInternal
  participant SequenceHelper as getNextSequenceNumber
  participant DbCounter as ledger_sequence_counters
  participant DbJournal as ledger_journal_entries

  Client->>LedgerMutation: call postEntryInternal(ctx, args)
  LedgerMutation->>SequenceHelper: getNextSequenceNumber(ctx)
  SequenceHelper->>DbCounter: query by_name name=ledger_sequence
  alt counter_not_initialized
    DbCounter-->>SequenceHelper: null
    SequenceHelper-->>LedgerMutation: throw ConvexError
    LedgerMutation-->>Client: error SEQUENCE_COUNTER_NOT_INITIALIZED
  else counter_initialized
    DbCounter-->>SequenceHelper: counter{name, value}
    SequenceHelper->>SequenceHelper: nextValue = value + 1
    SequenceHelper->>DbCounter: patch counter.value = nextValue
    DbCounter-->>SequenceHelper: ok
    SequenceHelper-->>LedgerMutation: nextValue
    LedgerMutation->>DbJournal: insert journal_entry(sequenceNumber=nextValue, ...)
    DbJournal-->>LedgerMutation: entry_id
    LedgerMutation-->>Client: success with entry_id
  end
Loading

ER diagram for ledger_sequence_counters and journal entries

erDiagram
  ledger_sequence_counters {
    string name
    bigint value
  }

  ledger_journal_entries {
    bigint sequenceNumber
    string entryType
    string description
    float amount
    string debitAccountId
    string creditAccountId
    int64 createdAt
  }

  ledger_sequence_counters ||--o{ ledger_journal_entries : assigns_sequence_numbers
Loading

Class diagram for ledger sequence counter helpers

classDiagram
  class LedgerSequenceCounterTable {
    string name
    bigint value
  }

  class InitializeSequenceCounterMutation {
    +initializeSequenceCounter(ctx)
  }

  class GetNextSequenceNumberHelper {
    +getNextSequenceNumber(ctx) bigint
  }

  class LedgerMutationsModule {
    +postEntryInternal(ctx, args)
  }

  class DemoLedgerModule {
    +postSeedEntry(ctx, args)
  }

  LedgerSequenceCounterTable <.. InitializeSequenceCounterMutation : queries_and_inserts
  LedgerSequenceCounterTable <.. GetNextSequenceNumberHelper : queries_and_patches
  GetNextSequenceNumberHelper <.. LedgerMutationsModule : uses
  GetNextSequenceNumberHelper <.. DemoLedgerModule : uses
Loading

File-Level Changes

Change Details Files
Introduce a singleton-backed ledger sequence counter and wire it into all journal writes.
  • Add ledger_sequence_counters table with name and value fields plus by_name index to the Convex schema.
  • Create ledger/sequenceCounter.ts exposing an idempotent initializeSequenceCounter ledgerMutation and a getNextSequenceNumber helper that increments a singleton counter document, throwing ConvexError when uninitialized.
  • Update ledger mutations and demo ledger seeding logic to use getNextSequenceNumber instead of computing the next sequence from the latest journal entry.
convex/schema.ts
convex/ledger/sequenceCounter.ts
convex/ledger/mutations.ts
convex/demo/ledger.ts
convex/ledger/internal.ts
Remove legacy query-last-entry sequencing implementation.
  • Delete nextSequenceNumber helper that queried ledger_journal_entries ordered by sequence index to derive the next sequence number.
  • Ensure all remaining call sites now use the new counter-based helper.
convex/ledger/internal.ts
convex/demo/ledger.ts
convex/ledger/mutations.ts
Expose the new sequence counter API module through the generated API surface.
  • Register ledger/sequenceCounter module in the generated api.d.ts so tests and application code can call initializeSequenceCounter via api.ledger.sequenceCounter.
  • Ensure type-safe access for both mutations and helpers in tests.
convex/_generated/api.d.ts
Add focused tests for the sequence counter behavior and make existing ledger tests explicitly initialize the counter.
  • Create sequenceCounter.test.ts covering initialization, idempotency, error on uninitialized usage, monotonic increments, and gap-free sequencing with verification of counter value.
  • Introduce a shared initCounter test helper in ledger.test.ts and call it at the start of all ledger integration tests that perform mutations so they don’t hit the uninitialized counter error.
convex/ledger/__tests__/sequenceCounter.test.ts
convex/ledger/__tests__/ledger.test.ts
Apply minor formatting/cleanup to existing code while touching related files.
  • Compact a withIndex chain in seedHelpers.ts without changing behavior for property lookups by postal code and address fields.
convex/seed/seedHelpers.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Connorbelez Connorbelez marked this pull request as ready for review March 16, 2026 23:05
Copilot AI review requested due to automatic review settings March 16, 2026 23:05
Copy link
Copy Markdown
Owner Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The new initializeSequenceCounter mutation is exposed as .public(), which means any caller with ledger access can (re)invoke it; consider restricting this to an admin-only or migration-only path so that the singleton counter cannot be tampered with from normal client flows.
  • Right now the sequence counter must be explicitly initialized and you wired that into the tests, but the demo/seed and any production bootstrap path still rely on getNextSequenceNumber and will now throw until initialization happens; consider adding a clear initialization hook (e.g. in seed/bootstrap or a migration) so non-test usage isn’t broken by the new ConvexError.
  • You added an initCounter helper local to ledger.test.ts and then call it in many tests; consider moving this into a shared test utility or wrapping it into createTestHarness/asLedgerUser to reduce repetition and ensure future tests can’t forget to initialize the counter.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `initializeSequenceCounter` mutation is exposed as `.public()`, which means any caller with ledger access can (re)invoke it; consider restricting this to an admin-only or migration-only path so that the singleton counter cannot be tampered with from normal client flows.
- Right now the sequence counter must be explicitly initialized and you wired that into the tests, but the demo/seed and any production bootstrap path still rely on `getNextSequenceNumber` and will now throw until initialization happens; consider adding a clear initialization hook (e.g. in seed/bootstrap or a migration) so non-test usage isn’t broken by the new ConvexError.
- You added an `initCounter` helper local to `ledger.test.ts` and then call it in many tests; consider moving this into a shared test utility or wrapping it into `createTestHarness`/`asLedgerUser` to reduce repetition and ensure future tests can’t forget to initialize the counter.

## Individual Comments

### Comment 1
<location path="convex/ledger/sequenceCounter.ts" line_range="11-20" />
<code_context>
+ * Bootstrap mutation: creates the singleton counter document with value 0.
+ * Idempotent — safe to call multiple times.
+ */
+export const initializeSequenceCounter = ledgerMutation
+	.handler(async (ctx) => {
+		const existing = await ctx.db
+			.query("ledger_sequence_counters")
+			.withIndex("by_name", (q) => q.eq("name", COUNTER_NAME))
+			.unique();
+
+		if (existing) {
+			return existing._id;
+		}
+
+		return ctx.db.insert("ledger_sequence_counters", {
+			name: COUNTER_NAME,
+			value: 0n,
</code_context>
<issue_to_address>
**issue (bug_risk):** The initialization mutation can race and create multiple counter documents, breaking `.unique()` assumptions.

Because this does a read followed by an insert, concurrent calls can both see `existing === null` and each insert a `ledger_sequence_counters` document with the same `name`. That would leave multiple matching documents and cause subsequent `.unique()` calls here and in `getNextSequenceNumber` to throw. Consider a race-safe pattern: e.g., attempt an insert and handle duplicate-key-like errors by re-reading, or enforce stronger uniqueness in the schema/logic and retry after a failed insert.
</issue_to_address>

### Comment 2
<location path="convex/ledger/__tests__/ledger.test.ts" line_range="927" />
<code_context>
 describe("Idempotency & Sequencing", () => {
 	it("T-065: same idempotencyKey returns existing entry, no double-post", async () => {
 		const t = createTestHarness();
+		await initCounter(t);
</code_context>
<issue_to_address>
**issue (testing):** Add an assertion that reusing the same idempotencyKey does not advance the sequence counter, to prove gap-free numbering under idempotent replays.

Since this test already covers `idempotencyKey` behavior, please also assert that reusing the same key does not change the journal entry’s `sequenceNumber`. For example, capture the `sequenceNumber` from the first call, invoke the same operation with the same `idempotencyKey`, and assert that the returned entry has the identical `sequenceNumber`. This will verify that idempotent retries do not introduce gaps in the new counter-based sequencing.
</issue_to_address>

### Comment 3
<location path="convex/ledger/__tests__/sequenceCounter.test.ts" line_range="56-76" />
<code_context>
+		expect(doc!.value).toBe(0n);
+	});
+
+	it("initializeSequenceCounter is idempotent", async () => {
+		const t = createTestHarness();
+		const auth = asLedgerUser(t);
+
+		const id1 = await auth.mutation(
+			api.ledger.sequenceCounter.initializeSequenceCounter,
+			{},
+		);
+		const id2 = await auth.mutation(
+			api.ledger.sequenceCounter.initializeSequenceCounter,
+			{},
+		);
+
+		expect(id1).toBe(id2);
+
+		// Verify only one document exists
+		const docs = await t.run(async (ctx) => {
+			return ctx.db.query("ledger_sequence_counters").collect();
+		});
+		expect(docs).toHaveLength(1);
+	});
+
</code_context>
<issue_to_address>
**suggestion (testing):** Extend the idempotency test to verify that `initializeSequenceCounter` does not reset an already-incremented counter value.

After the first initialization, trigger a mutation that calls `getNextSequenceNumber` to increment the counter, then invoke `initializeSequenceCounter` again and assert that the stored `value` remains the incremented value. This will help catch regressions where initialization might overwrite an existing sequence value.

```suggestion
	it("initializeSequenceCounter is idempotent", async () => {
		const t = createTestHarness();
		const auth = asLedgerUser(t);

		const id1 = await auth.mutation(
			api.ledger.sequenceCounter.initializeSequenceCounter,
			{},
		);

		// Increment the sequence counter after initialization
		await auth.mutation(
			api.ledger.sequenceCounter.getNextSequenceNumber,
			{},
		);

		const id2 = await auth.mutation(
			api.ledger.sequenceCounter.initializeSequenceCounter,
			{},
		);

		expect(id1).toBe(id2);

		// Verify only one document exists and that the value was not reset
		const docs = await t.run(async (ctx) => {
			return ctx.db.query("ledger_sequence_counters").collect();
		});
		expect(docs).toHaveLength(1);
		expect(docs[0]!.value).toBe(1n);
	});
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread convex/ledger/sequenceCounter.ts
Comment thread convex/ledger/__tests__/ledger.test.ts
Comment thread convex/ledger/__tests__/sequenceCounter.test.ts
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR replaces the previous query-last-entry sequence generation strategy with a singleton counter document in a new ledger_sequence_counters table, creating an explicit OCC serialization point for all ledger journal writes. The approach is architecturally sound for Convex — the atomic read-increment-patch within a mutation transaction is safe and gap-free. However, two deployment-critical issues must be resolved before merging to a non-empty environment.

  • Duplicate sequence numbers on existing deployments: initializeSequenceCounter unconditionally seeds the counter at value: 0n. Any environment that already has journal entries will have its counter start at 0, causing getNextSequenceNumber to re-issue sequence numbers that already exist (e.g., 1, 2, 3...). The initialization must read MAX(sequenceNumber) from existing entries and seed from there.
  • No automated initialization path: There is no migration, seed script, or on-deploy hook that calls initializeSequenceCounter. Deploying this code to any environment will immediately break every ledger mutation (mintMortgage, issueShares, transferShares, redeemShares, postEntry, burnMortgage, demo postSeedEntry) until an operator manually invokes the mutation.
  • Demo/seed functions not guarded: convex/demo/ledger.ts was updated to use getNextSequenceNumber but does not call initializeSequenceCounter, so seed/demo flows will fail without prior manual initialization.
  • Bootstrap mutation permission scope: initializeSequenceCounter is exposed as .public() under ledger:correct — a one-time infrastructure operation is arguably more appropriately restricted to an admin-only or internal mutation.
  • The overall design of the counter module, the schema definition, the placement of the increment after validation in postEntryInternal, and the test coverage are all well-executed.

Confidence Score: 2/5

  • Not safe to merge without addressing the counter initialization value — deploying to a non-empty environment will silently produce duplicate sequence numbers.
  • The core mechanism is well-designed and the tests are thorough, but there are two blocking deployment issues: the counter always initializes to 0 (causing duplicate sequence numbers on existing deployments), and there is no automated step to initialize the counter before the new code begins serving ledger mutations. Both issues can cause silent data corruption or a hard outage on deploy.
  • convex/ledger/sequenceCounter.ts requires the most attention — both the initialization value logic and the lack of a deployment automation path originate there.

Important Files Changed

Filename Overview
convex/ledger/sequenceCounter.ts New singleton counter module — core of this PR. Contains two critical issues: initializeSequenceCounter always seeds the counter at 0 (will cause duplicate sequence numbers on non-empty deployments), and there is no automated deployment path to call it (all ledger mutations will fail until manually initialized).
convex/ledger/mutations.ts Correctly swaps nextSequenceNumber for getNextSequenceNumber in postEntryInternal. Placement of the counter increment (after all validation, before insert) is sound — Convex's ACID transactions ensure the increment is rolled back if the subsequent insert fails, preserving gap-freedom.
convex/ledger/tests/sequenceCounter.test.ts Five focused tests covering initialization, idempotency, uninitialized error, monotonicity, and gap-freedom. Tests are well-structured and validate the counter document state directly. Does not test concurrent-mutation OCC behavior (acceptable since Convex serializes mutations).
convex/ledger/tests/ledger.test.ts All 45 existing tests correctly updated to call initCounter before any ledger mutation. The helper is consistently applied across every describe block.
convex/demo/ledger.ts Import updated from nextSequenceNumber to getNextSequenceNumber. However, the demo seed functions do not call initializeSequenceCounter, so any demo/seed invocation will fail with the uninitialized error unless the counter has been initialized externally beforehand.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ledgerMutation
    participant postEntryInternal
    participant sequenceCounter
    participant DB

    Note over Client,DB: Prerequisite: initializeSequenceCounter must be called first
    Client->>ledgerMutation: initializeSequenceCounter()
    ledgerMutation->>DB: query ledger_sequence_counters (by_name)
    alt counter exists
        DB-->>ledgerMutation: existing doc
        ledgerMutation-->>Client: existing._id (idempotent)
    else not initialized
        ledgerMutation->>DB: insert { name: "ledger_sequence", value: 0n }
        DB-->>ledgerMutation: new _id
        ledgerMutation-->>Client: new _id
    end

    Note over Client,DB: Normal ledger write flow
    Client->>ledgerMutation: mintMortgage / issueShares / etc.
    ledgerMutation->>postEntryInternal: postEntryInternal(ctx, args)
    postEntryInternal->>DB: idempotency check (by_idempotency)
    postEntryInternal->>DB: load debit + credit accounts
    postEntryInternal->>postEntryInternal: validateEntryType()
    postEntryInternal->>sequenceCounter: getNextSequenceNumber(ctx)
    sequenceCounter->>DB: query ledger_sequence_counters (by_name)
    alt counter missing
        sequenceCounter-->>postEntryInternal: throw ConvexError(NOT_INITIALIZED)
        postEntryInternal-->>Client: error
    else counter found
        sequenceCounter->>DB: patch counter { value: value + 1n }
        sequenceCounter-->>postEntryInternal: nextValue (bigint)
        postEntryInternal->>DB: insert ledger_journal_entries { sequenceNumber: nextValue }
        postEntryInternal->>DB: patch debitAccount cumulativeDebits
        postEntryInternal->>DB: patch creditAccount cumulativeCredits
        postEntryInternal-->>ledgerMutation: journalEntry doc
        ledgerMutation-->>Client: result
    end
Loading

Comments Outside Diff (3)

  1. convex/ledger/sequenceCounter.ts, line 22-25 (link)

    Counter always resets to 0 — breaks existing deployments

    initializeSequenceCounter hardcodes value: 0n, which means the first call after deploying this PR will set the counter to 0 regardless of what sequence numbers already exist in ledger_journal_entries. On any non-empty deployment, the subsequent getNextSequenceNumber calls will start issuing sequence numbers from 1 again, creating duplicate sequenceNumber values for existing entries.

    A safe initialization must seed the counter to the current maximum sequence number:

    const latest = await ctx.db
      .query("ledger_journal_entries")
      .withIndex("by_sequence")
      .order("desc")
      .first();
    
    return ctx.db.insert("ledger_sequence_counters", {
      name: COUNTER_NAME,
      value: latest ? latest.sequenceNumber : 0n,
    });

    Without this, deploying to any environment that already has journal entries will silently allow duplicate sequence numbers to be written, breaking the monotonicity and gap-free guarantees the PR is designed to enforce.

  2. convex/ledger/sequenceCounter.ts, line 11-27 (link)

    No automated deployment path — all ledger mutations will fail after deploy

    initializeSequenceCounter must be called manually before any ledger mutation can succeed. This PR adds no seed, migration, or run-on-deploy hook that calls it. Deploying this to production will immediately break every ledger mutation (mintMortgage, issueShares, transferShares, redeemShares, postEntry, burnMortgage, and the demo postSeedEntry) with "Ledger sequence counter not initialized" until an operator manually invokes the mutation.

    Consider adding an internal mutation or Convex migration (see Convex migrations docs) that is run as part of the deployment process, or at a minimum document the required manual step prominently and add a guard in the existing seed helpers.

  3. convex/ledger/sequenceCounter.ts, line 11-27 (link)

    Bootstrap operation exposed with ledger:correct permission

    initializeSequenceCounter is a one-time infrastructure bootstrap step, but it is exposed as a .public() mutation gated only by the ledger:correct permission (from ledgerMutation). Any user holding that permission can call it at any time. While idempotency makes repeated calls safe, the semantics of ledger:correct ("make corrections to ledger entries") are narrower than "bootstrap infrastructure." Consider restricting this to an adminMutation or an internal mutation called from a migration, to make the intended caller explicit and reduce the surface area of the public API.

Last reviewed commit: 5a5b53b

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements a dedicated singleton sequence counter document for ledger journal entry numbering, replacing the prior “query latest journal entry” approach to ensure monotonic, gap-free sequencing via an explicit OCC serialization point.

Changes:

  • Adds ledger_sequence_counters table to the Convex schema and a new convex/ledger/sequenceCounter.ts helper (init + next sequence).
  • Updates ledger journal write paths to use getNextSequenceNumber and removes the old nextSequenceNumber helper.
  • Adds new unit tests for the counter and updates existing ledger tests to initialize the counter.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
convex/seed/seedHelpers.ts Formatting-only change in an indexed query.
convex/schema.ts Adds ledger_sequence_counters table + index.
convex/ledger/sequenceCounter.ts New initialization mutation and next-sequence helper.
convex/ledger/mutations.ts Switches journal entry sequencing to the new helper.
convex/ledger/internal.ts Removes the old nextSequenceNumber implementation.
convex/ledger/tests/sequenceCounter.test.ts Adds focused tests for init/idempotency/monotonic/gap-free/error cases.
convex/ledger/tests/ledger.test.ts Introduces initCounter helper and calls it in tests before ledger mutations.
convex/demo/ledger.ts Demo seed journal writes now use the new counter-based sequencing.
convex/_generated/api.d.ts Updates generated API typings to include ledger/sequenceCounter.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread convex/ledger/sequenceCounter.ts
Comment thread convex/ledger/sequenceCounter.ts Outdated
Comment thread convex/demo/ledger.ts
Copy link
Copy Markdown
Owner Author

@greptile please review

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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)
convex/ledger/__tests__/ledger.test.ts (1)

43-50: Recommended: centralize initialized test harness creation.

initCounter is good, but repeating await initCounter(t) across many tests is easy to miss in future additions. Prefer a single helper that returns an already-initialized harness.

♻️ Suggested refactor
+async function createInitializedHarness() {
+	const t = createTestHarness();
+	await initCounter(t);
+	return { t, auth: asLedgerUser(t) };
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/ledger/__tests__/ledger.test.ts` around lines 43 - 50, Create a single
helper that returns a test harness with the ledger sequence counter already
initialized instead of calling initCounter(t) in every test; for example add a
function (e.g., createInitializedHarness) that calls createTestHarness(), then
runs
asLedgerUser(harness).mutation(api.ledger.sequenceCounter.initializeSequenceCounter,
{}) and returns the harness so tests can use it directly—update tests to
import/use createInitializedHarness (or modify createTestHarness to perform the
init) and remove repeated await initCounter(t) calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@convex/ledger/sequenceCounter.ts`:
- Around line 34-45: The getNextSequenceNumber function currently throws
SEQUENCE_COUNTER_NOT_INITIALIZED if the ledger_sequence_counters row is missing;
update it to automatically initialize the counter on first access or provide a
bootstrap migration/HTTP endpoint that calls initializeSequenceCounter.
Specifically, inside getNextSequenceNumber (and any callers in
convex/ledger/mutations.ts and convex/demo/ledger.ts) detect a missing counter
(ledger_sequence_counters with name COUNTER_NAME) and either invoke
initializeSequenceCounter to insert the initial row before proceeding or
document/implement a startup migration that ensures initializeSequenceCounter
runs during deployment; keep the ConvexError only for truly unrecoverable cases
and ensure COUNTER_NAME and ledger_sequence_counters are used consistently.

---

Nitpick comments:
In `@convex/ledger/__tests__/ledger.test.ts`:
- Around line 43-50: Create a single helper that returns a test harness with the
ledger sequence counter already initialized instead of calling initCounter(t) in
every test; for example add a function (e.g., createInitializedHarness) that
calls createTestHarness(), then runs
asLedgerUser(harness).mutation(api.ledger.sequenceCounter.initializeSequenceCounter,
{}) and returns the harness so tests can use it directly—update tests to
import/use createInitializedHarness (or modify createTestHarness to perform the
init) and remove repeated await initCounter(t) calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9a258703-9d89-488f-89b4-a79477cb932c

📥 Commits

Reviewing files that changed from the base of the PR and between c51c678 and 5a5b53b.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (8)
  • convex/demo/ledger.ts
  • convex/ledger/__tests__/ledger.test.ts
  • convex/ledger/__tests__/sequenceCounter.test.ts
  • convex/ledger/internal.ts
  • convex/ledger/mutations.ts
  • convex/ledger/sequenceCounter.ts
  • convex/schema.ts
  • convex/seed/seedHelpers.ts
💤 Files with no reviewable changes (1)
  • convex/ledger/internal.ts

Comment thread convex/ledger/sequenceCounter.ts
Connorbelez and others added 2 commits March 16, 2026 19:18
…rap migration

Addresses PR review feedback:
- Replace .unique() with .first() in both initializeSequenceCounter and
  getNextSequenceNumber to be resilient against duplicate documents
- Add bootstrapSequenceCounter admin mutation in migrations.ts as a
  production bootstrap path for fresh deployments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extend idempotency test to verify init doesn't reset incremented counter
- Add assertion that idempotent replay doesn't advance sequence number
- Add counter initialization to demo seedData for fresh DB support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Connorbelez Connorbelez merged commit bac21db into main Mar 16, 2026
1 of 3 checks passed
Connorbelez added a commit that referenced this pull request Apr 20, 2026
…e numbering (#84)

Replace query-last-entry sequence generation with a singleton counter
document pattern. Every journal write now touches a dedicated counter
doc, creating an explicit OCC serialization point per the spec.

- Add ledger_sequence_counters table to schema
- Create convex/ledger/sequenceCounter.ts with initializeSequenceCounter
  (idempotent ledgerMutation) and getNextSequenceNumber (ConvexError on
  uninitialized)
- Remove old nextSequenceNumber from internal.ts
- Update mutations.ts and demo/ledger.ts to use new helper
- Add 5 unit tests (init, idempotency, monotonic, gap-free, error)
- Update all 45 existing ledger tests to initialize counter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

## Summary by Sourcery

Introduce a dedicated ledger sequence counter backed by a singleton table and wire all ledger writes to use it for monotonic, gap-free journal numbering.

New Features:
- Add a ledger_sequence_counters table and sequenceCounter module to manage a singleton ledger sequence counter with explicit initialization and next-number helper.

Bug Fixes:
- Prevent race conditions and gaps in ledger journal sequence numbers by replacing query-last-entry sequencing with a centralized counter document.

Tests:
- Add focused unit tests for sequence counter initialization, idempotency, monotonicity, gap-freedom, and error-on-uninitialized, and update existing ledger tests to initialize the counter before mutations.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Tests**
  * Added comprehensive test suite for ledger sequence counter initialization and monotonic number generation, ensuring sequence number consistency across operations.

* **Chores**
  * Refactored internal sequence number generation mechanism for improved maintainability and reliability.
  * Added new sequence counter table to data schema.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

2 participants