Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(store/v2): Add historical queries support for IAVL v1 to v2 migration #23685

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

avorylli
Copy link

@avorylli avorylli commented Feb 12, 2025

Problem:

  • During migration from IAVL v1 to v2, access to historical data before migration height is lost

Solution:

  • Store migration height
  • Route queries to old/new tree based on version and migration point
  • Add configuration option to enable/disable historical queries
  • Zero overhead when disabled
  • Full test coverage

Features:

  • Historical data access preserved during migration
  • Configurable by node operators
  • Automatic query routing based on version
  • Proper error handling and nil checks

Summary by CodeRabbit

  • New Features

    • Introduced historical querying capability to access pre-migration data when enabled.
    • Enhanced the migration process to record migration heights for improved continuity.
  • Documentation

    • Updated instructions for node operators with clear guidance on configuring and using historical queries.
  • Tests

    • Added tests to validate the behavior of historical queries and migration height management.

@avorylli avorylli requested a review from a team as a code owner February 12, 2025 20:16
Copy link
Contributor

coderabbitai bot commented Feb 12, 2025

📝 Walkthrough

Walkthrough

This pull request introduces historical queries functionality to the IAVL tree migration process. The documentation is updated with a new "Historical Queries After Migration" section that explains configuration, operational details, and node operator instructions. The IAVL configuration is enhanced by adding an EnableHistoricalQueries field, and a new HistoricalQuerier is implemented to handle queries based on migration heights. Additionally, the migration manager now records the migration height in the database, and new tests have been added to validate historical querying and migration height functionality.

Changes

File(s) Change Summary
store/v2/README.md Added a "Historical Queries After Migration" section with "Configuration", "How It Works", and "Node Operator Instructions" subsections to document new migration and historical query features.
store/v2/commitment/iavl/config.go, store/v2/commitment/iavl/tree.go, store/v2/commitment/iavl/historical_queries.go Enhanced IAVL configuration and functionality: added EnableHistoricalQueries to Config, updated default CacheSize, integrated HistoricalQuerier for handling queries based on migration height, and introduced a new method to set the old reader.
store/v2/commitment/iavl/historical_queries_test.go, store/v2/migration/migration_height_test.go Introduced unit tests for validating historical queries and migration height functionality, including error handling and configuration validations.
store/v2/migration/manager.go, store/v2/migration/migration_height.go Updated migration manager to store the migration height using new helper functions, enabling recording and retrieval of migration height from the database.

Sequence Diagram(s)

sequenceDiagram
    participant C as Client
    participant T as IavlTree
    participant HQ as HistoricalQuerier
    participant R as OldReader
    participant N as NewTree
    C->>T: Query (version, key)
    T->>HQ: Forward query request
    alt Version < migration height and historical queries enabled
        HQ->>R: Retrieve value from legacy tree
        R-->>HQ: Return old data
    else
        HQ->>N: Retrieve value from current tree
        N-->>HQ: Return current data
    end
    HQ-->>T: Return fetched data
    T-->>C: Deliver query result
Loading
sequenceDiagram
    participant M as Migration Manager
    participant DB as Database
    M->>DB: StoreMigrationHeight(height)
    DB-->>M: Acknowledge or error response
Loading

Possibly related PRs

Suggested labels

C:Store, C:x/upgrade, C:x/auth, C:x/distribution, C:x/mint, C:x/slashing, C:x/evidence, C:x/gov, C:x/accounts, C:x/accounts/lockup, C:x/accounts/multisig, C:x/accounts/base, C:server/v2, C:core/testing

Suggested reviewers

  • kocubinski
  • sontrinh16
  • lucaslopezf
  • testinginprod
  • hieuvubk
  • julienrbrt
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
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: 3

🧹 Nitpick comments (6)
store/v2/commitment/iavl/tree.go (1)

55-60: Reinitializing querier after setting oldReader is sensible.
This design recalculates the querier only when needed. Keep in mind this may add overhead on the first subsequent call to Get or Iterator. If performance is an issue, consider re-creating the querier here directly.

store/v2/migration/migration_height.go (1)

10-13: Consider using a more descriptive key format.

The current key format m/height could be made more descriptive to avoid potential collisions with other migration-related keys.

-	migrationHeightKey = "m/height"
+	migrationHeightKey = "migration/store_v2/height"
store/v2/README.md (4)

72-75: Add package import to the configuration example.

The configuration example would be clearer with the package import statement.

+import "github.com/cosmos/cosmos-sdk/store/v2/commitment/iavl"
+
 cfg := iavl.DefaultConfig()
 cfg.EnableHistoricalQueries = true

81-83: Use consistent list style with asterisks.

For better maintainability and consistency with Markdown standards, use asterisks instead of dashes for unordered lists.

-   - If version < migration height: uses old tree (if historical queries enabled)
-   - If version >= migration height: uses new tree
-   - If historical queries disabled: always uses new tree
+   * If version < migration height: uses old tree (if historical queries enabled)
+   * If version >= migration height: uses new tree
+   * If historical queries disabled: always uses new tree
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


82-82: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


83-83: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


88-89: Use consistent list style and add specific preservation instructions.

Use asterisks for list items and provide more specific instructions about data preservation.

-   - Ensure old tree data is preserved
-   - Configure `EnableHistoricalQueries` if historical access is needed
+   * Ensure old tree data is preserved by backing up the database directory
+   * Configure `EnableHistoricalQueries` in app.toml if historical access is needed
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

88-88: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


89-89: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


92-93: Use consistent list style and clarify configuration location.

Use asterisks for list items and specify where to configure the setting.

-   - Historical queries will automatically use appropriate tree based on height
-   - Can disable historical queries later by setting `EnableHistoricalQueries = false`
+   * Historical queries will automatically use appropriate tree based on height
+   * Can disable historical queries later by setting `EnableHistoricalQueries = false` in app.toml
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

92-92: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


93-93: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f072eca and c7540d0.

📒 Files selected for processing (8)
  • store/v2/README.md (1 hunks)
  • store/v2/commitment/iavl/config.go (1 hunks)
  • store/v2/commitment/iavl/historical_queries.go (1 hunks)
  • store/v2/commitment/iavl/historical_queries_test.go (1 hunks)
  • store/v2/commitment/iavl/tree.go (2 hunks)
  • store/v2/migration/manager.go (1 hunks)
  • store/v2/migration/migration_height.go (1 hunks)
  • store/v2/migration/migration_height_test.go (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
`**/*.go`: Review the Golang code for conformity with the Ub...

**/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

  • store/v2/commitment/iavl/historical_queries_test.go
  • store/v2/migration/migration_height.go
  • store/v2/commitment/iavl/tree.go
  • store/v2/commitment/iavl/config.go
  • store/v2/commitment/iavl/historical_queries.go
  • store/v2/migration/migration_height_test.go
  • store/v2/migration/manager.go
`**/*_test.go`: "Assess the unit test code assessing suffici...

**/*_test.go: "Assess the unit test code assessing sufficient code coverage for the changes associated in the pull request"

  • store/v2/commitment/iavl/historical_queries_test.go
  • store/v2/migration/migration_height_test.go
`**/*.md`: "Assess the documentation for misspellings, gramm...

**/*.md: "Assess the documentation for misspellings, grammatical errors, missing documentation and correctness"

  • store/v2/README.md
🪛 golangci-lint (1.62.2)
store/v2/commitment/iavl/historical_queries_test.go

21-21: m.Called undefined (type *mockReader has no field or method Called)

(typecheck)


27-27: m.Called undefined (type *mockReader has no field or method Called)

(typecheck)


50-50: m.On undefined (type *mockReader has no field or method On)

(typecheck)


84-84: cannot use mockOldReader (variable of type *mockReader) as "cosmossdk.io/core/store".Reader value in argument to NewHistoricalQuerier: *mockReader does not implement "cosmossdk.io/core/store".Reader (wrong type for method Get)
have Get(uint64, []byte) ([]byte, error)
want Get([]byte) ([]byte, error)

(typecheck)


105-105: mockOldReader.AssertExpectations undefined (type *mockReader has no field or method AssertExpectations)

(typecheck)


107-107: mockOldReader.AssertNotCalled undefined (type *mockReader has no field or method AssertNotCalled)

(typecheck)


148-148: mockOldReader.On undefined (type *mockReader has no field or method On)

(typecheck)

store/v2/commitment/iavl/historical_queries.go

57-57: too many arguments in call to h.oldReader.Get
have (uint64, []byte)
want ([]byte)

(typecheck)


77-77: too many arguments in call to h.oldReader.Iterator
have (uint64, []byte, []byte, bool)
want ([]byte, []byte)

(typecheck)

🪛 markdownlint-cli2 (0.17.2)
store/v2/README.md

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


82-82: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


83-83: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


88-88: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


89-89: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


92-92: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


93-93: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Summary
🔇 Additional comments (16)
store/v2/commitment/iavl/historical_queries.go (3)

1-14: Documentation is clearly written.
The high-level explanation of historical queries is straightforward and helps maintainers understand the new functionality quickly. No issues here.


15-19: Struct naming and field usage look consistent.
The fields in the HistoricalQuerier are well-named and capture all the necessary store references. No further concerns.


25-41: Basic parameter validation is a good practice.
The constructor checks for nil parameters and returns default configurations. This is solid defensive programming.

store/v2/commitment/iavl/tree.go (4)

27-30: Adding references for db, oldReader, querier, and config is clear.
These fields neatly group all relevant components for historical queries and IAVL configuration.


34-53: Consider validating db before passing it to the iavl.NewMutableTree call.
db is assumed not to be nil but presently not validated, and a nil db might trigger a panic.

 func NewIavlTree(db corestore.KVStoreWithBatch, logger log.Logger, cfg *Config) (*IavlTree, error) {
+    if db == nil {
+        return nil, fmt.Errorf("db cannot be nil")
+    }
     if cfg == nil {
         cfg = DefaultConfig()

143-153: Lazy initialization of querier is fine but be mindful of concurrency.
Repeatedly creating the querier could be costly if called often. However, since the tree is not marked as concurrency-safe, the potential data race is likely not a concern here.


155-165: Iterator logic is consistent with the Get method.
The approach to lazily create the querier if none is present matches the pattern in Get. No further issues.

store/v2/migration/migration_height_test.go (1)

1-29: Straightforward coverage of migration height logic.
The test thoroughly checks initial height and after storing a new height, ensuring correct readback. Consider future expansions for negative or boundary scenarios (e.g., storing zero or extremely large heights).

store/v2/migration/migration_height.go (2)

15-20: LGTM! Clean and efficient implementation.

The function correctly handles the conversion of uint64 to bytes using big-endian encoding and stores it in the database.


22-33: LGTM! Robust error handling and nil checks.

The function properly handles:

  • Database errors with descriptive error messages
  • Nil case for when no migration has occurred
  • Byte decoding using big-endian
store/v2/commitment/iavl/config.go (2)

4-18: LGTM! Well-documented configuration options.

The Config struct is thoroughly documented with clear explanations for each field, including the new EnableHistoricalQueries option.


20-28: Verify the significant increase in default cache size.

The default cache size has been increased from 1000 to 1000000, which is a 1000x increase. While this might improve performance, it could significantly impact memory usage.

Please confirm:

  1. The memory impact of this change
  2. Whether this value has been performance tested
  3. If this is the optimal default for most use cases
store/v2/migration/manager.go (1)

108-112: LGTM! Proper error handling for migration height storage.

The implementation correctly:

  • Stores the migration height after successful migration
  • Uses descriptive error message with proper error wrapping
store/v2/commitment/iavl/historical_queries_test.go (3)

31-168: LGTM! Comprehensive test coverage for historical queries.

The test cases thoroughly cover:

  • Historical queries before and after migration
  • Enabled and disabled configurations
  • Mock expectations validation
🧰 Tools
🪛 golangci-lint (1.62.2)

50-50: m.On undefined (type *mockReader has no field or method On)

(typecheck)


84-84: cannot use mockOldReader (variable of type *mockReader) as "cosmossdk.io/core/store".Reader value in argument to NewHistoricalQuerier: *mockReader does not implement "cosmossdk.io/core/store".Reader (wrong type for method Get)
have Get(uint64, []byte) ([]byte, error)
want Get([]byte) ([]byte, error)

(typecheck)


105-105: mockOldReader.AssertExpectations undefined (type *mockReader has no field or method AssertExpectations)

(typecheck)


107-107: mockOldReader.AssertNotCalled undefined (type *mockReader has no field or method AssertNotCalled)

(typecheck)


148-148: mockOldReader.On undefined (type *mockReader has no field or method On)

(typecheck)


175-189: LGTM! Good error handling validation.

The test properly validates error cases when migration height is not set.


191-250: LGTM! Thorough validation of querier initialization.

The test cases cover all edge cases for querier initialization:

  • Nil tree validation
  • Nil database validation
  • Nil configuration handling

Comment on lines +46 to +61
func (h *HistoricalQuerier) Get(version uint64, key []byte) ([]byte, error) {
if !h.config.EnableHistoricalQueries {
return h.tree.Get(version, key)
}

migrationHeight, err := migration.GetMigrationHeight(h.db)
if err != nil {
return nil, fmt.Errorf("failed to get migration height: %w", err)
}

if version < migrationHeight && h.oldReader != nil {
return h.oldReader.Get(version, key)
}

return h.tree.Get(version, key)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix interface mismatch in oldReader calls.
Static analysis indicates that h.oldReader.Get(...) may not match the signature of store.Reader, which typically expects only Get(key []byte) ([]byte, error) without a version parameter. This code will fail to compile or will lead to a runtime panic if oldReader does not support version-based APIs.

Below is a possible fix if the old reader does not support version-based queries. You could remove the version parameter in the call (and logic), or employ a specialized versioned interface:

-if version < migrationHeight && h.oldReader != nil {
-    return h.oldReader.Get(version, key)
+if version < migrationHeight && h.oldReader != nil {
+    // Consider using a specialized interface or remove version argument:
+    return h.oldReader.Get(key)
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (h *HistoricalQuerier) Get(version uint64, key []byte) ([]byte, error) {
if !h.config.EnableHistoricalQueries {
return h.tree.Get(version, key)
}
migrationHeight, err := migration.GetMigrationHeight(h.db)
if err != nil {
return nil, fmt.Errorf("failed to get migration height: %w", err)
}
if version < migrationHeight && h.oldReader != nil {
return h.oldReader.Get(version, key)
}
return h.tree.Get(version, key)
}
func (h *HistoricalQuerier) Get(version uint64, key []byte) ([]byte, error) {
if !h.config.EnableHistoricalQueries {
return h.tree.Get(version, key)
}
migrationHeight, err := migration.GetMigrationHeight(h.db)
if err != nil {
return nil, fmt.Errorf("failed to get migration height: %w", err)
}
if version < migrationHeight && h.oldReader != nil {
// Consider using a specialized interface or remove version argument:
return h.oldReader.Get(key)
}
return h.tree.Get(version, key)
}
🧰 Tools
🪛 golangci-lint (1.62.2)

57-57: too many arguments in call to h.oldReader.Get
have (uint64, []byte)
want ([]byte)

(typecheck)

Comment on lines +63 to +81
// Iterator returns an iterator over a domain of keys at the specified version.
// If historical queries are enabled and the version is before migration height,
// it will use the old reader. Otherwise, it uses the current tree.
func (h *HistoricalQuerier) Iterator(version uint64, start, end []byte, ascending bool) (store.Iterator, error) {
if !h.config.EnableHistoricalQueries {
return h.tree.Iterator(version, start, end, ascending)
}

migrationHeight, err := migration.GetMigrationHeight(h.db)
if err != nil {
return nil, fmt.Errorf("failed to get migration height: %w", err)
}

if version < migrationHeight && h.oldReader != nil {
return h.oldReader.Iterator(version, start, end, ascending)
}

return h.tree.Iterator(version, start, end, ascending)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Same interface mismatch for oldReader in the iterator call.
As with Get, static analysis shows a similar discrepancy in oldReader.Iterator(...). The standard store.Reader interface typically has a method signature like Iterator(start, end []byte), lacking a version argument.

Below is a possible fix if your old reader is not version aware:

-if version < migrationHeight && h.oldReader != nil {
-    return h.oldReader.Iterator(version, start, end, ascending)
+if version < migrationHeight && h.oldReader != nil {
+    // Use an interface that supports version-based iteration or remove version argument:
+    return h.oldReader.Iterator(start, end)
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Iterator returns an iterator over a domain of keys at the specified version.
// If historical queries are enabled and the version is before migration height,
// it will use the old reader. Otherwise, it uses the current tree.
func (h *HistoricalQuerier) Iterator(version uint64, start, end []byte, ascending bool) (store.Iterator, error) {
if !h.config.EnableHistoricalQueries {
return h.tree.Iterator(version, start, end, ascending)
}
migrationHeight, err := migration.GetMigrationHeight(h.db)
if err != nil {
return nil, fmt.Errorf("failed to get migration height: %w", err)
}
if version < migrationHeight && h.oldReader != nil {
return h.oldReader.Iterator(version, start, end, ascending)
}
return h.tree.Iterator(version, start, end, ascending)
}
// Iterator returns an iterator over a domain of keys at the specified version.
// If historical queries are enabled and the version is before migration height,
// it will use the old reader. Otherwise, it uses the current tree.
func (h *HistoricalQuerier) Iterator(version uint64, start, end []byte, ascending bool) (store.Iterator, error) {
if !h.config.EnableHistoricalQueries {
return h.tree.Iterator(version, start, end, ascending)
}
migrationHeight, err := migration.GetMigrationHeight(h.db)
if err != nil {
return nil, fmt.Errorf("failed to get migration height: %w", err)
}
if version < migrationHeight && h.oldReader != nil {
// Use an interface that supports version-based iteration or remove version argument:
return h.oldReader.Iterator(start, end)
}
return h.tree.Iterator(version, start, end, ascending)
}
🧰 Tools
🪛 golangci-lint (1.62.2)

77-77: too many arguments in call to h.oldReader.Iterator
have (uint64, []byte, []byte, bool)
want ([]byte, []byte)

(typecheck)

Comment on lines +14 to +29
// mockReader implements store.Reader for testing purposes
type mockReader struct {
mock.Mock
}

// Get implements store.Reader
func (m *mockReader) Get(version uint64, key []byte) ([]byte, error) {
args := m.Called(version, key)
return args.Get(0).([]byte), args.Error(1)
}

// Iterator implements store.Reader
func (m *mockReader) Iterator(version uint64, start, end []byte, ascending bool) (store.Iterator, error) {
args := m.Called(version, start, end, ascending)
return args.Get(0).(store.Iterator), args.Error(1)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the mock implementation to match the store.Reader interface.

The mockReader implementation doesn't match the store.Reader interface signature.

-func (m *mockReader) Get(version uint64, key []byte) ([]byte, error) {
+func (m *mockReader) Get(key []byte) ([]byte, error) {
 	args := m.Called(version, key)
 	return args.Get(0).([]byte), args.Error(1)
 }

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 golangci-lint (1.62.2)

21-21: m.Called undefined (type *mockReader has no field or method Called)

(typecheck)


27-27: m.Called undefined (type *mockReader has no field or method Called)

(typecheck)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant