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 }