Mon, 30 Oct 2023 14:44:36 +0100
add possibility to show issues w/o version or component - fixes #335
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(
561 project: Project,
562 includeDone: Boolean,
563 specificVersion: Boolean,
564 version: Version?,
565 specificComponent: Boolean,
566 component: Component?
567 ): List<Issue> =
568 withStatement(
569 """$issueQuery where i.project = ? and
570 (? or phase < 2) and
571 (not ? or ? in (resolved, affected)) and (not ? or (resolved is null and affected is null)) and
572 (not ? or component = ?) and (not ? or component is null)
573 """.trimIndent()
574 ) {
575 setInt(1, project.id)
576 setBoolean(2, includeDone)
578 setBoolean(3, specificVersion && version != null)
579 setInt(4, version?.id ?: 0)
580 setBoolean(5, specificVersion && version == null)
582 setBoolean(6, specificComponent && component != null)
583 setInt(7, component?.id ?: 0)
584 setBoolean(8, specificComponent && component == null)
586 queryAll { it.extractIssue() }
587 }
589 override fun findIssue(id: Int): Issue? =
590 withStatement("$issueQuery where issueid = ?") {
591 setInt(1, id)
592 querySingle { it.extractIssue() }
593 }
595 override fun insertIssue(issue: Issue): Int {
596 val id = withStatement(
597 """
598 insert into lpit_issue (component, status, category, subject, description, assignee, eta, affected, resolved, project)
599 values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?, ?)
600 returning issueid
601 """.trimIndent()
602 ) {
603 val col = setIssue(1, issue)
604 setInt(col, issue.project.id)
605 querySingle { it.getInt(1) }!!
606 }
607 return id
608 }
610 override fun updateIssue(issue: Issue) {
611 withStatement(
612 """
613 update lpit_issue set updated = now(),
614 component = ?, status = ?::issue_status, category = ?::issue_category, subject = ?,
615 description = ?, assignee = ?, eta = ?, affected = ?, resolved = ?
616 where issueid = ?
617 """.trimIndent()
618 ) {
619 val col = setIssue(1, issue)
620 setInt(col, issue.id)
621 executeUpdate()
622 }
623 }
625 override fun insertHistoryEvent(issue: Issue, newId: Int) {
626 val type = if (newId > 0) IssueHistoryType.New else IssueHistoryType.Update
627 val issueid = if (newId > 0) newId else issue.id
629 val eventid =
630 withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") {
631 setInt(1, issueid)
632 setString(2, issue.subject)
633 setEnum(3, type)
634 querySingle { it.getInt(1) }!!
635 }
636 withStatement(
637 """
638 insert into lpit_issue_history_data (component, status, category, description, assignee, eta, affected, resolved, eventid)
639 values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?)
640 """.trimIndent()
641 ) {
642 setStringOrNull(1, issue.component?.name)
643 setEnum(2, issue.status)
644 setEnum(3, issue.category)
645 setStringOrNull(4, issue.description)
646 setStringOrNull(5, issue.assignee?.shortDisplayname)
647 setDateOrNull(6, issue.eta)
648 setStringOrNull(7, issue.affected?.name)
649 setStringOrNull(8, issue.resolved?.name)
650 setInt(9, eventid)
651 executeUpdate()
652 }
653 }
655 override fun listCommitRefs(issue: Issue): List<CommitRef> =
656 withStatement("select commit_hash, commit_brief from lpit_commit_ref where issueid = ?") {
657 setInt(1, issue.id)
658 queryAll {
659 CommitRef(
660 issueId = issue.id,
661 hash = it.getString("commit_hash"),
662 message = it.getString("commit_brief")
663 )
664 }
665 }
667 //</editor-fold>
669 //<editor-fold desc="Issue Relations">
670 override fun insertIssueRelation(rel: IssueRelation) {
671 withStatement(
672 """
673 insert into lpit_issue_relation (from_issue, to_issue, type)
674 values (?, ?, ?::relation_type)
675 on conflict do nothing
676 """.trimIndent()
677 ) {
678 if (rel.reverse) {
679 setInt(2, rel.from.id)
680 setInt(1, rel.to.id)
681 } else {
682 setInt(1, rel.from.id)
683 setInt(2, rel.to.id)
684 }
685 setEnum(3, rel.type)
686 executeUpdate()
687 }
688 }
690 override fun deleteIssueRelation(rel: IssueRelation) {
691 withStatement("delete from lpit_issue_relation where from_issue = ? and to_issue = ? and type=?::relation_type") {
692 if (rel.reverse) {
693 setInt(2, rel.from.id)
694 setInt(1, rel.to.id)
695 } else {
696 setInt(1, rel.from.id)
697 setInt(2, rel.to.id)
698 }
699 setEnum(3, rel.type)
700 executeUpdate()
701 }
702 }
704 override fun listIssueRelations(issue: Issue): List<IssueRelation> = buildList {
705 withStatement("select to_issue, type from lpit_issue_relation where from_issue = ?") {
706 setInt(1, issue.id)
707 queryAll { IssueRelation(issue, findIssue(it.getInt("to_issue"))!!, it.getEnum("type"), false) }
708 }.forEach(this::add)
709 withStatement("select from_issue, type from lpit_issue_relation where to_issue = ?") {
710 setInt(1, issue.id)
711 queryAll { IssueRelation(issue, findIssue(it.getInt("from_issue"))!!, it.getEnum("type"), true) }
712 }.forEach(this::add)
713 }
715 override fun getIssueRelationMap(project: Project, includeDone: Boolean): IssueRelationMap =
716 withStatement(
717 """
718 select r.from_issue, r.to_issue, r.type
719 from lpit_issue_relation r
720 join lpit_issue i on i.issueid = r.from_issue
721 join lpit_issue_phases p on i.status = p.status
722 where i.project = ? and (? or p.phase < 2)
723 """.trimIndent()
724 ) {
725 setInt(1, project.id)
726 setBoolean(2, includeDone)
727 queryAll { Pair(it.getInt("from_issue"), Pair(it.getInt("to_issue"), it.getEnum<RelationType>("type"))) }
728 }.groupBy({it.first},{it.second})
729 //</editor-fold>
731 //<editor-fold desc="IssueComment">
733 private fun ResultSet.extractIssueComment() =
734 IssueComment(getInt("commentid"), getInt("issueid")).apply {
735 created = getTimestamp("created")
736 updated = getTimestamp("updated")
737 updateCount = getInt("updatecount")
738 comment = getString("comment")
739 author = extractOptionalUser()
740 }
742 override fun listComments(issue: Issue): List<IssueComment> =
743 withStatement("select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created") {
744 setInt(1, issue.id)
745 queryAll { it.extractIssueComment() }
746 }
748 override fun findComment(id: Int): IssueComment? =
749 withStatement("select * from lpit_issue_comment left join lpit_user using (userid) where commentid = ?") {
750 setInt(1, id)
751 querySingle { it.extractIssueComment() }
752 }
754 override fun insertComment(issueComment: IssueComment): Int =
755 useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate ->
756 withStatement("insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?) returning commentid") {
757 with(issueComment) {
758 updateIssueDate.setInt(1, issueid)
759 setInt(1, issueid)
760 setStringSafe(2, comment)
761 setIntOrNull(3, author?.id)
762 }
763 val commentid = querySingle { it.getInt(1) }!!
764 updateIssueDate.executeUpdate()
765 commentid
766 }
767 }
769 override fun updateComment(issueComment: IssueComment) {
770 useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate ->
771 withStatement("update lpit_issue_comment set comment = ?, updatecount = updatecount + 1, updated = now() where commentid = ?") {
772 with(issueComment) {
773 updateIssueDate.setInt(1, issueid)
774 setStringSafe(1, comment)
775 setInt(2, id)
776 }
777 executeUpdate()
778 updateIssueDate.executeUpdate()
779 }
780 }
781 }
784 override fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int) {
785 val type = if (newId > 0) IssueHistoryType.NewComment else IssueHistoryType.UpdateComment
786 val commentid = if (newId > 0) newId else issueComment.id
788 val eventid =
789 withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") {
790 setInt(1, issueComment.issueid)
791 setString(2, issue.subject)
792 setEnum(3, type)
793 querySingle { it.getInt(1) }!!
794 }
795 withStatement("insert into lpit_issue_comment_history (commentid, eventid, comment) values (?,?,?)") {
796 setInt(1, commentid)
797 setInt(2, eventid)
798 setString(3, issueComment.comment)
799 executeUpdate()
800 }
801 }
803 //</editor-fold>
805 //<editor-fold desc="Issue History">
807 override fun listIssueHistory(projectId: Int, days: Int) =
808 withStatement(
809 """
810 select u.username as current_assignee, evt.*, evtdata.*
811 from lpit_issue_history_event evt
812 join lpit_issue issue using (issueid)
813 left join lpit_user u on u.userid = issue.assignee
814 join lpit_issue_history_data evtdata using (eventid)
815 where project = ?
816 and time > now() - (? * interval '1' day)
817 order by time desc
818 """.trimIndent()
819 ) {
820 setInt(1, projectId)
821 setInt(2, days)
822 queryAll { rs->
823 with(rs) {
824 IssueHistoryEntry(
825 subject = getString("subject"),
826 time = getTimestamp("time"),
827 type = getEnum("type"),
828 currentAssignee = getString("current_assignee"),
829 issueid = getInt("issueid"),
830 component = getString("component") ?: "",
831 status = getEnum("status"),
832 category = getEnum("category"),
833 description = getString("description") ?: "",
834 assignee = getString("assignee") ?: "",
835 eta = getDate("eta"),
836 affected = getString("affected") ?: "",
837 resolved = getString("resolved") ?: ""
838 )
839 }
840 }
841 }
843 override fun listIssueCommentHistory(projectId: Int, days: Int) =
844 withStatement(
845 """
846 select u.username as current_assignee, evt.*, evtdata.*
847 from lpit_issue_history_event evt
848 join lpit_issue issue using (issueid)
849 left join lpit_user u on u.userid = issue.assignee
850 join lpit_issue_comment_history evtdata using (eventid)
851 where project = ?
852 and time > now() - (? * interval '1' day)
853 order by time desc
854 """.trimIndent()
855 ) {
856 setInt(1, projectId)
857 setInt(2, days)
858 queryAll { rs->
859 with(rs) {
860 IssueCommentHistoryEntry(
861 subject = getString("subject"),
862 time = getTimestamp("time"),
863 type = getEnum("type"),
864 currentAssignee = getString("current_assignee"),
865 issueid = getInt("issueid"),
866 commentid = getInt("commentid"),
867 comment = getString("comment")
868 )
869 }
870 }
871 }
873 //</editor-fold>
874 }