Repository: algorand/indexer
Pull request #559 (2021-07-02 23:10:15)

Store next round in import state
## Summary

Currently the import state stores the last accounted round which creates ambiguities: we cannot distinguish between the state before accounting round 0 and after. This works because block 0 contains no transactions, and we don't actually do accounting for it. However, with one phase import it will be a problem.

This PR changes the import state to store the next round to account instead, `GetMaxRoundAccounted()` is replaced with `GetNextRoundToAccount()`, and a blocking migration is added.

## Test Plan

Added tests for the migration, and manually tested the migration when max round accounted is 0 and >0.

codecov-commenter: # [Codecov](https://codecov.io/gh/algorand/indexer/pull/559?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#559](https://codecov.io/gh/algorand/indexer/pull/559?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (0c454d0) into [develop](https://codecov.io/gh/algorand/indexer/commit/7cf9b17ae01799a1de1149b2b96bbb929195cf76?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (7cf9b17) will **increase** coverage by `0.32%`.
> The diff coverage is `78.84%`.

[![Impacted file tree graph](https://codecov.io/gh/algorand/indexer/pull/559/graphs/tree.svg?width=650&height=150&src=pr&token=BSHI3PT6QP&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/algorand/indexer/pull/559?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@             Coverage Diff             @@
##           develop     #559      +/-   ##
===========================================
+ Coverage    46.39%   46.71%   +0.32%     
===========================================
  Files           24       24              
  Lines         3897     3930      +33     
===========================================
+ Hits          1808     1836      +28     
+ Misses        1818     1817       -1     
- Partials       271      277       +6     
```


| [Impacted Files](https://codecov.io/gh/algorand/indexer/pull/559?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [idb/postgres/postgres\_migrations.go](https://codecov.io/gh/algorand/indexer/pull/559/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aWRiL3Bvc3RncmVzL3Bvc3RncmVzX21pZ3JhdGlvbnMuZ28=) | `73.89% <71.42%> (+0.83%)` | :arrow_up: |
| [idb/postgres/postgres.go](https://codecov.io/gh/algorand/indexer/pull/559/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aWRiL3Bvc3RncmVzL3Bvc3RncmVzLmdv) | `39.84% <94.11%> (+0.19%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/algorand/indexer/pull/559?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/algorand/indexer/pull/559?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [7cf9b17...0c454d0](https://codecov.io/gh/algorand/indexer/pull/559?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/cmd/algorand-indexer/daemon.go b/cmd/algorand-indexer/daemon.go
index 958c196..8fb6e39 100644
--- a/cmd/algorand-indexer/daemon.go
+++ b/cmd/algorand-indexer/daemon.go
@@ -197,18 +197,11 @@ func (bih *blockImporterHandler) HandleBlock(block *types.EncodedBlockCert) {
 	start := time.Now()
 	_, err := bih.imp.ImportDecodedBlock(block)
 	maybeFail(err, "ImportDecodedBlock %d", block.Block.Round)
-	maxRoundAccounted, err := bih.db.GetMaxRoundAccounted()
-	var nextUnaccountedRound uint64
-	// Special case to start at round 0 if things are uninitialized, otherwise start at the first unaccounted round.
-	if err == idb.ErrorNotInitialized {
-		nextUnaccountedRound = 0
-	} else {
-		maybeFail(err, "failed to get max round accounted.")
-		nextUnaccountedRound = maxRoundAccounted + 1
-	}
+	startRound, err := bih.db.GetNextRoundToAccount()
+	maybeFail(err, "failed to get next round to account")
 	// During normal operation StartRound and MaxRound will be the same round.
 	filter := idb.UpdateFilter{
-		StartRound: nextUnaccountedRound,
+		StartRound: startRound,
 		MaxRound:   uint64(block.Block.Round),
 	}
 	importer.UpdateAccounting(bih.db, bih.cache, filter, logger)
diff --git a/idb/dummy/dummy.go b/idb/dummy/dummy.go
index e6df86f..5b61443 100644
--- a/idb/dummy/dummy.go
+++ b/idb/dummy/dummy.go
@@ -42,13 +42,13 @@ func (db *dummyIndexerDb) LoadGenesis(genesis types.Genesis) (err error) {
 	return nil
 }
 
-// GetMaxRoundAccounted is part of idb.IndexerDB
-func (db *dummyIndexerDb) GetMaxRoundAccounted() (round uint64, err error) {
+// GetNextRoundToAccount is part of idb.IndexerDB
+func (db *dummyIndexerDb) GetNextRoundToAccount() (uint64, error) {
 	return 0, nil
 }
 
 // GetNextRoundToLoad is part of idb.IndexerDB
-func (db *dummyIndexerDb) GetNextRoundToLoad() (round uint64, err error) {
+func (db *dummyIndexerDb) GetNextRoundToLoad() (uint64, error) {
 	return 0, nil
 }
 
diff --git a/idb/idb.go b/idb/idb.go
index 4549923..d7bab32 100644
--- a/idb/idb.go
+++ b/idb/idb.go
@@ -85,9 +85,9 @@ type IndexerDb interface {
 
 	LoadGenesis(genesis types.Genesis) (err error)
 
-	// GetMaxRoundAccounted returns ErrorNotInitialized if there are no accounted rounds.
-	GetMaxRoundAccounted() (round uint64, err error)
-	GetNextRoundToLoad() (round uint64, err error)
+	// GetNextRoundToAccount returns ErrorNotInitialized if genesis is not loaded.
+	GetNextRoundToAccount() (uint64, error)
+	GetNextRoundToLoad() (uint64, error)
 	GetSpecialAccounts() (SpecialAccounts, error)
 	GetDefaultFrozen() (defaultFrozen map[uint64]bool, err error)
 
diff --git a/idb/mocks/IndexerDb.go b/idb/mocks/IndexerDb.go
index 6f59627..f63fc6b 100644
--- a/idb/mocks/IndexerDb.go
+++ b/idb/mocks/IndexerDb.go
@@ -205,8 +205,8 @@ func (_m *IndexerDb) GetDefaultFrozen() (map[uint64]bool, error) {
 	return r0, r1
 }
 
-// GetMaxRoundAccounted provides a mock function with given fields:
-func (_m *IndexerDb) GetMaxRoundAccounted() (uint64, error) {
+// GetNextRoundToAccount provides a mock function with given fields:
+func (_m *IndexerDb) GetNextRoundToAccount() (uint64, error) {
 	ret := _m.Called()
 
 	var r0 uint64
diff --git a/idb/postgres/postgres.go b/idb/postgres/postgres.go
index 17e6b3f..d96f490 100644
--- a/idb/postgres/postgres.go
+++ b/idb/postgres/postgres.go
@@ -36,8 +36,10 @@ import (
 )
 
 type importState struct {
-	// Last accounted round.
+	// DEPRECATED. Last accounted round.
 	AccountRound *int64 `codec:"account_round"`
+	// Next round to account.
+	NextRoundToAccount *uint64 `codec:"next_account_round"`
 }
 
 const stateMetastateKey = "state"
@@ -65,8 +67,9 @@ func OpenPostgres(connection string, opts idb.IndexerDbOptions, log *log.Logger)
 // Allow tests to inject a DB
 func openPostgres(db *sql.DB, opts idb.IndexerDbOptions, logger *log.Logger) (pdb *IndexerDb, err error) {
 	pdb = &IndexerDb{
-		log: logger,
-		db:  db,
+		readonly: opts.ReadOnly,
+		log:      logger,
+		db:       db,
 	}
 
 	if pdb.log == nil {
@@ -88,7 +91,8 @@ func openPostgres(db *sql.DB, opts idb.IndexerDbOptions, logger *log.Logger) (pd
 
 // IndexerDb is an idb.IndexerDB implementation
 type IndexerDb struct {
-	log *log.Logger
+	readonly bool
+	log      *log.Logger
 
 	db *sql.DB
 
@@ -334,9 +338,9 @@ func (db *IndexerDb) LoadGenesis(genesis types.Genesis) (err error) {
 		}
 	}
 
-	round := int64(0)
+	nextRound := uint64(0)
 	importstate := importState{
-		AccountRound: &round,
+		NextRoundToAccount: &nextRound,
 	}
 	err = db.setImportState(nil, importstate)
 	if err != nil {
@@ -411,9 +415,9 @@ func (db *IndexerDb) setImportState(tx *sql.Tx, state importState) error {
 	return db.setMetastate(tx, stateMetastateKey, string(encoding.EncodeJSON(state)))
 }
 
-// Returns idb.ErrorNotInitialized if uninitialized.
+// Returns ErrorNotInitialized if genesis is not loaded.
 // If `tx` is nil, use a normal query.
-func (db *IndexerDb) getMaxRoundAccounted(tx *sql.Tx) (uint64, error) {
+func (db *IndexerDb) getNextRoundToAccount(tx *sql.Tx) (uint64, error) {
 	state, err := db.getImportState(tx)
 	if err == idb.ErrorNotInitialized {
 		return 0, err
@@ -422,16 +426,30 @@ func (db *IndexerDb) getMaxRoundAccounted(tx *sql.Tx) (uint64, error) {
 		return 0, fmt.Errorf("getNextRoundToAccount() err: %w", err)
 	}
 
-	if state.AccountRound == nil {
+	if state.NextRoundToAccount == nil {
 		return 0, idb.ErrorNotInitialized
 	}
-	return uint64(*state.AccountRound), nil
+	return *state.NextRoundToAccount, nil
 }
 
-// GetMaxRoundAccounted is part of idb.IndexerDB
-// Returns idb.ErrorNotInitialized if uninitialized.
-func (db *IndexerDb) GetMaxRoundAccounted() (round uint64, err error) {
-	return db.getMaxRoundAccounted(nil)
+// GetNextRoundToAccount is part of idb.IndexerDB
+// Returns ErrorNotInitialized if genesis is not loaded.
+func (db *IndexerDb) GetNextRoundToAccount() (uint64, error) {
+	return db.getNextRoundToAccount(nil)
+}
+
+// Returns ErrorNotInitialized if genesis is not loaded.
+// If `tx` is nil, use a normal query.
+func (db *IndexerDb) getMaxRoundAccounted(tx *sql.Tx) (uint64, error) {
+	round, err := db.getNextRoundToAccount(tx)
+	if err != nil {
+		return 0, err
+	}
+
+	if round > 0 {
+		round--
+	}
+	return round, nil
 }
 
 // GetNextRoundToLoad is part of idb.IndexerDB
@@ -1294,17 +1312,17 @@ ON CONFLICT (addr, assetid) DO UPDATE SET amount = account_asset.amount + EXCLUD
 		return err
 	}
 
-	if importstate.AccountRound == nil {
+	if importstate.NextRoundToAccount == nil {
 		return fmt.Errorf("importstate.AccountRound is nil")
 	}
 
-	if uint64(*importstate.AccountRound) >= round {
+	if uint64(*importstate.NextRoundToAccount) > round {
 		return fmt.Errorf(
-			"metastate round = %d while trying to write round %d",
-			*importstate.AccountRound, round)
+			"next round to account is %d while trying to write round %d",
+			*importstate.NextRoundToAccount, round)
 	}
 
-	*importstate.AccountRound = int64(round)
+	*importstate.NextRoundToAccount = round + 1
 	err = db.setImportState(tx, importstate)
 	if err != nil {
 		return
@@ -2928,7 +2946,10 @@ func (db *IndexerDb) Health() (idb.Health, error) {
 	errString := ""
 	var data = make(map[string]interface{})
 
-	// If we are not in read-only mode, there will be a migration object.
+	if db.readonly {
+		data["read-only-mode"] = true
+	}
+
 	if db.migration != nil {
 		state := db.migration.GetStatus()
 
@@ -2943,7 +2964,6 @@ func (db *IndexerDb) Health() (idb.Health, error) {
 		migrating = state.Running
 		blocking = state.Blocking
 	} else {
-		data["read-only-node"] = true
 		state, err := db.getMigrationState()
 		if err == nil {
 			blocking = migrationStateBlocked(*state)
@@ -2953,7 +2973,7 @@ func (db *IndexerDb) Health() (idb.Health, error) {
 
 	data["migration-required"] = migrationRequired
 
-	round, err := db.GetMaxRoundAccounted()
+	round, err := db.getMaxRoundAccounted(nil)
 
 	// We'll just have to set the round to 0
 	if err == idb.ErrorNotInitialized {
diff --git a/idb/postgres/postgres_migrations.go b/idb/postgres/postgres_migrations.go
index 7b4846f..79e813a 100644
--- a/idb/postgres/postgres_migrations.go
+++ b/idb/postgres/postgres_migrations.go
@@ -42,6 +42,7 @@ func init() {
 		{FixFreezeLookupMigration, false, "Fix search by asset freeze address."},
 		{ClearAccountDataMigration, false, "clear account data for accounts that have been closed"},
 		{MakeDeletedNotNullMigration, false, "make all \"deleted\" columns NOT NULL"},
+		{MaxRoundAccountedMigration, true, "change import state format"},
 	}
 }
 
@@ -99,27 +100,16 @@ func needsMigration(state MigrationState) bool {
 	return state.NextMigration < len(migrations)
 }
 
-// upsertMigrationStateTx updates the migration state, and optionally increments the next counter with an existing
-// transaction.
-func upsertMigrationStateTx(tx *sql.Tx, state *MigrationState, incrementNextMigration bool) (err error) {
+// upsertMigrationState updates the migration state, and optionally increments
+// the next counter with an existing transaction.
+// If `tx` is nil, use a normal query.
+func upsertMigrationState(db *IndexerDb, tx *sql.Tx, state *MigrationState, incrementNextMigration bool) error {
 	if incrementNextMigration {
 		state.NextMigration++
 	}
 	migrationStateJSON := encoding.EncodeJSON(state)
-	_, err = tx.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
 
-	return err
-}
-
-// upsertMigrationState updates the migration state, and optionally increments the next counter.
-func upsertMigrationState(db *IndexerDb, state *MigrationState, incrementNextMigration bool) (err error) {
-	if incrementNextMigration {
-		state.NextMigration++
-	}
-	migrationStateJSON := encoding.EncodeJSON(state)
-	_, err = db.db.Exec(setMetastateUpsert, migrationMetastateKey, migrationStateJSON)
-
-	return err
+	return db.setMetastate(tx, migrationMetastateKey, string(migrationStateJSON))
 }
 
 func (db *IndexerDb) runAvailableMigrations(migrationStateJSON string) (err error) {
@@ -348,7 +338,7 @@ func FixFreezeLookupMigration(db *IndexerDb, state *MigrationState) error {
 	}
 
 	// Update migration state
-	return upsertMigrationState(db, state, true)
+	return upsertMigrationState(db, nil, state, true)
 }
 
 type account struct {
@@ -538,3 +528,56 @@ func MakeDeletedNotNullMigration(db *IndexerDb, state *MigrationState) error {
 	}
 	return sqlMigration(db, state, queries)
 }
+
+// MaxRoundAccountedMigration converts the import state.
+func MaxRoundAccountedMigration(db *IndexerDb, migrationState *MigrationState) error {
+	db.accountingLock.Lock()
+	defer db.accountingLock.Unlock()
+
+	nextMigrationState := *migrationState
+	nextMigrationState.NextMigration++
+
+	f := func(ctx context.Context, tx *sql.Tx) error {
+		defer tx.Rollback()
+
+		importstate, err := db.getImportState(tx)
+		if err == idb.ErrorNotInitialized {
+			// Leave uninitialized.
+			db.log.Printf("Import state is not initialized, leaving unchanged.")
+		} else if err != nil {
+			return err
+		} else {
+			if importstate.AccountRound == nil {
+				db.log.Printf("Account round is not set, leaving unchanged.")
+			} else {
+				nextRound := uint64(0)
+				if *importstate.AccountRound > 0 {
+					nextRound = uint64(*importstate.AccountRound + 1)
+				}
+				importstate.NextRoundToAccount = &nextRound
+				importstate.AccountRound = nil
+
+				db.log.Printf("Setting import state to %s", encoding.EncodeJSON(importstate))
+
+				err = db.setImportState(tx, importstate)
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		err = upsertMigrationState(db, tx, &nextMigrationState, false)
+		if err != nil {
+			return err
+		}
+
+		return tx.Commit()
+	}
+	err := db.txWithRetry(context.Background(), serializable, f)
+	if err != nil {
+		return err
+	}
+
+	*migrationState = nextMigrationState
+	return nil
+}
diff --git a/importer/helper.go b/importer/helper.go
index 26fc7be..b2fc11f 100644
--- a/importer/helper.go
+++ b/importer/helper.go
@@ -83,14 +83,12 @@ func (h *ImportHelper) Import(db idb.IndexerDb, args []string) {
 		h.Log.Infof("%d blocks in %s, %.0f/s, %d txn, %.0f/s", blocks, dt.String(), float64(time.Second)*float64(blocks)/float64(dt), txCount, float64(time.Second)*float64(txCount)/float64(dt))
 	}
 
-	var startRound uint64 = 0
-
-	lastRound, err := db.GetMaxRoundAccounted()
+	startRound, err := db.GetNextRoundToAccount()
 	if err == idb.ErrorNotInitialized {
 		InitialImport(db, h.GenesisJSONPath, nil, h.Log)
+		startRound = 0
 	} else {
 		maybeFail(err, h.Log, "problem getting the import state")
-		startRound = lastRound + 1
 	}
 
 	filter := idb.UpdateFilter{
@@ -221,7 +219,7 @@ func loadGenesis(db idb.IndexerDb, in io.Reader) (err error) {
 
 // InitialImport imports the genesis block if needed. Returns true if the initial import occurred.
 func InitialImport(db idb.IndexerDb, genesisJSONPath string, client *algod.Client, l *log.Logger) bool {
-	_, err := db.GetMaxRoundAccounted()
+	_, err := db.GetNextRoundToAccount()
 
 	// Exit immediately or crash if we don't see ErrorNotInitialized.
 	if err != idb.ErrorNotInitialized {


Test patch:
diff --git a/idb/postgres/postgres_integration_test.go b/idb/postgres/postgres_integration_test.go
index 3323dcd..dbe935b 100644
--- a/idb/postgres/postgres_integration_test.go
+++ b/idb/postgres/postgres_integration_test.go
@@ -36,47 +36,36 @@ func TestMaxRoundOnUninitializedDB(t *testing.T) {
 	_, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
 
-	///////////
-	// Given // A database that has not yet imported the genesis accounts.
-	///////////
-	db, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
+	db, err := OpenPostgres(connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 
-	//////////
-	// When // We request the max round.
-	//////////
-	roundA, errA := db.GetMaxRoundAccounted()
-	roundL, errL := db.GetNextRoundToLoad()
+	round, err := db.GetNextRoundToAccount()
+	assert.Equal(t, err, idb.ErrorNotInitialized)
+	assert.Equal(t, uint64(0), round)
 
-	//////////
-	// Then // The error message should be set.
-	//////////
-	assert.Equal(t, errA, idb.ErrorNotInitialized)
-	assert.Equal(t, uint64(0), roundA)
+	round, err = db.getMaxRoundAccounted(nil)
+	assert.Equal(t, err, idb.ErrorNotInitialized)
+	assert.Equal(t, uint64(0), round)
 
-	require.NoError(t, errL)
-	assert.Equal(t, uint64(0), roundL)
+	round, err = db.GetNextRoundToLoad()
+	require.NoError(t, err)
+	assert.Equal(t, uint64(0), round)
 }
 
 // TestMaxRoundEmptyMetastate makes sure we return 0 when the metastate is empty.
 func TestMaxRoundEmptyMetastate(t *testing.T) {
 	pg, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
-	///////////
-	// Given // The database has the metastate set but the account_round is missing.
-	///////////
-	db, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
+
+	db, err := OpenPostgres(connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
 	pg.Exec(`INSERT INTO metastate (k, v) values ('state', '{}')`)
 
-	//////////
-	// When // We request the max round.
-	//////////
-	round, err := db.GetMaxRoundAccounted()
+	round, err := db.GetNextRoundToAccount()
+	assert.Equal(t, err, idb.ErrorNotInitialized)
+	assert.Equal(t, uint64(0), round)
 
-	//////////
-	// Then // The error message should be set.
-	//////////
+	round, err = db.getMaxRoundAccounted(nil)
 	assert.Equal(t, err, idb.ErrorNotInitialized)
 	assert.Equal(t, uint64(0), round)
 }
@@ -85,27 +74,49 @@ func TestMaxRoundEmptyMetastate(t *testing.T) {
 func TestMaxRound(t *testing.T) {
 	db, connStr, shutdownFunc := setupPostgres(t)
 	defer shutdownFunc()
-	///////////
-	// Given // The database has the metastate set normally.
-	///////////
-	pdb, err := idb.IndexerDbByName("postgres", connStr, idb.IndexerDbOptions{}, nil)
-	assert.NoError(t, err)
-	db.Exec(`INSERT INTO metastate (k, v) values ($1, $2)`, "state", "{\"account_round\":123454321}")
-	db.Exec(`INSERT INTO block_header (round, realtime, rewardslevel, header) VALUES ($1, NOW(), 0, '{}') ON CONFLICT DO NOTHING`, 543212345)
 
-	//////////
-	// When // We request the max round.
-	//////////
-	roundA, err := pdb.GetMaxRoundAccounted()
+	pdb, err := OpenPostgres(connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
-	roundL, err := pdb.GetNextRoundToLoad()
+	db.Exec(
+		`INSERT INTO metastate (k, v) values ($1, $2)`,
+		"state",
+		`{"next_account_round":123454322}`)
+	db.Exec(
+		`INSERT INTO block_header (round, realtime, rewardslevel, header) `+
+			`VALUES ($1, NOW(), 0, '{}') ON CONFLICT DO NOTHING`,
+		543212345)
+
+	round, err := pdb.GetNextRoundToAccount()
+	require.NoError(t, err)
+	assert.Equal(t, uint64(123454322), round)
+
+	round, err = pdb.getMaxRoundAccounted(nil)
+	require.NoError(t, err)
+	assert.Equal(t, uint64(123454321), round)
+
+	round, err = pdb.GetNextRoundToLoad()
+	require.NoError(t, err)
+	assert.Equal(t, uint64(543212346), round)
+}
+
+func TestAccountedRoundNextRound0(t *testing.T) {
+	db, connStr, shutdownFunc := setupPostgres(t)
+	defer shutdownFunc()
+
+	pdb, err := OpenPostgres(connStr, idb.IndexerDbOptions{}, nil)
 	assert.NoError(t, err)
+	db.Exec(
+		`INSERT INTO metastate (k, v) values ($1, $2)`,
+		"state",
+		`{"next_account_round":0}`)
 
-	//////////
-	// Then // There should be no error and we return that there are zero rounds.
-	//////////
-	assert.Equal(t, uint64(123454321), roundA)
-	assert.Equal(t, uint64(543212346), roundL)
+	round, err := pdb.GetNextRoundToAccount()
+	require.NoError(t, err)
+	assert.Equal(t, uint64(0), round)
+
+	round, err = pdb.getMaxRoundAccounted(nil)
+	require.NoError(t, err)
+	assert.Equal(t, uint64(0), round)
 }
 
 func assertAccountAsset(t *testing.T, db *sql.DB, addr sdk_types.Address, assetid uint64, frozen bool, amount uint64) {
@@ -421,7 +432,9 @@ func TestBlockWithTransactions(t *testing.T) {
 	txns := []*sdk_types.SignedTxnWithAD{tx1, tx2, tx3, tx4, tx5}
 	txnRows := []*idb.TxnRow{row1, row2, row3, row4, row5}
 
-	_, err = db.Exec(`INSERT INTO metastate (k, v) values ($1, $2)`, "state", `{"account_round": 11}`)
+	_, err = db.Exec(
+		`INSERT INTO metastate (k, v) values ($1, $2)`, "state",
+		`{"next_account_round": 12}`)
 	require.NoError(t, err)
 	_, err = db.Exec(`INSERT INTO block_header (round, realtime, rewardslevel, header) VALUES ($1, NOW(), 0, '{}') ON CONFLICT DO NOTHING`, test.Round)
 	require.NoError(t, err)
diff --git a/idb/postgres/postgres_migrations_integration_test.go b/idb/postgres/postgres_migrations_integration_test.go
index d4ed575..35f051f 100644
--- a/idb/postgres/postgres_migrations_integration_test.go
+++ b/idb/postgres/postgres_migrations_integration_test.go
@@ -302,3 +302,86 @@ func TestMakeDeletedNotNullMigration(t *testing.T) {
 		assert.Equal(t, false, *deleted)
 	}
 }
+
+func TestMaxRoundAccountedMigrationAccountRound0(t *testing.T) {
+	_, connStr, shutdownFunc := setupPostgres(t)
+	defer shutdownFunc()
+	db, err := OpenPostgres(connStr, idb.IndexerDbOptions{}, nil)
+	assert.NoError(t, err)
+
+	round := int64(0)
+	importstate := importState{
+		AccountRound: &round,
+	}
+	err = db.setImportState(nil, importstate)
+	require.NoError(t, err)
+
+	migrationState := MigrationState{NextMigration: 4}
+	err = MaxRoundAccountedMigration(db, &migrationState)
+	require.NoError(t, err)
+
+	importstate, err = db.getImportState(nil)
+	require.NoError(t, err)
+
+	nextRound := uint64(0)
+	importstateExpected := importState{
+		NextRoundToAccount: &nextRound,
+	}
+	assert.Equal(t, importstateExpected, importstate)
+
+	// Check the next migration number.
+	assert.Equal(t, 5, migrationState.NextMigration)
+	newNum := nextMigrationNum(t, db)
+	assert.Equal(t, 5, newNum)
+}
+
+func TestMaxRoundAccountedMigrationAccountRoundPositive(t *testing.T) {
+	_, connStr, shutdownFunc := setupPostgres(t)
+	defer shutdownFunc()
+	db, err := OpenPostgres(connStr, idb.IndexerDbOptions{}, nil)
+	assert.NoError(t, err)
+
+	round := int64(2)
+	importstate := importState{
+		AccountRound: &round,
+	}
+	err = db.setImportState(nil, importstate)
+	require.NoError(t, err)
+
+	migrationState := MigrationState{NextMigration: 4}
+	err = MaxRoundAccountedMigration(db, &migrationState)
+	require.NoError(t, err)
+
+	importstate, err = db.getImportState(nil)
+	require.NoError(t, err)
+
+	nextRound := uint64(3)
+	importstateExpected := importState{
+		NextRoundToAccount: &nextRound,
+	}
+	assert.Equal(t, importstateExpected, importstate)
+
+	// Check the next migration number.
+	assert.Equal(t, 5, migrationState.NextMigration)
+	newNum := nextMigrationNum(t, db)
+	assert.Equal(t, 5, newNum)
+}
+
+func TestMaxRoundAccountedMigrationUninitialized(t *testing.T) {
+	_, connStr, shutdownFunc := setupPostgres(t)
+	defer shutdownFunc()
+	db, err := OpenPostgres(connStr, idb.IndexerDbOptions{}, nil)
+	assert.NoError(t, err)
+
+	migrationState := MigrationState{NextMigration: 4}
+	err = MaxRoundAccountedMigration(db, &migrationState)
+	require.NoError(t, err)
+
+	_, err = db.getImportState(nil)
+	assert.Equal(t, idb.ErrorNotInitialized, err)
+
+	// Check the next migration number.
+	assert.Equal(t, 5, migrationState.NextMigration)
+	newNum := nextMigrationNum(t, db)
+	assert.Equal(t, 5, newNum)
+}