Skip to content

Store account_id fields as BYTEA#488

Open
aditya1702 wants to merge 32 commits intodb-indexesfrom
accountid-bytea-txns
Open

Store account_id fields as BYTEA#488
aditya1702 wants to merge 32 commits intodb-indexesfrom
accountid-bytea-txns

Conversation

@aditya1702
Copy link
Contributor

What

[TODO: Short statement about what is changing.]

Why

[TODO: Why this change is being made. Include any context required to understand the why.]

Known limitations

[TODO or N/A]

Issue that this PR addresses

[TODO: Attach the link to the GitHub issue or task. Include the priority of the task here in addition to the link.]

Checklist

PR Structure

  • It is not possible to break this PR down into smaller PRs.
  • This PR does not mix refactoring changes with feature changes.
  • This PR's title starts with name of package that is most changed in the PR, or all if the changes are broad or impact many packages.

Thoroughness

  • This PR adds tests for the new functionality or fixes.
  • All updated queries have been tested (refer to this check if the data set returned by the updated query is expected to be same as the original one).

Release

  • This is not a breaking change.
  • This is ready to be tested in development.
  • The new functionality is gated with a feature flag if this is not ready for production.

- Add StellarAddress type with sql.Scanner/driver.Valuer for automatic
  conversion between string addresses (G.../C...) and 33-byte BYTEA
- Update accounts table: stellar_address TEXT -> BYTEA
- Update transactions_accounts table: account_id TEXT -> BYTEA
- Use StellarAddress type for Get, Insert, Delete operations
- Convert []string to [][]byte for BatchGetByIDs ANY() clause
- Handle mixed BYTEA/VARCHAR in IsAccountFeeBumpEligible query
- Convert addresses to []byte for BatchInsert UNNEST($11::bytea[])
- Use raw []byte instead of pgtype.Text for BatchCopy
- Pass StellarAddress type which implements driver.Valuer for auto-conversion
- Test nil, valid, and error cases for both Scan and Value
- Test roundtrip conversion preserves address
- Fix IsAccountFeeBumpEligible to use separate EXISTS checks instead of UNION
  (avoids type mismatch between BYTEA and VARCHAR columns)
- Update test fixtures to use types.StellarAddress for BYTEA columns
- Use types.StellarAddress for scanning account_id from transactions_accounts
This is the first step in converting the operations_accounts table
to use BYTEA storage for account IDs, matching the pattern already
used by transactions_accounts.
All account_id columns now use BYTEA storage format, so the string
case handling for TEXT columns is no longer needed.
All account_id columns now use BYTEA format, so the conditional
conversion logic is no longer needed. Always use AddressBytea.
Both operations_accounts and transactions_accounts now use BYTEA
for account_id, so the field is no longer needed in the config.
Convert addresses to BYTEA format using AddressBytea.Value() when
copying to operations_accounts, matching the transactions pattern.
Convert accountAddr to BYTEA format in HasOperationForAccount query
since operations_accounts.account_id is now BYTEA.
- operations_test.go: Use AddressBytea type for scanning account_id
- accounts_test.go: Convert address to BYTEA in INSERT statement
- test_utils.go: Use AddressBytea directly (already has Value() method)
- Change SQL query to use UNNEST($9::bytea[]) instead of text
- Convert addresses to []byte using AddressBytea.Value()
- Use AddressBytea type for scanning account_id in both BatchInsert
  and BatchCopy test verification
- Use AddressBytea when inserting test accounts
Update migration to use BYTEA type for 7 account_id columns:
- account_id
- signer_account_id, spender_account_id
- sponsored_account_id, sponsor_account_id
- deployer_account_id, funder_account_id
Helper function to convert nullable address strings to bytes for BYTEA
insert operations. Returns nil for invalid/empty addresses.
Update the StateChange struct to use AddressBytea type for account_id
field to match the BYTEA database column type.
Convert 7 account_id columns from text arrays to bytea arrays:
- account_id (required) uses AddressBytea.Value()
- 6 nullable columns use pgtypeBytesFromNullStringAddress helper
Convert 7 account_id columns from pgtype.Text to raw []byte:
- account_id (required) uses AddressBytea.Value()
- 6 nullable columns use pgtypeBytesFromNullStringAddress helper
Convert accountAddress parameter to AddressBytea for BYTEA column query.
@aditya1702 aditya1702 changed the title accounts, transactions_accounts: Store account_id as BYTEA Store account_id fields as BYTEA Feb 5, 2026
Copy link

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

This pull request converts storage of account_id fields from TEXT to BYTEA in the database. The change optimizes storage and query performance for Stellar addresses by storing them in their native 33-byte binary format (1 version byte + 32 raw key bytes) rather than as 56-character StrKey strings.

Changes:

  • Introduced AddressBytea and NullAddressBytea types with SQL Scanner/Valuer interfaces for encoding/decoding between StrKey strings and BYTEA storage
  • Updated database migrations to change account_id columns from TEXT to BYTEA in accounts, transactions_accounts, operations_accounts, and state_changes tables
  • Modified data access layer to convert addresses to/from BYTEA when inserting or querying
  • Updated tests to use valid Stellar addresses generated from keypairs instead of hardcoded placeholder strings

Reviewed changes

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

Show a summary per file
File Description
internal/indexer/types/types.go Added AddressBytea and NullAddressBytea types with Scan/Value implementations for BYTEA conversion
internal/indexer/types/types_test.go Added comprehensive tests for AddressBytea Scan/Value/String methods and roundtrip conversion
internal/db/migrations/*.sql Updated CREATE TABLE statements to use BYTEA for all account_id columns
internal/data/accounts.go Updated queries to convert string addresses to BYTEA; refactored IsAccountFeeBumpEligible to use OR instead of UNION
internal/data/transactions.go Added address-to-BYTEA conversion for transactions_accounts link table insertions
internal/data/operations.go Added address-to-BYTEA conversion for operations_accounts link table insertions
internal/data/statechanges.go Added conversion for account_id and nullable address fields to BYTEA
internal/data/query_utils.go Added pgtypeBytesFromNullAddressBytea helper for nullable address conversions
internal/utils/sql.go Added NullAddressBytea helper function for creating nullable address values
internal/services/ingest_test.go Replaced hardcoded test addresses with valid addresses from keypairs
internal/serve/graphql/resolvers/*.go Updated resolvers to convert AddressBytea to string for GraphQL responses
internal/indexer/*.go Updated to handle AddressBytea type conversions
internal/integrationtests/infrastructure/backfill_helpers.go Updated queries to use AddressBytea for account address parameters

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

Comment on lines +32 to +40
// Test addresses generated from valid keypairs for use in tests.
// These are deterministic seeds to ensure consistent test addresses.
var (
testKP1 = keypair.MustRandom()
testKP2 = keypair.MustRandom()
testKP3 = keypair.MustRandom()
testAddr1 = testKP1.Address()
testAddr2 = testKP2.Address()
testAddrUnreg = testKP3.Address()
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The comment says "These are deterministic seeds to ensure consistent test addresses" but keypair.MustRandom() generates random keypairs, not deterministic ones. This means test addresses will be different on each test run, which could affect test reproducibility. Consider using keypair.MustParseFull() with fixed seed strings if deterministic addresses are needed, or update the comment to accurately reflect that these are randomly generated.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +52
// sharedTestAccountAddress is a fixed test address used by tests that rely on setupDB.
// It's generated once and reused to ensure test data consistency.
var sharedTestAccountAddress = keypair.MustRandom().Address()
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The comment says "It's generated once and reused to ensure test data consistency" but keypair.MustRandom() generates a random keypair. While it's generated once per test run (as a package-level variable), it will be different between test runs. Consider using a fixed test address if true consistency across runs is needed, or clarify that the comment refers to consistency within a single test run.

Copilot uses AI. Check for mistakes.
if err != nil {
return nil, fmt.Errorf("converting account_id: %w", err)
}
accountIDBytes[i] = addrBytes.([]byte)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Potential panic if addrBytes is nil. While the AddressBytea.Value() method returns nil for empty strings, the code unconditionally performs a type assertion addrBytes.([]byte) without checking if addrBytes is nil first. For required account_id fields, this should not happen in practice since valid state changes should have non-empty account IDs. However, consider adding a nil check or validation to make the code more defensive, or add a comment explaining why this case is impossible.

Suggested change
accountIDBytes[i] = addrBytes.([]byte)
if addrBytes == nil {
return nil, fmt.Errorf("converting account_id: got nil BYTEA for required field")
}
accountIDByteSlice, ok := addrBytes.([]byte)
if !ok {
return nil, fmt.Errorf("converting account_id: unexpected type %T, want []byte", addrBytes)
}
accountIDBytes[i] = accountIDByteSlice

Copilot uses AI. Check for mistakes.
if err != nil {
return nil, fmt.Errorf("converting address %s to bytes: %w", address, err)
}
stellarAddressBytes = append(stellarAddressBytes, addrBytes.([]byte))
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Potential panic if addrBytes is nil. The code performs an unconditional type assertion addrBytes.([]byte) without checking if addrBytes is nil. While AddressBytea.Value() only returns nil for empty strings (which shouldn't occur for valid addresses), consider adding a nil check or validation to make the code more defensive.

Suggested change
stellarAddressBytes = append(stellarAddressBytes, addrBytes.([]byte))
if addrBytes == nil {
return nil, fmt.Errorf("converting address %s to bytes: got nil value", address)
}
addrByteSlice, ok := addrBytes.([]byte)
if !ok {
return nil, fmt.Errorf("converting address %s to bytes: unexpected type %T", address, addrBytes)
}
stellarAddressBytes = append(stellarAddressBytes, addrByteSlice)

Copilot uses AI. Check for mistakes.
Comment on lines +309 to +313
addrBytes, err := types.AddressBytea(address).Value()
if err != nil {
return nil, fmt.Errorf("converting address %s to bytes: %w", address, err)
}
stellarAddressBytes = append(stellarAddressBytes, addrBytes.([]byte))
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Potential panic if addrBytes is nil. The code performs an unconditional type assertion addrBytes.([]byte) without checking if addrBytes is nil. While AddressBytea.Value() only returns nil for empty strings (which shouldn't occur for valid addresses), consider adding a nil check or validation to make the code more defensive.

Suggested change
addrBytes, err := types.AddressBytea(address).Value()
if err != nil {
return nil, fmt.Errorf("converting address %s to bytes: %w", address, err)
}
stellarAddressBytes = append(stellarAddressBytes, addrBytes.([]byte))
addrBytesValue, err := types.AddressBytea(address).Value()
if err != nil {
return nil, fmt.Errorf("converting address %s to bytes: %w", address, err)
}
addrBytes, ok := addrBytesValue.([]byte)
if !ok || addrBytes == nil {
return nil, fmt.Errorf("converting address %s to bytes: unexpected value %T", address, addrBytesValue)
}
stellarAddressBytes = append(stellarAddressBytes, addrBytes)

Copilot uses AI. Check for mistakes.
if err != nil {
return 0, fmt.Errorf("converting address %s to bytes: %w", addr, err)
}
taRows = append(taRows, []any{toIDPgtype, addrBytes})
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Missing nil check before appending addrBytes. If AddressBytea.Value() returns nil for an empty address, this would append a nil value to taRows, which might work correctly for BYTEA columns. However, for clarity and to prevent potential issues, consider either validating that addresses are non-empty before conversion, or explicitly handling the nil case.

Suggested change
taRows = append(taRows, []any{toIDPgtype, addrBytes})
if addrBytes == nil {
taRows = append(taRows, []any{toIDPgtype, nil})
} else {
taRows = append(taRows, []any{toIDPgtype, addrBytes})
}

Copilot uses AI. Check for mistakes.
@@ -1,7 +1,7 @@
-- +migrate Up
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The PR description contains unfilled TODO placeholders for "What", "Why", "Known limitations", and "Issue that this PR addresses". Please complete the PR description with the actual information before merging. This helps reviewers understand the context and future developers understand why these changes were made.

Copilot uses AI. Check for mistakes.
}

// BatchGetByIDs returns the subset of provided account IDs that exist in the accounts table.
// BatchGetByIDs returns the subset of provided account IDs that exist in the accounts table.
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Duplicate comment found. Line 112 duplicates line 113.

Suggested change
// BatchGetByIDs returns the subset of provided account IDs that exist in the accounts table.

Copilot uses AI. Check for mistakes.
if err != nil {
return 0, fmt.Errorf("converting address %s to bytes: %w", addr, err)
}
oaRows = append(oaRows, []any{opIDPgtype, addrBytes})
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Missing nil check before appending addrBytes. If AddressBytea.Value() returns nil for an empty address, this would append a nil value to oaRows, which might work correctly for BYTEA columns. However, for clarity and to prevent potential issues, consider either validating that addresses are non-empty before conversion, or explicitly handling the nil case.

Suggested change
oaRows = append(oaRows, []any{opIDPgtype, addrBytes})
if addrBytes == nil {
oaRows = append(oaRows, []any{opIDPgtype, nil})
} else {
oaRows = append(oaRows, []any{opIDPgtype, addrBytes})
}

Copilot uses AI. Check for mistakes.
@aditya1702 aditya1702 marked this pull request as ready for review February 11, 2026 17:56
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.

1 participant