1package repositories
2
3import (
4	"database/sql"
5	"strings"
6
7	"github.com/pkg/errors"
8	"github.com/satori/go.uuid"
9
10	"repodiff/constants"
11	e "repodiff/entities"
12	"repodiff/interactors"
13	"repodiff/mappers"
14	repoSQL "repodiff/persistence/sql"
15	"repodiff/utils"
16)
17
18type NullCommit struct {
19	originalErr error
20}
21
22func (n NullCommit) InsertCommitRows(commitRows []e.AnalyzedCommitRow) error {
23	return n.originalErr
24}
25func (n NullCommit) GetFirstSeenTimestamp(commitHashes []string, nullTimestamp e.RepoTimestamp) (map[string]e.RepoTimestamp, error) {
26	return nil, n.originalErr
27}
28func (n NullCommit) GetMostRecentCommits() ([]e.AnalyzedCommitRow, error) {
29	return nil, n.originalErr
30}
31
32type Commit struct {
33	db                 *sql.DB
34	target             e.MappedDiffTarget
35	timestampGenerator func() e.RepoTimestamp
36}
37
38func (c Commit) WithTimestampGenerator(t func() e.RepoTimestamp) Commit {
39	return Commit{
40		db:                 c.db,
41		target:             c.target,
42		timestampGenerator: t,
43	}
44}
45
46func (c Commit) InsertCommitRows(commitRows []e.AnalyzedCommitRow) error {
47	return errors.Wrap(
48		repoSQL.SingleTransactionInsert(
49			c.db,
50			`INSERT INTO project_commit (
51				upstream_target_id,
52				downstream_target_id,
53				timestamp,
54				uuid,
55				row_index,
56				commit_,
57				downstream_project,
58				author,
59				subject,
60				project_type
61			) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
62			mappers.PrependMappedDiffTarget(
63				c.target,
64				mappers.CommitRowsToPersistCols(commitRows, c.timestampGenerator()),
65			),
66		),
67		"Error inserting rows into project_commit",
68	)
69}
70
71func (c Commit) GetMostRecentOuterKey() (int64, uuid.UUID, error) {
72	var timestamp int64
73	var uuidBytes []byte
74	err := c.db.QueryRow(
75		`SELECT timestamp, uuid FROM project_commit WHERE upstream_target_id = ? AND downstream_target_id = ? AND timestamp=(
76			SELECT MAX(timestamp) FROM project_commit WHERE upstream_target_id = ? AND downstream_target_id = ?
77		) LIMIT 1`,
78		c.target.UpstreamTarget,
79		c.target.DownstreamTarget,
80		c.target.UpstreamTarget,
81		c.target.DownstreamTarget,
82	).Scan(
83		&timestamp,
84		&uuidBytes,
85	)
86	if err != nil {
87		return 0, constants.NullUUID(), err
88	}
89	u, err := uuid.FromBytes(uuidBytes)
90	if err != nil {
91		return 0, constants.NullUUID(), errors.Wrap(err, "Error casting string to UUID")
92	}
93	return timestamp, u, nil
94}
95
96func (c Commit) GetMostRecentCommits() ([]e.AnalyzedCommitRow, error) {
97	timestamp, uid, err := c.GetMostRecentOuterKey()
98	if err == sql.ErrNoRows {
99		return nil, nil
100	}
101	if err != nil {
102		return nil, err
103	}
104	var errMapping error
105
106	var commitRows []e.AnalyzedCommitRow
107	var commitCursor e.AnalyzedCommitRow
108	errSelect := repoSQL.Select(
109		c.db,
110		func(row *sql.Rows) {
111			if err := interactors.ExistingErrorOr(
112				errMapping,
113				func() error {
114					commitCursor, err = mappers.SQLRowToCommitRow(row)
115					return err
116				},
117			); err != nil {
118				errMapping = err
119				return
120			}
121			commitRows = append(
122				commitRows,
123				commitCursor,
124			)
125		},
126		`SELECT
127		  timestamp,
128			uuid,
129			row_index,
130			commit_,
131			downstream_project,
132			author,
133			subject,
134			project_type
135		FROM project_commit
136		WHERE
137		  upstream_target_id = ?
138			AND downstream_target_id = ?
139			AND timestamp = ?
140			AND uuid = ?`,
141		c.target.UpstreamTarget,
142		c.target.DownstreamTarget,
143		timestamp,
144		string(uid.Bytes()),
145	)
146	if err := interactors.AnyError(errSelect, errMapping); err != nil {
147		return nil, err
148	}
149	return commitRows, nil
150}
151
152func (c Commit) GetFirstSeenTimestamp(commitHashes []string, nullTimestamp e.RepoTimestamp) (map[string]e.RepoTimestamp, error) {
153	if len(commitHashes) == 0 {
154		return map[string]e.RepoTimestamp{}, nil
155	}
156	commitToTimestamp := make(map[string]e.RepoTimestamp, len(commitHashes))
157	var commitCursor string
158	var timestampCursor e.RepoTimestamp
159	var errMapping error
160
161	errSelect := repoSQL.Select(
162		c.db,
163		func(row *sql.Rows) {
164			if err := interactors.ExistingErrorOr(
165				errMapping,
166				func() error {
167					return row.Scan(
168						&commitCursor,
169						&timestampCursor,
170					)
171				},
172			); err != nil {
173				errMapping = err
174				return
175			}
176			commitToTimestamp[commitCursor] = timestampCursor
177		},
178		`SELECT commit_, MIN(timestamp)
179			FROM project_commit
180				WHERE upstream_target_id = ?
181					AND downstream_target_id = ?
182					AND commit_ IN(?`+strings.Repeat(",?", len(commitHashes)-1)+`)
183				GROUP BY commit_
184		`,
185		append(
186			[]interface{}{
187				c.target.UpstreamTarget,
188				c.target.DownstreamTarget,
189			},
190			asInterfaceSlice(commitHashes)...,
191		)...,
192	)
193	if err := interactors.AnyError(errSelect, errMapping); err != nil {
194		return nil, err
195	}
196	mutateEmptyValues(commitToTimestamp, commitHashes, nullTimestamp)
197	return commitToTimestamp, nil
198}
199
200func NewCommitRepository(target e.MappedDiffTarget) (Commit, error) {
201	db, err := repoSQL.GetDBConnectionPool()
202	return Commit{
203		db:                 db,
204		target:             target,
205		timestampGenerator: utils.TimestampSeconds,
206	}, errors.Wrap(err, "Could not establish a database connection")
207}
208
209func NewNullObject(originalErr error) NullCommit {
210	return NullCommit{
211		originalErr: originalErr,
212	}
213}
214
215func mutateEmptyValues(existing map[string]e.RepoTimestamp, shouldExist []string, defaultValue e.RepoTimestamp) {
216	for _, key := range shouldExist {
217		if _, ok := existing[key]; !ok {
218			existing[key] = defaultValue
219		}
220	}
221}
222
223func asInterfaceSlice(strings []string) []interface{} {
224	casted := make([]interface{}, len(strings))
225	for i, s := range strings {
226		casted[i] = s
227	}
228	return casted
229}
230