decred.org/dcrdex@v1.0.5/server/db/driver/pg/tables.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  
    12  	"decred.org/dcrdex/dex"
    13  	"decred.org/dcrdex/server/db/driver/pg/internal"
    14  )
    15  
    16  const (
    17  	marketsTableName      = "markets"
    18  	metaTableName         = "meta"
    19  	feeKeysTableName      = "fee_keys"
    20  	accountsTableName     = "accounts"
    21  	bondsTableName        = "bonds"
    22  	prepaidBondsTableName = "prepaid_bonds"
    23  
    24  	indexBondsOnAccountName  = "idx_bonds_on_acct"
    25  	indexBondsOnLockTimeName = "idx_bonds_on_locktime"
    26  	indexBondsOnCoinIDName   = "idx_bonds_on_coinid"
    27  
    28  	// market schema tables
    29  	matchesTableName         = "matches"
    30  	epochsTableName          = "epochs"
    31  	ordersArchivedTableName  = "orders_archived"
    32  	ordersActiveTableName    = "orders_active"
    33  	cancelsArchivedTableName = "cancels_archived"
    34  	cancelsActiveTableName   = "cancels_active"
    35  	epochReportsTableName    = "epoch_reports"
    36  	candlesTableName         = "candles"
    37  )
    38  
    39  type tableStmt struct {
    40  	name string
    41  	stmt string
    42  }
    43  
    44  var createDEXTableStatements = []tableStmt{
    45  	{marketsTableName, internal.CreateMarketsTable},
    46  	{metaTableName, internal.CreateMetaTable},
    47  }
    48  
    49  var createAccountTableStatements = []tableStmt{
    50  	{feeKeysTableName, internal.CreateFeeKeysTable},
    51  	{accountsTableName, internal.CreateAccountsTable},
    52  	{bondsTableName, internal.CreateBondsTable},
    53  	{prepaidBondsTableName, internal.CreatePrepaidBondsTable},
    54  }
    55  
    56  type indexStmt struct {
    57  	idxName string
    58  	stmt    string
    59  }
    60  
    61  var createBondIndexesStatements = []indexStmt{
    62  	{indexBondsOnAccountName, internal.CreateBondsAcctIndex},
    63  	{indexBondsOnLockTimeName, internal.CreateBondsLockTimeIndex},
    64  	{indexBondsOnCoinIDName, internal.CreateBondsCoinIDIndex},
    65  }
    66  
    67  var createMarketTableStatements = []tableStmt{
    68  	{ordersArchivedTableName, internal.CreateOrdersTable},
    69  	{ordersActiveTableName, internal.CreateOrdersTable},
    70  	{cancelsArchivedTableName, internal.CreateCancelOrdersTable},
    71  	{cancelsActiveTableName, internal.CreateCancelOrdersTable},
    72  	{matchesTableName, internal.CreateMatchesTable}, // just one matches table per market for now
    73  	{epochsTableName, internal.CreateEpochsTable},
    74  	{epochReportsTableName, internal.CreateEpochReportTable},
    75  }
    76  
    77  var tableMap = func() map[string]string {
    78  	m := make(map[string]string, len(createDEXTableStatements)+
    79  		len(createMarketTableStatements)+len(createAccountTableStatements))
    80  	for _, tbl := range createDEXTableStatements {
    81  		m[tbl.name] = tbl.stmt
    82  	}
    83  	for _, tbl := range createMarketTableStatements {
    84  		m[tbl.name] = tbl.stmt
    85  	}
    86  	for _, tbl := range createAccountTableStatements {
    87  		m[tbl.name] = tbl.stmt
    88  	}
    89  	return m
    90  }()
    91  
    92  func fullOrderTableName(dbName, marketSchema string, active bool) string {
    93  	var orderTable string
    94  	if active {
    95  		orderTable = ordersActiveTableName
    96  	} else {
    97  		orderTable = ordersArchivedTableName
    98  	}
    99  
   100  	return fullTableName(dbName, marketSchema, orderTable)
   101  }
   102  
   103  func fullCancelOrderTableName(dbName, marketSchema string, active bool) string {
   104  	var orderTable string
   105  	if active {
   106  		orderTable = cancelsActiveTableName
   107  	} else {
   108  		orderTable = cancelsArchivedTableName
   109  	}
   110  
   111  	return fullTableName(dbName, marketSchema, orderTable)
   112  }
   113  
   114  func fullMatchesTableName(dbName, marketSchema string) string {
   115  	return dbName + "." + marketSchema + "." + matchesTableName
   116  }
   117  
   118  func fullEpochsTableName(dbName, marketSchema string) string {
   119  	return dbName + "." + marketSchema + "." + epochsTableName
   120  }
   121  
   122  func fullEpochReportsTableName(dbName, marketSchema string) string {
   123  	return dbName + "." + marketSchema + "." + epochReportsTableName
   124  }
   125  
   126  func fullCandlesTableName(dbName, marketSchema string, candleDur uint64) string {
   127  	const fiveMin = 5 * 60 * 1000
   128  	const oneHour = 60 * 60 * 1000
   129  	const aDay = 24 * oneHour
   130  	var binSize string
   131  	switch candleDur {
   132  	case fiveMin:
   133  		binSize = "5m"
   134  	case oneHour:
   135  		binSize = "1h"
   136  	case aDay:
   137  		binSize = "24h"
   138  	default:
   139  		binSize = "epoch"
   140  	}
   141  	return dbName + "." + marketSchema + "." + candlesTableName + "_" + binSize
   142  }
   143  
   144  // createTable creates one of the known tables by name. The table will be
   145  // created in the specified schema (schema.tableName). If schema is empty,
   146  // "public" is used.
   147  func createTable(db sqlQueryExecutor, schema, tableName string) (bool, error) {
   148  	createCommand, tableNameFound := tableMap[tableName]
   149  	if !tableNameFound {
   150  		return false, fmt.Errorf("table name %q unknown", tableName)
   151  	}
   152  
   153  	if schema == "" {
   154  		schema = publicSchema
   155  	}
   156  	return createTableStmt(db, createCommand, schema, tableName)
   157  }
   158  
   159  // prepareTables ensures that all tables required by the DEX market config,
   160  // mktConfig, are ready. This also runs any required DB scheme upgrades. The
   161  // Context allows safely canceling upgrades, which may be long running. Returns
   162  // a slice of markets that should have orders flushed due to lot size changes.
   163  func prepareTables(ctx context.Context, db *sql.DB, mktConfig []*dex.MarketInfo) ([]string, error) {
   164  	// Create the markets table in the public schema.
   165  	created, err := createTable(db, publicSchema, marketsTableName)
   166  	if err != nil {
   167  		return nil, fmt.Errorf("failed to create markets table: %w", err)
   168  	}
   169  	if created { // Fresh install
   170  		// Create the meta table in the public schema.
   171  		created, err = createTable(db, publicSchema, metaTableName)
   172  		if err != nil {
   173  			return nil, fmt.Errorf("failed to create meta table: %w", err)
   174  		}
   175  		if !created {
   176  			return nil, fmt.Errorf("existing meta table but no markets table: corrupt DB")
   177  		}
   178  		_, err = db.Exec(internal.CreateMetaRow)
   179  		if err != nil {
   180  			return nil, fmt.Errorf("failed to create row for meta table: %w", err)
   181  		}
   182  		err = setDBVersion(db, dbVersion) // no upgrades
   183  		if err != nil {
   184  			return nil, fmt.Errorf("failed to set db version in meta table: %w", err)
   185  		}
   186  		log.Infof("Created new meta table at version %d", dbVersion)
   187  	}
   188  	// Prepare the account and registration key counter tables.
   189  	if err = createAccountTables(db); err != nil {
   190  		return nil, err
   191  	}
   192  	if !created {
   193  		// Attempt upgrade.
   194  		if err = upgradeDB(ctx, db); err != nil {
   195  			// If the context is canceled, it will either be context.Canceled
   196  			// from db.BeginTx, or sql.ErrTxDone from any of the tx operations.
   197  			if errors.Is(err, context.Canceled) || errors.Is(err, sql.ErrTxDone) {
   198  				return nil, fmt.Errorf("upgrade DB canceled: %w", err)
   199  			}
   200  			return nil, fmt.Errorf("upgrade DB failed: %w", err)
   201  		}
   202  	}
   203  
   204  	// Verify config of existing markets, creating a new markets table if none
   205  	// exists. This is done after upgrades since it can create new tables with
   206  	// the current DB scheme for newly configured markets.
   207  	log.Infof("Configuring %d markets tables: %v", len(mktConfig), mktConfig)
   208  	return prepareMarkets(db, mktConfig)
   209  }
   210  
   211  // prepareMarkets ensures that the market-specific tables required by the DEX
   212  // market config, mktConfig, are ready. See also prepareTables.
   213  func prepareMarkets(db *sql.DB, mktConfig []*dex.MarketInfo) ([]string, error) {
   214  	// Load existing markets and ensure there aren't multiple with the same ID.
   215  	mkts, err := loadMarkets(db, marketsTableName)
   216  	if err != nil {
   217  		return nil, fmt.Errorf("failed to read markets table: %w", err)
   218  	}
   219  	marketMap := make(map[string]*dex.MarketInfo, len(mkts))
   220  	for _, mkt := range mkts {
   221  		if _, found := marketMap[mkt.Name]; found {
   222  			// should never happen since market name is (unique) primary key
   223  			panic(fmt.Sprintf(`multiple markets with the same name "%s" found!`,
   224  				mkt.Name))
   225  		}
   226  		marketMap[mkt.Name] = mkt
   227  	}
   228  
   229  	var purgeMarkets []string
   230  	// Create any markets in the config that do not already exist. Also create
   231  	// any missing tables for existing markets.
   232  	for _, mkt := range mktConfig {
   233  		existingMkt := marketMap[mkt.Name]
   234  		if existingMkt == nil {
   235  			log.Infof("New market specified in config: %s", mkt.Name)
   236  			err = newMarket(db, marketsTableName, mkt)
   237  			if err != nil {
   238  				return nil, fmt.Errorf("newMarket failed: %w", err)
   239  			}
   240  		} else {
   241  			if mkt.LotSize != existingMkt.LotSize {
   242  				err = updateLotSize(db, publicSchema, mkt.Name, mkt.LotSize)
   243  				if err != nil {
   244  					return nil, fmt.Errorf("unable to update lot size for %s: %w", mkt.Name, err)
   245  				}
   246  				// archiver.markets use market schema name.
   247  				schema := marketSchema(mkt.Name)
   248  				purgeMarkets = append(purgeMarkets, schema)
   249  			}
   250  		}
   251  
   252  		// Create the tables in the markets schema.
   253  		err = createMarketTables(db, mkt.Name)
   254  		if err != nil {
   255  			return nil, fmt.Errorf("createMarketTables failed: %w", err)
   256  		}
   257  	}
   258  
   259  	return purgeMarkets, nil
   260  }
   261  
   262  // updateLotSize updates the lot size for a market. Must only be called on an
   263  // existing market.
   264  func updateLotSize(db sqlQueryExecutor, schema, mktName string, lotSize uint64) error {
   265  	if schema == "" {
   266  		schema = publicSchema
   267  	}
   268  	nameSpacedTable := schema + "." + marketsTableName
   269  	stmt := fmt.Sprintf(internal.UpdateLotSize, nameSpacedTable)
   270  	_, err := db.Exec(stmt, mktName, lotSize)
   271  	if err != nil {
   272  		return err
   273  	}
   274  	log.Debugf("Updated %s lot size to %d.", mktName, lotSize)
   275  	return nil
   276  }