github.com/decred/politeia@v1.4.0/politeiawww/legacy/codetracker/github/database/cockroachdb/cockroachdb.go (about) 1 // Copyright (c) 2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package cockroachdb 6 7 import ( 8 "fmt" 9 "net/url" 10 "path/filepath" 11 "sync" 12 "time" 13 14 "github.com/decred/politeia/politeiawww/legacy/codetracker/github/database" 15 "github.com/jinzhu/gorm" 16 _ "github.com/jinzhu/gorm/dialects/postgres" 17 ) 18 19 const ( 20 cacheID = "ghtracker" 21 22 // Database table names 23 tableNameVersions = "versions" 24 tableNamePullRequest = "pullrequests" 25 tableNameCommits = "commits" 26 tableNameReviews = "reviews" 27 28 userPoliteiawww = "politeiawww" // cmsdb user (read/write access) 29 ) 30 31 // cockroachdb implements the cache interface. 32 type cockroachdb struct { 33 sync.RWMutex 34 shutdown bool // Backend is shutdown 35 recordsdb *gorm.DB // Database context 36 } 37 38 // NewPullRequest creates a new pull request row in the db. 39 func (c *cockroachdb) NewPullRequest(dbPullRequest *database.PullRequest) error { 40 pr := EncodePullRequest(dbPullRequest) 41 42 log.Debugf("NewPullRequest: %v", pr.URL) 43 return c.recordsdb.Create(&pr).Error 44 } 45 46 // UpdatePullRequest updates an existing pull request row in the db. 47 func (c *cockroachdb) UpdatePullRequest(dbPullRequest *database.PullRequest) error { 48 pr := EncodePullRequest(dbPullRequest) 49 50 log.Debugf("UpdatePullRequest: %v", pr.URL) 51 return c.recordsdb.Save(&pr).Error 52 } 53 54 // PullRequestByURL return a PullRequest by its URL. 55 func (c *cockroachdb) PullRequestByID(id string) (*database.PullRequest, error) { 56 log.Debugf("PullRequestByID: %v", id) 57 58 pr := PullRequest{ 59 ID: id, 60 } 61 err := c.recordsdb. 62 Find(&pr).Error 63 if err != nil { 64 if err == gorm.ErrRecordNotFound { 65 err = database.ErrNoPullRequestFound 66 } 67 return nil, err 68 } 69 70 return DecodePullRequest(&pr), nil 71 } 72 73 // MergedPullRequestsByUserDates takes a username, start and end date and 74 // returns merged pull requests that match those criteria. 75 func (c *cockroachdb) MergedPullRequestsByUserDates(username string, start, end int64) ([]*database.PullRequest, error) { 76 log.Debugf("MergedPullRequestsByUserDates: %v %v", time.Unix(start, 0), 77 time.Unix(end, 0)) 78 79 // Get all PRs from a user between the given dates. 80 prs := make([]PullRequest, 0, 1024) // PNOOMA 81 err := c.recordsdb. 82 Where("author = ? AND "+ 83 "merged_at BETWEEN ? AND ?", 84 username, 85 start, 86 end). 87 Find(&prs). 88 Error 89 if err != nil { 90 return nil, err 91 } 92 dbPRs := make([]*database.PullRequest, 0, len(prs)) 93 for _, vv := range prs { 94 dbPRs = append(dbPRs, DecodePullRequest(&vv)) 95 } 96 return dbPRs, nil 97 } 98 99 // UpdatedPullRequestsByUserDates takes a username, start and end date and 100 // returns updated pull requests that match those criteria. 101 func (c *cockroachdb) UpdatedPullRequestsByUserDates(username string, start, end int64) ([]*database.PullRequest, error) { 102 log.Debugf("UpdatedPullRequestsByUserDates: %v %v", time.Unix(start, 0), 103 time.Unix(end, 0)) 104 105 // Select the most recent pullrequests (by url) that match author and are 106 // between start and end. 107 query := ` 108 SELECT * FROM pullrequests 109 WHERE author = $1 AND updated_at IN 110 (SELECT 111 MAX(updated_at) 112 FROM pullrequests 113 WHERE updated_at BETWEEN $2 AND $3 114 GROUP BY url 115 )` 116 rows, err := c.recordsdb.Raw(query, username, start, end).Rows() 117 if err != nil { 118 return nil, err 119 } 120 defer rows.Close() 121 122 prs := make([]PullRequest, 0, 1024) // PNOOMA 123 for rows.Next() { 124 var pr PullRequest 125 err := c.recordsdb.ScanRows(rows, &pr) 126 if err != nil { 127 return nil, err 128 } 129 prs = append(prs, pr) 130 } 131 if err = rows.Err(); err != nil { 132 return nil, err 133 } 134 dbPRs := make([]*database.PullRequest, 0, len(prs)) 135 for _, pr := range prs { 136 dbPRs = append(dbPRs, DecodePullRequest(&pr)) 137 } 138 return dbPRs, nil 139 } 140 141 // PullRequestByURL takes a url and returns all PRs that may match. 142 func (c *cockroachdb) PullRequestsByURL(url string) ([]*database.PullRequest, error) { 143 urlPRs := make([]PullRequest, 0, 1024) // PNOOMA 144 err := c.recordsdb. 145 Where("url = ?", 146 url). 147 Find(&urlPRs). 148 Error 149 if err != nil { 150 return nil, err 151 } 152 dbPRs := make([]*database.PullRequest, 0, len(urlPRs)) 153 for _, pr := range urlPRs { 154 dbPRs = append(dbPRs, DecodePullRequest(&pr)) 155 } 156 return dbPRs, nil 157 } 158 159 // MatchingReviews is a struct that will return results from a review 160 // request. 161 type MatchingReviews struct { 162 PullRequestURL string 163 ID int64 164 Author string 165 State string 166 SubmittedAt int64 167 CommitID string 168 Repo string 169 Number int 170 Additions int 171 Deletions int 172 } 173 174 // ReviewsByUserDates takes username, start and end date and returns all reviews 175 // that match the provided criteria. 176 func (c *cockroachdb) ReviewsByUserDates(username string, start, end int64) ([]database.PullRequestReview, error) { 177 log.Debugf("ReviewsByUserDates: %v %v", time.Unix(start, 0), 178 time.Unix(end, 0)) 179 180 // Get all Reviews from a user between the given dates. 181 query := ` 182 SELECT 183 reviews.pull_request_url, 184 reviews.id, 185 reviews.author, 186 reviews.state, 187 reviews.submitted_at, 188 reviews.commit_id, 189 reviews.repo, 190 reviews.number, 191 pullrequests.additions, 192 pullrequests.deletions 193 FROM reviews 194 INNER JOIN pullrequests 195 ON pullrequests.url = reviews.pull_request_url 196 WHERE reviews.author = $1 AND reviews.state = $2 AND 197 reviews.submitted_at BETWEEN $3 AND $4` 198 199 rows, err := c.recordsdb.Raw(query, username, "APPROVED", start, end).Rows() 200 if err != nil { 201 return nil, err 202 } 203 defer rows.Close() 204 205 matching := make([]MatchingReviews, 0, 1024) 206 for rows.Next() { 207 var i MatchingReviews 208 err := c.recordsdb.ScanRows(rows, &i) 209 if err != nil { 210 return nil, err 211 } 212 matching = append(matching, i) 213 } 214 if err = rows.Err(); err != nil { 215 return nil, err 216 } 217 218 return convertMatchingReviewsToDatabaseReviews(matching), nil 219 } 220 221 // NewPullRequestReview creates a new pull request review row. 222 func (c *cockroachdb) NewPullRequestReview(dbPullRequestReview *database.PullRequestReview) error { 223 pr := EncodePullRequestReview(dbPullRequestReview) 224 225 log.Debugf("NewPullRequestReview: %v", pr.CommitID) 226 return c.recordsdb.Create(&pr).Error 227 } 228 229 // UpdatePullRequestReview updates an existing pull request review row. 230 func (c *cockroachdb) UpdatePullRequestReview(dbPullRequestReview *database.PullRequestReview) error { 231 pr := EncodePullRequestReview(dbPullRequestReview) 232 233 log.Debugf("UpdatePullRequestReview: %v", pr.CommitID) 234 return c.recordsdb.Save(&pr).Error 235 } 236 237 // This function must be called within a transaction. 238 func createGHTables(tx *gorm.DB) error { 239 log.Infof("createGHTables") 240 241 // Create cms tables 242 if !tx.HasTable(tableNamePullRequest) { 243 err := tx.CreateTable(&PullRequest{}).Error 244 if err != nil { 245 return err 246 } 247 } 248 if !tx.HasTable(tableNameReviews) { 249 err := tx.CreateTable(&PullRequestReview{}).Error 250 if err != nil { 251 return err 252 } 253 } 254 if !tx.HasTable(tableNameCommits) { 255 err := tx.CreateTable(&Commit{}).Error 256 if err != nil { 257 return err 258 } 259 } 260 261 return nil 262 263 } 264 265 // ReviewByID returns a review given the provided id. 266 func (c *cockroachdb) ReviewByID(id int64) (*database.PullRequestReview, error) { 267 log.Debugf("ReviewByID: %v", id) 268 269 review := PullRequestReview{ 270 ID: id, 271 } 272 err := c.recordsdb. 273 Find(&review).Error 274 if err != nil { 275 if err == gorm.ErrRecordNotFound { 276 err = database.ErrNoPullRequestReviewFound 277 } 278 return nil, err 279 } 280 281 return DecodePullRequestReview(&review), nil 282 } 283 284 // NewCommit creates a new commit row. 285 func (c *cockroachdb) NewCommit(dbCommit *database.Commit) error { 286 commit := encodeCommit(dbCommit) 287 288 log.Debugf("NewCommit: %v", commit.SHA) 289 return c.recordsdb.Create(&commit).Error 290 } 291 292 // CommitBySHA returns a commit given the provided hash. 293 func (c *cockroachdb) CommitBySHA(sha string) (*database.Commit, error) { 294 log.Debugf("CommitBySHA: %v", sha) 295 296 commit := Commit{ 297 SHA: sha, 298 } 299 err := c.recordsdb. 300 Find(&commit).Error 301 if err != nil { 302 if err == gorm.ErrRecordNotFound { 303 err = database.ErrNoCommitFound 304 } 305 return nil, err 306 } 307 308 return decodeCommit(&commit), nil 309 } 310 311 // CommitsByUserDates takes a username, start and end date and 312 // returns commits that match those criteria. 313 func (c *cockroachdb) CommitsByUserDates(username string, start, end int64) ([]database.Commit, error) { 314 log.Debugf("CommitsByUserDates: %v %v", time.Unix(start, 0), 315 time.Unix(end, 0)) 316 317 // Get all commits from a user between the given dates. 318 commits := make([]Commit, 0, 1024) // PNOOMA 319 err := c.recordsdb. 320 Where("author = ? AND "+ 321 "date BETWEEN ? AND ?", 322 username, 323 start, 324 end). 325 Find(&commits). 326 Error 327 if err != nil { 328 return nil, err 329 } 330 dbCommits := make([]database.Commit, 0, len(commits)) 331 for _, vv := range commits { 332 dbCommits = append(dbCommits, *decodeCommit(&vv)) 333 } 334 return dbCommits, nil 335 } 336 337 // Setup calls the tables creation function to ensure the database is prepared 338 // for use. 339 func (c *cockroachdb) Setup() error { 340 tx := c.recordsdb.Begin() 341 err := createGHTables(tx) 342 if err != nil { 343 tx.Rollback() 344 return err 345 } 346 347 return tx.Commit().Error 348 } 349 350 func buildQueryString(user, rootCert, cert, key string) string { 351 v := url.Values{} 352 v.Set("sslmode", "require") 353 v.Set("sslrootcert", filepath.Clean(rootCert)) 354 v.Set("sslcert", filepath.Join(cert)) 355 v.Set("sslkey", filepath.Join(key)) 356 return v.Encode() 357 } 358 359 // New returns a new cockroachdb context that contains a connection to the 360 // specified database that was made using the politeiawww user and the passed 361 // in certificates. 362 func New(host, rootCert, cert, key string) (*cockroachdb, error) { 363 log.Tracef("New: %v %v %v %v %v", host, rootCert, cert, key) 364 365 // Connect to database 366 dbName := cacheID 367 h := "postgresql://" + userPoliteiawww + "@" + host + "/" + dbName 368 u, err := url.Parse(h) 369 if err != nil { 370 return nil, fmt.Errorf("parse url '%v': %v", h, err) 371 } 372 373 qs := buildQueryString(u.User.String(), rootCert, cert, key) 374 addr := u.String() + "?" + qs 375 db, err := gorm.Open("postgres", addr) 376 if err != nil { 377 return nil, fmt.Errorf("connect to database '%v': %v", addr, err) 378 } 379 380 // Create context 381 c := &cockroachdb{ 382 recordsdb: db, 383 } 384 385 // Disable gorm logging. This prevents duplicate errors from 386 // being printed since we handle errors manually. 387 c.recordsdb.LogMode(false) 388 389 // Disable automatic table name pluralization. We set table 390 // names manually. 391 c.recordsdb.SingularTable(true) 392 return c, err 393 } 394 395 // Close satisfies the database interface. 396 func (c *cockroachdb) Close() error { 397 return c.recordsdb.Close() 398 }