decred.org/dcrdex@v1.0.5/server/db/driver/pg/upgrades.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package pg
     5  
     6  import (
     7  	"context"
     8  	"database/sql"
     9  	"errors"
    10  	"fmt"
    11  	"math"
    12  	"strings"
    13  
    14  	"decred.org/dcrdex/dex"
    15  	"decred.org/dcrdex/dex/calc"
    16  	"decred.org/dcrdex/server/asset"
    17  	"decred.org/dcrdex/server/db/driver/pg/internal"
    18  )
    19  
    20  const dbVersion = 6
    21  
    22  // The number of upgrades defined MUST be equal to dbVersion.
    23  var upgrades = []func(db *sql.Tx) error{
    24  	// v1 upgrade adds the schema_version column to the meta table, possibly
    25  	// creating the table if it was missing.
    26  	v1Upgrade,
    27  
    28  	// v2 upgrade creates epochs_report table, if it does not exist, and
    29  	// populates the table with partial historical data from the epochs and
    30  	// matches table. This includes match volumes, high/low/start/end rates, but
    31  	// does not include the booked volume statistics in the book_buys* and
    32  	// book_sells* columns since this data requires a book snapshot at the time
    33  	// of matching to generate.
    34  	v2Upgrade,
    35  
    36  	// v3 upgrade adds the fee_asset column to the accounts table.
    37  	v3Upgrade,
    38  
    39  	// v4 upgrade updates the markets tables to use a integer type that can
    40  	// accommodate a 32-bit unsigned integer.
    41  	v4Upgrade,
    42  
    43  	// v5 upgrade adds an epoch_gap column to the cancel order tables to
    44  	// facilitate free cancels.
    45  	v5Upgrade,
    46  
    47  	// v6 upgrade creates the bonds table. A future upgrade may add a new
    48  	// old_fee_coin column to the accounts table for when a manual refund is
    49  	// processed.
    50  	v6Upgrade,
    51  }
    52  
    53  // v1Upgrade adds the schema_version column and removes the state_hash column
    54  // from the meta table.
    55  func v1Upgrade(tx *sql.Tx) error {
    56  	// Create the meta table with the v0 scheme. Even if the table does not
    57  	// exists, we should not create it fresh with the current scheme since one
    58  	// or more subsequent upgrades may alter the meta scheme.
    59  	metaV0Stmt := `CREATE TABLE IF NOT EXISTS %s (state_hash BYTEA)`
    60  	metaCreated, err := createTableStmt(tx, metaV0Stmt, publicSchema, metaTableName)
    61  	if err != nil {
    62  		return fmt.Errorf("failed to create meta table: %w", err)
    63  	}
    64  	if metaCreated {
    65  		log.Infof("Created new %q table", metaTableName)    // from 0.2+pre master
    66  		_, err = tx.Exec(`INSERT INTO meta DEFAULT VALUES`) // might be CreateMetaRow, but pin to the v0 stmt
    67  		if err != nil {
    68  			return fmt.Errorf("failed to create row for meta table: %w", err)
    69  		}
    70  	} else {
    71  		log.Infof("Existing %q table", metaTableName) // from release-0.1
    72  	}
    73  
    74  	// Create the schema_version column. The caller must set the version to 1.
    75  	_, err = tx.Exec(`ALTER TABLE ` + metaTableName + ` ADD COLUMN IF NOT EXISTS schema_version INT4 DEFAULT 0;`)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	_, err = tx.Exec(`ALTER TABLE ` + metaTableName + ` DROP COLUMN IF EXISTS state_hash;`)
    80  	return err
    81  }
    82  
    83  // matchStatsForMarketEpoch is used by v2Upgrade to retrieve match rates and
    84  // quantities for a given epoch.
    85  func matchStatsForMarketEpoch(stmt *sql.Stmt, epochIdx, epochDur uint64) (rates, quantities []uint64, sell []bool, err error) {
    86  	var rows *sql.Rows
    87  	rows, err = stmt.Query(epochIdx, epochDur)
    88  	if err != nil {
    89  		return
    90  	}
    91  	defer rows.Close()
    92  
    93  	for rows.Next() {
    94  		var rate, quantity fastUint64
    95  		var takerSell bool
    96  		err = rows.Scan(&quantity, &rate, &takerSell)
    97  		if err != nil {
    98  			return nil, nil, nil, err
    99  		}
   100  		rates = append(rates, uint64(rate))
   101  		quantities = append(quantities, uint64(quantity))
   102  		sell = append(sell, takerSell)
   103  	}
   104  
   105  	if err = rows.Err(); err != nil {
   106  		return nil, nil, nil, err
   107  	}
   108  	return
   109  }
   110  
   111  // v2Upgrade populates the epoch_reports table with historical data from the
   112  // matches table.
   113  func v2Upgrade(tx *sql.Tx) error {
   114  	mkts, err := loadMarkets(tx, marketsTableName)
   115  	if err != nil {
   116  		return fmt.Errorf("failed to read markets table: %w", err)
   117  	}
   118  
   119  	unitInfo := func(assetID uint32) dex.UnitInfo {
   120  		ui, err := asset.UnitInfo(assetID)
   121  		if err != nil {
   122  			log.Errorf("no unit info found for %d (%q)", assetID, dex.BipIDSymbol(assetID))
   123  			ui.Conventional.ConversionFactor = 1e8
   124  		}
   125  		return ui
   126  	}
   127  
   128  	doMarketMatches := func(mkt *dex.MarketInfo) error {
   129  		log.Infof("Populating %s with volume data for market %q matches...", epochsTableName, mkt.Name)
   130  
   131  		baseUnitInfo, quoteUnitInfo := unitInfo(mkt.Base), unitInfo(mkt.Quote)
   132  
   133  		// Create the epochs_report table if it does not already exist.
   134  		_, err := createTable(tx, mkt.Name, epochReportsTableName)
   135  		if err != nil {
   136  			return err
   137  		}
   138  
   139  		// For each unique epoch duration, get the first and last epoch index.
   140  		fullEpochsTableName := mkt.Name + "." + epochsTableName
   141  		stmt := fmt.Sprintf(`SELECT epoch_dur, MIN(epoch_idx), MAX(epoch_idx)
   142  			FROM %s GROUP BY epoch_dur;`, fullEpochsTableName)
   143  		rows, err := tx.Query(stmt)
   144  		if err != nil {
   145  			return err
   146  		}
   147  		defer rows.Close()
   148  
   149  		var durs, starts, ends []uint64
   150  		for rows.Next() {
   151  			var dur, first, last uint64
   152  			if err = rows.Scan(&dur, &first, &last); err != nil {
   153  				return err
   154  			}
   155  			durs = append(durs, dur)
   156  			starts = append(starts, first)
   157  			ends = append(ends, last)
   158  		}
   159  
   160  		if err = rows.Err(); err != nil {
   161  			return err
   162  		}
   163  
   164  		// epoch_reports INSERT statement
   165  		mktEpochReportsTablename := mkt.Name + "." + epochReportsTableName
   166  		reportStmt := fmt.Sprintf(internal.InsertPartialEpochReport, mktEpochReportsTablename)
   167  		reportStmtPrep, err := tx.Prepare(reportStmt)
   168  		if err != nil {
   169  			return err
   170  		}
   171  		defer reportStmtPrep.Close()
   172  
   173  		// Create a temporary matches index on (epochidx, epochdur).
   174  		fullMatchesTableName := mkt.Name + "." + matchesTableName
   175  		matchIndexName := "matches_epidxdur_temp_idx"
   176  		_, err = tx.Exec(fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s (epochidx, epochdur);",
   177  			matchIndexName, fullMatchesTableName))
   178  		if err != nil {
   179  			return err
   180  		}
   181  		defer func() {
   182  			if errors.Is(err, sql.ErrTxDone) {
   183  				return // whole transaction including index creation is rolled back
   184  			}
   185  			// Success or other error - drop the index explicitly.
   186  			fullIndexName := mkt.Name + "." + matchIndexName
   187  			_, errDrop := tx.Exec(fmt.Sprintf("DROP INDEX %s;", fullIndexName))
   188  			if errDrop != nil {
   189  				log.Warnf("Failed to drop index %v: %v", fullIndexName, errDrop)
   190  			}
   191  		}()
   192  
   193  		// matches(qty,rate,takerSell) SELECT statement
   194  		matchStatsStmt := fmt.Sprintf(internal.RetrieveMatchStatsByEpoch, fullMatchesTableName)
   195  		matchStatsStmtPrep, err := tx.Prepare(matchStatsStmt)
   196  		if err != nil {
   197  			return err
   198  		}
   199  		defer matchStatsStmtPrep.Close()
   200  
   201  		var startRate, endRate uint64
   202  		var totalMatches uint64
   203  		var totalVolume, totalQVolume uint64
   204  		for i, dur := range durs {
   205  			log.Infof("Processing all %d of the %d ms %q epochs from idx %d to %d...",
   206  				ends[i]-starts[i]+1, dur, mkt.Name, starts[i], ends[i])
   207  			endIdx := ends[i]
   208  			for idx := starts[i]; idx <= endIdx; idx++ {
   209  				if idx%50000 == 0 {
   210  					to := idx + 50000
   211  					if to > endIdx+1 {
   212  						to = endIdx + 1
   213  					}
   214  					log.Infof(" - Processing epochs [%d, %d)...", idx, to)
   215  				}
   216  				var rates, quantities []uint64 // don't shadow err from outer scope
   217  				rates, quantities, _, err = matchStatsForMarketEpoch(matchStatsStmtPrep, idx, dur)
   218  				if err != nil {
   219  					return err
   220  				}
   221  				epochEnd := (idx + 1) * dur
   222  				if len(rates) == 0 {
   223  					// No trade matches in this epoch.
   224  					_, err = reportStmtPrep.Exec(epochEnd, dur, 0, 0, 0, 0, startRate, startRate)
   225  					if err != nil {
   226  						return err
   227  					}
   228  					continue
   229  				}
   230  
   231  				var matchVolume, quoteVolume, highRate uint64
   232  				lowRate := uint64(math.MaxInt64)
   233  				for i, qty := range quantities {
   234  					matchVolume += qty
   235  					rate := rates[i]
   236  					quoteVolume += calc.BaseToQuote(rate, qty)
   237  					if rate > highRate {
   238  						highRate = rate
   239  					}
   240  					if rate < lowRate {
   241  						lowRate = rate
   242  					}
   243  				}
   244  				totalVolume += matchVolume
   245  				totalQVolume += quoteVolume
   246  				totalMatches += uint64(len(quantities))
   247  
   248  				// In the absence of a book snapshot, ballpark the rates. Note
   249  				// that cancel order matches that change the mid market book
   250  				// rate are not captured so start/end rates can be inaccurate
   251  				// given long periods with no trades but book changes.
   252  				midRate := (lowRate + highRate) / 2 // maybe average instead
   253  				if startRate == 0 {
   254  					startRate = midRate
   255  				} else {
   256  					startRate = endRate // from previous epoch with matches
   257  				}
   258  				endRate = midRate
   259  
   260  				// No book buy / sell depth (see bookVolumes in server/matcher).
   261  				_, err = reportStmtPrep.Exec(epochEnd, dur, matchVolume, quoteVolume,
   262  					highRate, lowRate, startRate, endRate)
   263  				if err != nil {
   264  					return err
   265  				}
   266  			}
   267  		} // range durs
   268  		log.Debugf("Processed %d matches doing %s in %s volume (%s in %s volume)", totalMatches,
   269  			baseUnitInfo.ConventionalString(totalVolume), strings.ToUpper(dex.BipIDSymbol(mkt.Base)),
   270  			quoteUnitInfo.ConventionalString(totalQVolume), strings.ToUpper(dex.BipIDSymbol(mkt.Quote)))
   271  		return nil
   272  	}
   273  
   274  	for _, mkt := range mkts {
   275  		err = doMarketMatches(mkt)
   276  		if err != nil {
   277  			return err
   278  		}
   279  	}
   280  	return nil
   281  }
   282  
   283  func v3Upgrade(tx *sql.Tx) error {
   284  	// Create the fee_asset column.
   285  	_, err := tx.Exec(`ALTER TABLE ` + accountsTableName + ` ADD COLUMN IF NOT EXISTS fee_asset INT4;`)
   286  	if err != nil {
   287  		return err
   288  	}
   289  	// Set existing rows fee_asset to 42, Decred's asset ID, since prior to this
   290  	// upgrade, only DCR was accepted for registration.
   291  	_, err = tx.Exec(`UPDATE ` + accountsTableName + ` SET fee_asset = 42;`) // not as default in ALTER
   292  	return err
   293  }
   294  
   295  func v4Upgrade(tx *sql.Tx) (err error) {
   296  	if _, err = tx.Exec("ALTER TABLE markets ALTER COLUMN base TYPE INT8;"); err != nil {
   297  		return
   298  	}
   299  	_, err = tx.Exec("ALTER TABLE markets ALTER COLUMN quote TYPE INT8;")
   300  	return err
   301  }
   302  
   303  func v5Upgrade(tx *sql.Tx) (err error) {
   304  	mkts, err := loadMarkets(tx, marketsTableName)
   305  	if err != nil {
   306  		return fmt.Errorf("failed to read markets table: %w", err)
   307  	}
   308  
   309  	doTable := func(tableName string) error {
   310  		_, err = tx.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN epoch_gap INT4 DEFAULT -1;", tableName))
   311  		return err
   312  	}
   313  
   314  	log.Infof("Adding epoch_gap column to cancel tables for %d markets", len(mkts))
   315  
   316  	for _, mkt := range mkts {
   317  		if err := doTable(mkt.Name + "." + cancelsArchivedTableName); err != nil {
   318  			return err
   319  		}
   320  		if err := doTable(mkt.Name + "." + cancelsActiveTableName); err != nil {
   321  			return err
   322  		}
   323  	}
   324  	return nil
   325  }
   326  
   327  // v6Upgrade creates the bonds table and its indexes on account_id and lockTime.
   328  func v6Upgrade(tx *sql.Tx) error {
   329  	bondsCreated, err := createTableStmt(tx, internal.CreateBondsTableV0, publicSchema, bondsTableName)
   330  	if err != nil {
   331  		return fmt.Errorf("failed to create bonds table: %w", err)
   332  	}
   333  	if bondsCreated {
   334  		log.Infof("Created new %q table", bondsTableName)
   335  	} else {
   336  		log.Warnf("Unexpected existing %q table!", bondsTableName)
   337  	}
   338  
   339  	namespacedBondsTable := publicSchema + "." + bondsTableName
   340  	err = createIndexStmt(tx, internal.CreateBondsAcctIndexV0, indexBondsOnAccountName, namespacedBondsTable)
   341  	if err != nil {
   342  		return fmt.Errorf("failed to index bonds table on account: %w", err)
   343  	}
   344  
   345  	err = createIndexStmt(tx, internal.CreateBondsLockTimeIndexV0, indexBondsOnLockTimeName, namespacedBondsTable)
   346  	if err != nil {
   347  		return fmt.Errorf("failed to index bonds table on lock time: %w", err)
   348  	}
   349  
   350  	// drop the accounts.broken_rule column
   351  	namespacedAccountsTable := publicSchema + "." + accountsTableName
   352  	_, err = tx.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN IF EXISTS broken_rule;", namespacedAccountsTable))
   353  	if err != nil {
   354  		return fmt.Errorf("failed to drop the accounts.broken_rule column: %w", err)
   355  	}
   356  
   357  	return nil
   358  }
   359  
   360  // DBVersion retrieves the database version from the meta table.
   361  func DBVersion(db *sql.DB) (ver uint32, err error) {
   362  	err = db.QueryRow(internal.SelectDBVersion).Scan(&ver)
   363  	return
   364  }
   365  
   366  func setDBVersion(db sqlExecutor, ver uint32) error {
   367  	res, err := db.Exec(internal.SetDBVersion, ver)
   368  	if err != nil {
   369  		return err
   370  	}
   371  
   372  	n, err := res.RowsAffected()
   373  	if err != nil {
   374  		return err
   375  	}
   376  	if n != 1 {
   377  		return fmt.Errorf("set the DB version in %d rows instead of 1", n)
   378  	}
   379  	return nil
   380  }
   381  
   382  func upgradeDB(ctx context.Context, db *sql.DB) error {
   383  	// Get the DB version from the meta table. Nonexistent meta table or
   384  	// meta.schema_version column implies v0, the upgrade from which adds the
   385  	// table and schema_version column.
   386  	var current uint32
   387  	found, err := tableExists(db, metaTableName)
   388  	if err != nil {
   389  		return err
   390  	}
   391  	if found {
   392  		found, err = columnExists(db, "public", metaTableName, "schema_version")
   393  		if err != nil {
   394  			return err
   395  		}
   396  		if found {
   397  			current, err = DBVersion(db)
   398  			if err != nil {
   399  				return fmt.Errorf("failed to get DB version: %w", err)
   400  			}
   401  		} // else v1 upgrade creates meta.schema_version column
   402  	} // else v1 upgrade creates meta table
   403  
   404  	if current == dbVersion {
   405  		log.Infof("DCRDEX database ready at version %d", dbVersion)
   406  		return nil // all upgraded
   407  	}
   408  
   409  	if current > dbVersion {
   410  		return fmt.Errorf("current DB version %d is newer than highest recognized version %d",
   411  			current, dbVersion)
   412  	}
   413  
   414  	runUpgradeTx := func(targetVer uint32, up func(db *sql.Tx) error) error {
   415  		// Canceling the context automatically rolls back the transaction.
   416  		tx, err := db.BeginTx(ctx, nil)
   417  		if err != nil {
   418  			return err
   419  		}
   420  		defer func() {
   421  			// On error, rollback the transaction unless ctx was canceled
   422  			// (sql.ErrTxDone) because then rollback is automatic. See the
   423  			// (*sql.DB).BeginTx docs.
   424  			if err == nil || errors.Is(err, sql.ErrTxDone) {
   425  				return
   426  			}
   427  			log.Warnf("Rolling back upgrade to version %d", targetVer-1)
   428  			errRollback := tx.Rollback()
   429  			if errRollback != nil {
   430  				log.Errorf("Rollback failed: %v", errRollback)
   431  			}
   432  		}()
   433  
   434  		if err = up(tx); err != nil {
   435  			return fmt.Errorf("failed to upgrade to db version %d: %w", targetVer, err)
   436  		}
   437  
   438  		if err = setDBVersion(tx, targetVer); err != nil {
   439  			return fmt.Errorf("failed to set new DB version %d: %w", targetVer, err)
   440  		}
   441  
   442  		err = tx.Commit() // for the defer
   443  		return err
   444  	}
   445  
   446  	log.Infof("Upgrading DB scheme from %d to %d", current, len(upgrades))
   447  	for i, up := range upgrades[current:] {
   448  		targetVer := current + uint32(i) + 1
   449  		log.Debugf("Upgrading DB scheme to %d...", targetVer)
   450  		if err = runUpgradeTx(targetVer, up); err != nil {
   451  			if errors.Is(err, sql.ErrTxDone) {
   452  				return fmt.Errorf("upgrade cancelled (rolled back to version %d)", current+uint32(i))
   453  			}
   454  			return err
   455  		}
   456  	}
   457  
   458  	current, err = DBVersion(db)
   459  	if err != nil {
   460  		return fmt.Errorf("failed to get DB version: %w", err)
   461  	}
   462  	log.Infof("Upgrades complete. DB is at version %d", current)
   463  	return nil
   464  }