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 }