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  }