Sat, 22 Jul 2023 22:32:04 +0200
add full support for commit references - fixes #276
1 /*
2 * Copyright 2021 Mike Becker. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are met:
6 *
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 *
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
26 package de.uapcore.lightpit.dao
28 import de.uapcore.lightpit.entities.*
29 import de.uapcore.lightpit.types.CommitRef
30 import de.uapcore.lightpit.types.IssueHistoryType
31 import de.uapcore.lightpit.types.RelationType
32 import de.uapcore.lightpit.types.WebColor
33 import de.uapcore.lightpit.viewmodel.ComponentSummary
34 import de.uapcore.lightpit.viewmodel.IssueSummary
35 import de.uapcore.lightpit.viewmodel.VersionSummary
36 import org.intellij.lang.annotations.Language
37 import java.sql.Connection
38 import java.sql.PreparedStatement
39 import java.sql.ResultSet
41 class PostgresDataAccessObject(private val connection: Connection) : DataAccessObject {
43 /**
44 * Prepares the given [sql] statement and executes the [block] function with the prepared statement as receiver.
45 * The statement is then closed properly.
46 */
47 private fun <R> withStatement(@Language("SQL") sql: String, block: PreparedStatement.() -> R) =
48 connection.prepareStatement(sql).use(block)
50 /**
51 * Prepares the given [sql] statement and executes the [block] function on that statement.
52 * The statement is then closed properly.
53 */
54 private fun <R> useStatement(@Language("SQL") sql: String, block: (PreparedStatement) -> R) =
55 connection.prepareStatement(sql).use(block)
57 /**
58 * Executes the statement and iterates the whole result set extracting the rows with the given [extractor] function.
59 */
60 private fun <T> PreparedStatement.queryAll(extractor: (ResultSet) -> T): List<T> = executeQuery().use {
61 sequence {
62 while (it.next()) {
63 yield(extractor(it))
64 }
65 }.toList()
66 }
68 /**
69 * Executes the statement and extracts a single row with the given [extractor] function.
70 * If the result set is empty, null is returned.
71 */
72 private fun <T> PreparedStatement.querySingle(extractor: (ResultSet) -> T): T? = executeQuery().use {
73 return if (it.next()) extractor(it) else null
74 }
76 //<editor-fold desc="User">
77 //language=SQL
78 private val userQuery = "select userid, username, lastname, givenname, mail from lpit_user"
80 private fun ResultSet.extractUser() = User(getInt("userid")).apply {
81 username = getString("username")
82 givenname = getString("givenname")
83 lastname = getString("lastname")
84 mail = getString("mail")
85 }
87 private fun ResultSet.containsUserInfo(): Boolean {
88 getInt("userid")
89 return !wasNull()
90 }
92 private fun ResultSet.extractOptionalUser() = if (containsUserInfo()) extractUser() else null
94 override fun listUsers() =
95 withStatement("$userQuery where userid > 0 order by username") {
96 queryAll { it.extractUser() }
97 }
99 override fun findUser(id: Int): User? =
100 withStatement("$userQuery where userid = ?") {
101 setInt(1, id)
102 querySingle { it.extractUser() }
103 }
105 override fun findUserByName(username: String): User? =
106 withStatement("$userQuery where lower(username) = lower(?)") {
107 setString(1, username)
108 querySingle { it.extractUser() }
109 }
111 override fun insertUser(user: User) {
112 withStatement("insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)") {
113 with(user) {
114 setStringSafe(1, username)
115 setStringOrNull(2, lastname)
116 setStringOrNull(3, givenname)
117 setStringOrNull(4, mail)
118 }
119 executeUpdate()
120 }
121 }
123 override fun updateUser(user: User) {
124 withStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?") {
125 with(user) {
126 setStringOrNull(1, lastname)
127 setStringOrNull(2, givenname)
128 setStringOrNull(3, mail)
129 setInt(4, id)
130 }
131 executeUpdate()
132 }
133 }
134 //</editor-fold>
136 //<editor-fold desc="Version">
137 //language=SQL
138 private val versionQuery = "select versionid, project, name, node, ordinal, status, release, eol from lpit_version"
140 private fun ResultSet.extractVersion() =
141 Version(getInt("versionid"), getInt("project")).apply {
142 name = getString("name")
143 node = getString("node")
144 ordinal = getInt("ordinal")
145 release = getDate("release")
146 eol = getDate("eol")
147 status = getEnum("status")
148 }
150 override fun listVersions(project: Project): List<Version> =
151 withStatement("$versionQuery where project = ? order by ordinal desc, lower(name) desc") {
152 setInt(1, project.id)
153 queryAll { it.extractVersion() }
154 }
156 override fun listVersionSummaries(project: Project): List<VersionSummary> =
157 withStatement(
158 """with
159 version_map as (
160 select issueid, status, resolved as versionid, true as isresolved from lpit_issue
161 union all
162 select issueid, status, affected as versionid, false as isresolved from lpit_issue
163 ), issues as (
164 select versionid, phase, isresolved, count(issueid) as total from version_map
165 join lpit_issue_phases using (status)
166 group by versionid, phase, isresolved
167 ),
168 summary as (
169 select versionid, phase, isresolved, total
170 from lpit_version v
171 left join issues using (versionid)
172 )
173 select v.versionid, project, name, node, ordinal, status, release, eol,
174 ro.total as resolved_open, ra.total as resolved_active, rd.total as resolved_done,
175 ao.total as affected_open, aa.total as affected_active, ad.total as affected_done
176 from lpit_version v
177 left join summary ro on ro.versionid = v.versionid and ro.phase = 0 and ro.isresolved
178 left join summary ra on ra.versionid = v.versionid and ra.phase = 1 and ra.isresolved
179 left join summary rd on rd.versionid = v.versionid and rd.phase = 2 and rd.isresolved
180 left join summary ao on ao.versionid = v.versionid and ao.phase = 0 and not ao.isresolved
181 left join summary aa on aa.versionid = v.versionid and aa.phase = 1 and not aa.isresolved
182 left join summary ad on ad.versionid = v.versionid and ad.phase = 2 and not ad.isresolved
183 where v.project = ?
184 order by ordinal desc, lower(name) desc
185 """.trimIndent()
186 ) {
187 setInt(1, project.id)
188 queryAll { rs ->
189 VersionSummary(rs.extractVersion()).apply {
190 reportedTotal.open = rs.getInt("affected_open")
191 reportedTotal.active = rs.getInt("affected_active")
192 reportedTotal.done = rs.getInt("affected_done")
193 resolvedTotal.open = rs.getInt("resolved_open")
194 resolvedTotal.active = rs.getInt("resolved_active")
195 resolvedTotal.done = rs.getInt("resolved_done")
196 }
197 }
198 }
200 override fun findVersion(id: Int): Version? =
201 withStatement("$versionQuery where versionid = ?") {
202 setInt(1, id)
203 querySingle { it.extractVersion() }
204 }
206 override fun findVersionByNode(project: Project, node: String): Version? =
207 withStatement("$versionQuery where project = ? and node = ?") {
208 setInt(1, project.id)
209 setString(2, node)
210 querySingle { it.extractVersion() }
211 }
213 override fun insertVersion(version: Version) {
214 withStatement("insert into lpit_version (name, node, ordinal, status, project, release, eol) values (?, ?, ?, ?::version_status, ?, ?, ?)") {
215 with(version) {
216 setStringSafe(1, name)
217 setStringSafe(2, node)
218 setInt(3, ordinal)
219 setEnum(4, status)
220 setInt(5, projectid)
221 setDateOrNull(6, release)
222 setDateOrNull(7, eol)
223 }
224 executeUpdate()
225 }
227 }
229 override fun updateVersion(version: Version) {
230 withStatement("update lpit_version set name = ?, node = ?, ordinal = ?, status = ?::version_status, release=?,eol=? where versionid = ?") {
231 with(version) {
232 setStringSafe(1, name)
233 setStringSafe(2, node)
234 setInt(3, ordinal)
235 setEnum(4, status)
236 setDateOrNull(5, version.release)
237 setDateOrNull(6, version.eol)
238 setInt(7, id)
239 }
240 executeUpdate()
241 }
242 }
243 //</editor-fold>
245 //<editor-fold desc="Component">
246 //language=SQL
247 private val componentQuery =
248 """
249 select id, project, name, node, color, ordinal, description, active,
250 userid, username, givenname, lastname, mail
251 from lpit_component
252 left join lpit_user on lead = userid
253 """.trimIndent()
255 private fun ResultSet.extractComponent(): Component =
256 Component(getInt("id"), getInt("project")).apply {
257 name = getString("name")
258 node = getString("node")
259 color = try {
260 WebColor(getString("color"))
261 } catch (ex: IllegalArgumentException) {
262 WebColor("000000")
263 }
264 ordinal = getInt("ordinal")
265 description = getString("description")
266 active = getBoolean("active")
267 lead = extractOptionalUser()
268 }
270 private fun PreparedStatement.setComponent(index: Int, component: Component): Int {
271 with(component) {
272 var i = index
273 setStringSafe(i++, name)
274 setStringSafe(i++, node)
275 setStringSafe(i++, color.hex)
276 setInt(i++, ordinal)
277 setStringOrNull(i++, description)
278 setBoolean(i++, active)
279 setIntOrNull(i++, lead?.id)
280 return i
281 }
282 }
284 override fun listComponents(project: Project): List<Component> =
285 withStatement("$componentQuery where project = ? order by ordinal, lower(name)") {
286 setInt(1, project.id)
287 queryAll { it.extractComponent() }
288 }
290 override fun listComponentSummaries(project: Project): List<ComponentSummary> =
291 withStatement(
292 """
293 with issues as (
294 select component, phase, count(issueid) as total
295 from lpit_issue
296 join lpit_issue_phases using (status)
297 group by component, phase
298 ),
299 summary as (
300 select c.id, phase, total
301 from lpit_component c
302 left join issues i on c.id = i.component
303 )
304 select c.id, project, name, node, color, ordinal, description, active,
305 userid, username, givenname, lastname, mail,
306 open.total as open, wip.total as wip, done.total as done
307 from lpit_component c
308 left join lpit_user on lead = userid
309 left join summary open on c.id = open.id and open.phase = 0
310 left join summary wip on c.id = wip.id and wip.phase = 1
311 left join summary done on c.id = done.id and done.phase = 2
312 where c.project = ?
313 order by ordinal, name
314 """.trimIndent()
315 ) {
316 setInt(1, project.id)
317 queryAll { rs ->
318 ComponentSummary(rs.extractComponent()).apply {
319 issueSummary.open = rs.getInt("open")
320 issueSummary.active = rs.getInt("wip")
321 issueSummary.done = rs.getInt("done")
322 }
323 }
324 }
326 override fun findComponent(id: Int): Component? =
327 withStatement("$componentQuery where id = ?") {
328 setInt(1, id)
329 querySingle { it.extractComponent() }
330 }
332 override fun findComponentByNode(project: Project, node: String): Component? =
333 withStatement("$componentQuery where project = ? and node = ?") {
334 setInt(1, project.id)
335 setString(2, node)
336 querySingle { it.extractComponent() }
337 }
339 override fun insertComponent(component: Component) {
340 withStatement("insert into lpit_component (name, node, color, ordinal, description, active, lead, project) values (?, ?, ?, ?, ?, ?, ?, ?)") {
341 val col = setComponent(1, component)
342 setInt(col, component.projectid)
343 executeUpdate()
344 }
345 }
347 override fun updateComponent(component: Component) {
348 withStatement("update lpit_component set name = ?, node = ?, color = ?, ordinal = ?, description = ?, active = ?, lead = ? where id = ?") {
349 val col = setComponent(1, component)
350 setInt(col, component.id)
351 executeUpdate()
352 }
353 }
355 //</editor-fold>
357 //<editor-fold desc="Project">
359 //language=SQL
360 private val projectQuery =
361 """
362 select projectid, name, node, ordinal, description, vcs, repourl,
363 userid, username, lastname, givenname, mail
364 from lpit_project
365 left join lpit_user owner on lpit_project.owner = owner.userid
366 """.trimIndent()
368 private fun ResultSet.extractProject() =
369 Project(getInt("projectid")).apply {
370 name = getString("name")
371 node = getString("node")
372 ordinal = getInt("ordinal")
373 description = getString("description")
374 vcs = getEnum("vcs")
375 repoUrl = getString("repourl")
376 owner = extractOptionalUser()
377 }
379 private fun PreparedStatement.setProject(index: Int, project: Project): Int {
380 var i = index
381 with(project) {
382 setStringSafe(i++, name)
383 setStringSafe(i++, node)
384 setInt(i++, ordinal)
385 setStringOrNull(i++, description)
386 setEnum(i++, vcs)
387 setStringOrNull(i++, repoUrl)
388 setIntOrNull(i++, owner?.id)
389 }
390 return i
391 }
393 override fun listProjects(): List<Project> =
394 withStatement("$projectQuery order by ordinal, lower(name)") {
395 queryAll { it.extractProject() }
396 }
398 override fun findProject(id: Int): Project? =
399 withStatement("$projectQuery where projectid = ?") {
400 setInt(1, id)
401 querySingle { it.extractProject() }
402 }
404 override fun findProjectByNode(node: String): Project? =
405 withStatement("$projectQuery where node = ?") {
406 setString(1, node)
407 querySingle { it.extractProject() }
408 }
410 override fun insertProject(project: Project) {
411 withStatement("insert into lpit_project (name, node, ordinal, description, vcs, repourl, owner) values (?, ?, ?, ?, ?::vcstype, ?, ?)") {
412 setProject(1, project)
413 executeUpdate()
414 }
415 }
417 override fun updateProject(project: Project) {
418 withStatement("update lpit_project set name = ?, node = ?, ordinal = ?, description = ?, vcs = ?::vcstype, repourl = ?, owner = ? where projectid = ?") {
419 val col = setProject(1, project)
420 setInt(col, project.id)
421 executeUpdate()
422 }
423 }
425 override fun collectIssueSummary(project: Project): IssueSummary =
426 withStatement(
427 """
428 select phase, count(*) as total
429 from lpit_issue
430 join lpit_issue_phases using(status)
431 where project = ?
432 group by phase
433 """.trimIndent()
434 ) {
435 setInt(1, project.id)
436 executeQuery().use {
437 val summary = IssueSummary()
438 while (it.next()) {
439 val phase = it.getInt("phase")
440 val total = it.getInt("total")
441 when (phase) {
442 0 -> summary.open = total
443 1 -> summary.active = total
444 2 -> summary.done = total
445 }
446 }
447 summary
448 }
449 }
451 override fun collectIssueSummary(assignee: User): IssueSummary =
452 withStatement(
453 """
454 select phase, count(*) as total
455 from lpit_issue
456 join lpit_issue_phases using(status)
457 where assignee = ?
458 group by phase
459 """.trimIndent()
460 ) {
461 setInt(1, assignee.id)
462 executeQuery().use {
463 val summary = IssueSummary()
464 while (it.next()) {
465 val phase = it.getInt("phase")
466 val total = it.getInt("total")
467 when (phase) {
468 0 -> summary.open = total
469 1 -> summary.active = total
470 2 -> summary.done = total
471 }
472 }
473 summary
474 }
475 }
477 override fun mergeCommitRefs(refs: List<CommitRef>) {
478 withStatement("insert into lpit_commit_ref (issueid, commit_hash, commit_brief) values (?,?,?) on conflict do nothing") {
479 refs.forEach { ref ->
480 setInt(1, ref.issueId)
481 setString(2, ref.hash)
482 setString(3, ref.message)
483 executeUpdate()
484 }
485 }
486 }
488 //</editor-fold>
490 //<editor-fold desc="Issue">
492 //language=SQL
493 private val issueQuery =
494 """
495 select issueid,
496 i.project, p.name as projectname, p.node as projectnode,
497 component, c.name as componentname, c.node as componentnode,
498 status, phase, category, subject, i.description,
499 userid, username, givenname, lastname, mail,
500 created, updated, eta, affected, resolved
501 from lpit_issue i
502 join lpit_project p on i.project = projectid
503 join lpit_issue_phases using (status)
504 left join lpit_component c on component = c.id
505 left join lpit_user on userid = assignee
506 """.trimIndent()
508 private fun ResultSet.extractIssue(): Issue {
509 val proj = Project(getInt("project")).apply {
510 name = getString("projectname")
511 node = getString("projectnode")
512 }
513 val comp = getInt("component").let {
514 if (wasNull()) null else
515 Component(it, proj.id).apply {
516 name = getString("componentname")
517 node = getString("componentnode")
518 }
519 }
520 val issue = Issue(getInt("issueid"), proj).apply {
521 component = comp
522 status = getEnum("status")
523 category = getEnum("category")
524 subject = getString("subject")
525 description = getString("description")
526 assignee = extractOptionalUser()
527 created = getTimestamp("created")
528 updated = getTimestamp("updated")
529 eta = getDate("eta")
530 affected = getInt("affected").takeIf { it > 0 }?.let { findVersion(it) }
531 resolved = getInt("resolved").takeIf { it > 0 }?.let { findVersion(it) }
532 }
534 return issue
535 }
537 private fun PreparedStatement.setIssue(index: Int, issue: Issue): Int {
538 var i = index
539 with(issue) {
540 setIntOrNull(i++, component?.id)
541 setEnum(i++, status)
542 setEnum(i++, category)
543 setStringSafe(i++, subject)
544 setStringOrNull(i++, description)
545 setIntOrNull(i++, assignee?.id)
546 setDateOrNull(i++, eta)
547 setIntOrNull(i++, affected?.id)
548 setIntOrNull(i++, resolved?.id)
549 }
550 return i
551 }
553 override fun listIssues(project: Project, includeDone: Boolean): List<Issue> =
554 withStatement("$issueQuery where i.project = ? and (? or phase < 2)") {
555 setInt(1, project.id)
556 setBoolean(2, includeDone)
557 queryAll { it.extractIssue() }
558 }
560 override fun listIssues(project: Project, includeDone: Boolean, version: Version?, component: Component?): List<Issue> =
561 withStatement(
562 """$issueQuery where i.project = ? and
563 (? or phase < 2) and
564 (not ? or ? in (resolved, affected)) and (not ? or (resolved is null and affected is null)) and
565 (not ? or component = ?) and (not ? or component is null)
566 """.trimIndent()
567 ) {
568 fun <T : Entity> applyFilter(search: T?, fflag: Int, nflag: Int, idcol: Int) {
569 if (search == null) {
570 setBoolean(fflag, false)
571 setBoolean(nflag, false)
572 setInt(idcol, 0)
573 } else {
574 setBoolean(fflag, true)
575 setBoolean(nflag, false)
576 setInt(idcol, search.id)
577 }
578 }
579 setInt(1, project.id)
580 setBoolean(2, includeDone)
581 applyFilter(version, 3, 5, 4)
582 applyFilter(component, 6, 8, 7)
584 queryAll { it.extractIssue() }
585 }
587 override fun findIssue(id: Int): Issue? =
588 withStatement("$issueQuery where issueid = ?") {
589 setInt(1, id)
590 querySingle { it.extractIssue() }
591 }
593 override fun insertIssue(issue: Issue): Int {
594 val id = withStatement(
595 """
596 insert into lpit_issue (component, status, category, subject, description, assignee, eta, affected, resolved, project)
597 values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?, ?)
598 returning issueid
599 """.trimIndent()
600 ) {
601 val col = setIssue(1, issue)
602 setInt(col, issue.project.id)
603 querySingle { it.getInt(1) }!!
604 }
605 return id
606 }
608 override fun updateIssue(issue: Issue) {
609 withStatement(
610 """
611 update lpit_issue set updated = now(),
612 component = ?, status = ?::issue_status, category = ?::issue_category, subject = ?,
613 description = ?, assignee = ?, eta = ?, affected = ?, resolved = ?
614 where issueid = ?
615 """.trimIndent()
616 ) {
617 val col = setIssue(1, issue)
618 setInt(col, issue.id)
619 executeUpdate()
620 }
621 }
623 override fun insertHistoryEvent(issue: Issue, newId: Int) {
624 val type = if (newId > 0) IssueHistoryType.New else IssueHistoryType.Update
625 val issueid = if (newId > 0) newId else issue.id
627 val eventid =
628 withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") {
629 setInt(1, issueid)
630 setString(2, issue.subject)
631 setEnum(3, type)
632 querySingle { it.getInt(1) }!!
633 }
634 withStatement(
635 """
636 insert into lpit_issue_history_data (component, status, category, description, assignee, eta, affected, resolved, eventid)
637 values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?)
638 """.trimIndent()
639 ) {
640 setStringOrNull(1, issue.component?.name)
641 setEnum(2, issue.status)
642 setEnum(3, issue.category)
643 setStringOrNull(4, issue.description)
644 setStringOrNull(5, issue.assignee?.shortDisplayname)
645 setDateOrNull(6, issue.eta)
646 setStringOrNull(7, issue.affected?.name)
647 setStringOrNull(8, issue.resolved?.name)
648 setInt(9, eventid)
649 executeUpdate()
650 }
651 }
653 override fun listCommitRefs(issue: Issue): List<CommitRef> =
654 withStatement("select commit_hash, commit_brief from lpit_commit_ref where issueid = ?") {
655 setInt(1, issue.id)
656 queryAll {
657 CommitRef(
658 issueId = issue.id,
659 hash = it.getString("commit_hash"),
660 message = it.getString("commit_brief")
661 )
662 }
663 }
665 //</editor-fold>
667 //<editor-fold desc="Issue Relations">
668 override fun insertIssueRelation(rel: IssueRelation) {
669 withStatement(
670 """
671 insert into lpit_issue_relation (from_issue, to_issue, type)
672 values (?, ?, ?::relation_type)
673 on conflict do nothing
674 """.trimIndent()
675 ) {
676 if (rel.reverse) {
677 setInt(2, rel.from.id)
678 setInt(1, rel.to.id)
679 } else {
680 setInt(1, rel.from.id)
681 setInt(2, rel.to.id)
682 }
683 setEnum(3, rel.type)
684 executeUpdate()
685 }
686 }
688 override fun deleteIssueRelation(rel: IssueRelation) {
689 withStatement("delete from lpit_issue_relation where from_issue = ? and to_issue = ? and type=?::relation_type") {
690 if (rel.reverse) {
691 setInt(2, rel.from.id)
692 setInt(1, rel.to.id)
693 } else {
694 setInt(1, rel.from.id)
695 setInt(2, rel.to.id)
696 }
697 setEnum(3, rel.type)
698 executeUpdate()
699 }
700 }
702 override fun listIssueRelations(issue: Issue): List<IssueRelation> = buildList {
703 withStatement("select to_issue, type from lpit_issue_relation where from_issue = ?") {
704 setInt(1, issue.id)
705 queryAll { IssueRelation(issue, findIssue(it.getInt("to_issue"))!!, it.getEnum("type"), false) }
706 }.forEach(this::add)
707 withStatement("select from_issue, type from lpit_issue_relation where to_issue = ?") {
708 setInt(1, issue.id)
709 queryAll { IssueRelation(issue, findIssue(it.getInt("from_issue"))!!, it.getEnum("type"), true) }
710 }.forEach(this::add)
711 }
713 override fun getIssueRelationMap(project: Project, includeDone: Boolean): IssueRelationMap =
714 withStatement(
715 """
716 select r.from_issue, r.to_issue, r.type
717 from lpit_issue_relation r
718 join lpit_issue i on i.issueid = r.from_issue
719 join lpit_issue_phases p on i.status = p.status
720 where i.project = ? and (? or p.phase < 2)
721 """.trimIndent()
722 ) {
723 setInt(1, project.id)
724 setBoolean(2, includeDone)
725 queryAll { Pair(it.getInt("from_issue"), Pair(it.getInt("to_issue"), it.getEnum<RelationType>("type"))) }
726 }.groupBy({it.first},{it.second})
727 //</editor-fold>
729 //<editor-fold desc="IssueComment">
731 private fun ResultSet.extractIssueComment() =
732 IssueComment(getInt("commentid"), getInt("issueid")).apply {
733 created = getTimestamp("created")
734 updated = getTimestamp("updated")
735 updateCount = getInt("updatecount")
736 comment = getString("comment")
737 author = extractOptionalUser()
738 }
740 override fun listComments(issue: Issue): List<IssueComment> =
741 withStatement("select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created") {
742 setInt(1, issue.id)
743 queryAll { it.extractIssueComment() }
744 }
746 override fun findComment(id: Int): IssueComment? =
747 withStatement("select * from lpit_issue_comment left join lpit_user using (userid) where commentid = ?") {
748 setInt(1, id)
749 querySingle { it.extractIssueComment() }
750 }
752 override fun insertComment(issueComment: IssueComment): Int =
753 useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate ->
754 withStatement("insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?) returning commentid") {
755 with(issueComment) {
756 updateIssueDate.setInt(1, issueid)
757 setInt(1, issueid)
758 setStringSafe(2, comment)
759 setIntOrNull(3, author?.id)
760 }
761 val commentid = querySingle { it.getInt(1) }!!
762 updateIssueDate.executeUpdate()
763 commentid
764 }
765 }
767 override fun updateComment(issueComment: IssueComment) {
768 useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate ->
769 withStatement("update lpit_issue_comment set comment = ?, updatecount = updatecount + 1, updated = now() where commentid = ?") {
770 with(issueComment) {
771 updateIssueDate.setInt(1, issueid)
772 setStringSafe(1, comment)
773 setInt(2, id)
774 }
775 executeUpdate()
776 updateIssueDate.executeUpdate()
777 }
778 }
779 }
782 override fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int) {
783 val type = if (newId > 0) IssueHistoryType.NewComment else IssueHistoryType.UpdateComment
784 val commentid = if (newId > 0) newId else issueComment.id
786 val eventid =
787 withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") {
788 setInt(1, issueComment.issueid)
789 setString(2, issue.subject)
790 setEnum(3, type)
791 querySingle { it.getInt(1) }!!
792 }
793 withStatement("insert into lpit_issue_comment_history (commentid, eventid, comment) values (?,?,?)") {
794 setInt(1, commentid)
795 setInt(2, eventid)
796 setString(3, issueComment.comment)
797 executeUpdate()
798 }
799 }
801 //</editor-fold>
803 //<editor-fold desc="Issue History">
805 override fun listIssueHistory(projectId: Int, days: Int) =
806 withStatement(
807 """
808 select u.username as current_assignee, evt.*, evtdata.*
809 from lpit_issue_history_event evt
810 join lpit_issue issue using (issueid)
811 left join lpit_user u on u.userid = issue.assignee
812 join lpit_issue_history_data evtdata using (eventid)
813 where project = ?
814 and time > now() - (? * interval '1' day)
815 order by time desc
816 """.trimIndent()
817 ) {
818 setInt(1, projectId)
819 setInt(2, days)
820 queryAll { rs->
821 with(rs) {
822 IssueHistoryEntry(
823 subject = getString("subject"),
824 time = getTimestamp("time"),
825 type = getEnum("type"),
826 currentAssignee = getString("current_assignee"),
827 issueid = getInt("issueid"),
828 component = getString("component") ?: "",
829 status = getEnum("status"),
830 category = getEnum("category"),
831 description = getString("description") ?: "",
832 assignee = getString("assignee") ?: "",
833 eta = getDate("eta"),
834 affected = getString("affected") ?: "",
835 resolved = getString("resolved") ?: ""
836 )
837 }
838 }
839 }
841 override fun listIssueCommentHistory(projectId: Int, days: Int) =
842 withStatement(
843 """
844 select u.username as current_assignee, evt.*, evtdata.*
845 from lpit_issue_history_event evt
846 join lpit_issue issue using (issueid)
847 left join lpit_user u on u.userid = issue.assignee
848 join lpit_issue_comment_history evtdata using (eventid)
849 where project = ?
850 and time > now() - (? * interval '1' day)
851 order by time desc
852 """.trimIndent()
853 ) {
854 setInt(1, projectId)
855 setInt(2, days)
856 queryAll { rs->
857 with(rs) {
858 IssueCommentHistoryEntry(
859 subject = getString("subject"),
860 time = getTimestamp("time"),
861 type = getEnum("type"),
862 currentAssignee = getString("current_assignee"),
863 issueid = getInt("issueid"),
864 commentid = getInt("commentid"),
865 comment = getString("comment")
866 )
867 }
868 }
869 }
871 //</editor-fold>
872 }