Fri, 30 Dec 2022 19:04:34 +0100
#29 add possibility to relate issues
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.IssueHistoryType
30 import de.uapcore.lightpit.types.WebColor
31 import de.uapcore.lightpit.viewmodel.ComponentSummary
32 import de.uapcore.lightpit.viewmodel.IssueSummary
33 import de.uapcore.lightpit.viewmodel.VersionSummary
34 import org.intellij.lang.annotations.Language
35 import java.sql.Connection
36 import java.sql.PreparedStatement
37 import java.sql.ResultSet
39 class PostgresDataAccessObject(private val connection: Connection) : DataAccessObject {
41 /**
42 * Prepares the given [sql] statement and executes the [block] function with the prepared statement as receiver.
43 * The statement is then closed properly.
44 */
45 private fun <R> withStatement(@Language("SQL") sql: String, block: PreparedStatement.() -> R) =
46 connection.prepareStatement(sql).use(block)
48 /**
49 * Prepares the given [sql] statement and executes the [block] function on that statement.
50 * The statement is then closed properly.
51 */
52 private fun <R> useStatement(@Language("SQL") sql: String, block: (PreparedStatement) -> R) =
53 connection.prepareStatement(sql).use(block)
55 /**
56 * Executes the statement and iterates the whole result set extracting the rows with the given [extractor] function.
57 */
58 private fun <T> PreparedStatement.queryAll(extractor: (ResultSet) -> T): List<T> = executeQuery().use {
59 sequence {
60 while (it.next()) {
61 yield(extractor(it))
62 }
63 }.toList()
64 }
66 /**
67 * Executes the statement and extracts a single row with the given [extractor] function.
68 * If the result set is empty, null is returned.
69 */
70 private fun <T> PreparedStatement.querySingle(extractor: (ResultSet) -> T): T? = executeQuery().use {
71 return if (it.next()) extractor(it) else null
72 }
74 //<editor-fold desc="User">
75 //language=SQL
76 private val userQuery = "select userid, username, lastname, givenname, mail from lpit_user"
78 private fun ResultSet.extractUser() = User(getInt("userid")).apply {
79 username = getString("username")
80 givenname = getString("givenname")
81 lastname = getString("lastname")
82 mail = getString("mail")
83 }
85 private fun ResultSet.containsUserInfo(): Boolean {
86 getInt("userid")
87 return !wasNull()
88 }
90 private fun ResultSet.extractOptionalUser() = if (containsUserInfo()) extractUser() else null
92 override fun listUsers() =
93 withStatement("$userQuery where userid > 0 order by username") {
94 queryAll { it.extractUser() }
95 }
97 override fun findUser(id: Int): User? =
98 withStatement("$userQuery where userid = ?") {
99 setInt(1, id)
100 querySingle { it.extractUser() }
101 }
103 override fun findUserByName(username: String): User? =
104 withStatement("$userQuery where lower(username) = lower(?)") {
105 setString(1, username)
106 querySingle { it.extractUser() }
107 }
109 override fun insertUser(user: User) {
110 withStatement("insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)") {
111 with(user) {
112 setStringSafe(1, username)
113 setStringOrNull(2, lastname)
114 setStringOrNull(3, givenname)
115 setStringOrNull(4, mail)
116 }
117 executeUpdate()
118 }
119 }
121 override fun updateUser(user: User) {
122 withStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?") {
123 with(user) {
124 setStringOrNull(1, lastname)
125 setStringOrNull(2, givenname)
126 setStringOrNull(3, mail)
127 setInt(4, id)
128 }
129 executeUpdate()
130 }
131 }
132 //</editor-fold>
134 //<editor-fold desc="Version">
135 //language=SQL
136 private val versionQuery = "select versionid, project, name, node, ordinal, status, release, eol from lpit_version"
138 private fun ResultSet.extractVersion() =
139 Version(getInt("versionid"), getInt("project")).apply {
140 name = getString("name")
141 node = getString("node")
142 ordinal = getInt("ordinal")
143 release = getDate("release")
144 eol = getDate("eol")
145 status = getEnum("status")
146 }
148 override fun listVersions(project: Project): List<Version> =
149 withStatement("$versionQuery where project = ? order by ordinal desc, lower(name) desc") {
150 setInt(1, project.id)
151 queryAll { it.extractVersion() }
152 }
154 override fun listVersionSummaries(project: Project): List<VersionSummary> =
155 withStatement(
156 """with
157 version_map as (
158 select issueid, status, resolved as versionid, true as isresolved from lpit_issue
159 union all
160 select issueid, status, affected as versionid, false as isresolved from lpit_issue
161 ), issues as (
162 select versionid, phase, isresolved, count(issueid) as total from version_map
163 join lpit_issue_phases using (status)
164 group by versionid, phase, isresolved
165 ),
166 summary as (
167 select versionid, phase, isresolved, total
168 from lpit_version v
169 left join issues using (versionid)
170 )
171 select v.versionid, project, name, node, ordinal, status, release, eol,
172 ro.total as resolved_open, ra.total as resolved_active, rd.total as resolved_done,
173 ao.total as affected_open, aa.total as affected_active, ad.total as affected_done
174 from lpit_version v
175 left join summary ro on ro.versionid = v.versionid and ro.phase = 0 and ro.isresolved
176 left join summary ra on ra.versionid = v.versionid and ra.phase = 1 and ra.isresolved
177 left join summary rd on rd.versionid = v.versionid and rd.phase = 2 and rd.isresolved
178 left join summary ao on ao.versionid = v.versionid and ao.phase = 0 and not ao.isresolved
179 left join summary aa on aa.versionid = v.versionid and aa.phase = 1 and not aa.isresolved
180 left join summary ad on ad.versionid = v.versionid and ad.phase = 2 and not ad.isresolved
181 where v.project = ?
182 order by ordinal desc, lower(name) desc
183 """.trimIndent()
184 ) {
185 setInt(1, project.id)
186 queryAll { rs ->
187 VersionSummary(rs.extractVersion()).apply {
188 reportedTotal.open = rs.getInt("affected_open")
189 reportedTotal.active = rs.getInt("affected_active")
190 reportedTotal.done = rs.getInt("affected_done")
191 resolvedTotal.open = rs.getInt("resolved_open")
192 resolvedTotal.active = rs.getInt("resolved_active")
193 resolvedTotal.done = rs.getInt("resolved_done")
194 }
195 }
196 }
198 override fun findVersion(id: Int): Version? =
199 withStatement("$versionQuery where versionid = ?") {
200 setInt(1, id)
201 querySingle { it.extractVersion() }
202 }
204 override fun findVersionByNode(project: Project, node: String): Version? =
205 withStatement("$versionQuery where project = ? and node = ?") {
206 setInt(1, project.id)
207 setString(2, node)
208 querySingle { it.extractVersion() }
209 }
211 override fun insertVersion(version: Version) {
212 withStatement("insert into lpit_version (name, node, ordinal, status, project, release, eol) values (?, ?, ?, ?::version_status, ?, ?, ?)") {
213 with(version) {
214 setStringSafe(1, name)
215 setStringSafe(2, node)
216 setInt(3, ordinal)
217 setEnum(4, status)
218 setInt(5, projectid)
219 setDateOrNull(6, release)
220 setDateOrNull(7, eol)
221 }
222 executeUpdate()
223 }
225 }
227 override fun updateVersion(version: Version) {
228 withStatement("update lpit_version set name = ?, node = ?, ordinal = ?, status = ?::version_status, release=?,eol=? where versionid = ?") {
229 with(version) {
230 setStringSafe(1, name)
231 setStringSafe(2, node)
232 setInt(3, ordinal)
233 setEnum(4, status)
234 setDateOrNull(5, version.release)
235 setDateOrNull(6, version.eol)
236 setInt(7, id)
237 }
238 executeUpdate()
239 }
240 }
241 //</editor-fold>
243 //<editor-fold desc="Component">
244 //language=SQL
245 private val componentQuery =
246 """
247 select id, project, name, node, color, ordinal, description, active,
248 userid, username, givenname, lastname, mail
249 from lpit_component
250 left join lpit_user on lead = userid
251 """.trimIndent()
253 private fun ResultSet.extractComponent(): Component =
254 Component(getInt("id"), getInt("project")).apply {
255 name = getString("name")
256 node = getString("node")
257 color = try {
258 WebColor(getString("color"))
259 } catch (ex: IllegalArgumentException) {
260 WebColor("000000")
261 }
262 ordinal = getInt("ordinal")
263 description = getString("description")
264 active = getBoolean("active")
265 lead = extractOptionalUser()
266 }
268 private fun PreparedStatement.setComponent(index: Int, component: Component): Int {
269 with(component) {
270 var i = index
271 setStringSafe(i++, name)
272 setStringSafe(i++, node)
273 setStringSafe(i++, color.hex)
274 setInt(i++, ordinal)
275 setStringOrNull(i++, description)
276 setBoolean(i++, active)
277 setIntOrNull(i++, lead?.id)
278 return i
279 }
280 }
282 override fun listComponents(project: Project): List<Component> =
283 withStatement("$componentQuery where project = ? order by ordinal, lower(name)") {
284 setInt(1, project.id)
285 queryAll { it.extractComponent() }
286 }
288 override fun listComponentSummaries(project: Project): List<ComponentSummary> =
289 withStatement(
290 """
291 with issues as (
292 select component, phase, count(issueid) as total
293 from lpit_issue
294 join lpit_issue_phases using (status)
295 group by component, phase
296 ),
297 summary as (
298 select c.id, phase, total
299 from lpit_component c
300 left join issues i on c.id = i.component
301 )
302 select c.id, project, name, node, color, ordinal, description, active,
303 userid, username, givenname, lastname, mail,
304 open.total as open, wip.total as wip, done.total as done
305 from lpit_component c
306 left join lpit_user on lead = userid
307 left join summary open on c.id = open.id and open.phase = 0
308 left join summary wip on c.id = wip.id and wip.phase = 1
309 left join summary done on c.id = done.id and done.phase = 2
310 where c.project = ?
311 order by ordinal, name
312 """.trimIndent()
313 ) {
314 setInt(1, project.id)
315 queryAll { rs ->
316 ComponentSummary(rs.extractComponent()).apply {
317 issueSummary.open = rs.getInt("open")
318 issueSummary.active = rs.getInt("wip")
319 issueSummary.done = rs.getInt("done")
320 }
321 }
322 }
324 override fun findComponent(id: Int): Component? =
325 withStatement("$componentQuery where id = ?") {
326 setInt(1, id)
327 querySingle { it.extractComponent() }
328 }
330 override fun findComponentByNode(project: Project, node: String): Component? =
331 withStatement("$componentQuery where project = ? and node = ?") {
332 setInt(1, project.id)
333 setString(2, node)
334 querySingle { it.extractComponent() }
335 }
337 override fun insertComponent(component: Component) {
338 withStatement("insert into lpit_component (name, node, color, ordinal, description, active, lead, project) values (?, ?, ?, ?, ?, ?, ?, ?)") {
339 val col = setComponent(1, component)
340 setInt(col, component.projectid)
341 executeUpdate()
342 }
343 }
345 override fun updateComponent(component: Component) {
346 withStatement("update lpit_component set name = ?, node = ?, color = ?, ordinal = ?, description = ?, active = ?, lead = ? where id = ?") {
347 val col = setComponent(1, component)
348 setInt(col, component.id)
349 executeUpdate()
350 }
351 }
353 //</editor-fold>
355 //<editor-fold desc="Project">
357 //language=SQL
358 private val projectQuery =
359 """
360 select projectid, name, node, ordinal, description, repourl,
361 userid, username, lastname, givenname, mail
362 from lpit_project
363 left join lpit_user owner on lpit_project.owner = owner.userid
364 """.trimIndent()
366 private fun ResultSet.extractProject() =
367 Project(getInt("projectid")).apply {
368 name = getString("name")
369 node = getString("node")
370 ordinal = getInt("ordinal")
371 description = getString("description")
372 repoUrl = getString("repourl")
373 owner = extractOptionalUser()
374 }
376 private fun PreparedStatement.setProject(index: Int, project: Project): Int {
377 var i = index
378 with(project) {
379 setStringSafe(i++, name)
380 setStringSafe(i++, node)
381 setInt(i++, ordinal)
382 setStringOrNull(i++, description)
383 setStringOrNull(i++, repoUrl)
384 setIntOrNull(i++, owner?.id)
385 }
386 return i
387 }
389 override fun listProjects(): List<Project> =
390 withStatement("$projectQuery order by ordinal, lower(name)") {
391 queryAll { it.extractProject() }
392 }
394 override fun findProject(id: Int): Project? =
395 withStatement("$projectQuery where projectid = ?") {
396 setInt(1, id)
397 querySingle { it.extractProject() }
398 }
400 override fun findProjectByNode(node: String): Project? =
401 withStatement("$projectQuery where node = ?") {
402 setString(1, node)
403 querySingle { it.extractProject() }
404 }
406 override fun insertProject(project: Project) {
407 withStatement("insert into lpit_project (name, node, ordinal, description, repourl, owner) values (?, ?, ?, ?, ?, ?)") {
408 setProject(1, project)
409 executeUpdate()
410 }
411 }
413 override fun updateProject(project: Project) {
414 withStatement("update lpit_project set name = ?, node = ?, ordinal = ?, description = ?, repourl = ?, owner = ? where projectid = ?") {
415 val col = setProject(1, project)
416 setInt(col, project.id)
417 executeUpdate()
418 }
419 }
421 override fun collectIssueSummary(project: Project): IssueSummary =
422 withStatement(
423 """
424 select phase, count(*) as total
425 from lpit_issue
426 join lpit_issue_phases using(status)
427 where project = ?
428 group by phase
429 """.trimIndent()
430 ) {
431 setInt(1, project.id)
432 executeQuery().use {
433 val summary = IssueSummary()
434 while (it.next()) {
435 val phase = it.getInt("phase")
436 val total = it.getInt("total")
437 when (phase) {
438 0 -> summary.open = total
439 1 -> summary.active = total
440 2 -> summary.done = total
441 }
442 }
443 summary
444 }
445 }
447 override fun collectIssueSummary(assignee: User): IssueSummary =
448 withStatement(
449 """
450 select phase, count(*) as total
451 from lpit_issue
452 join lpit_issue_phases using(status)
453 where assignee = ?
454 group by phase
455 """.trimIndent()
456 ) {
457 setInt(1, assignee.id)
458 executeQuery().use {
459 val summary = IssueSummary()
460 while (it.next()) {
461 val phase = it.getInt("phase")
462 val total = it.getInt("total")
463 when (phase) {
464 0 -> summary.open = total
465 1 -> summary.active = total
466 2 -> summary.done = total
467 }
468 }
469 summary
470 }
471 }
473 //</editor-fold>
475 //<editor-fold desc="Issue">
477 //language=SQL
478 private val issueQuery =
479 """
480 select issueid,
481 i.project, p.name as projectname, p.node as projectnode,
482 component, c.name as componentname, c.node as componentnode,
483 status, category, subject, i.description,
484 userid, username, givenname, lastname, mail,
485 created, updated, eta, affected, resolved
486 from lpit_issue i
487 join lpit_project p on i.project = projectid
488 left join lpit_component c on component = c.id
489 left join lpit_user on userid = assignee
490 """.trimIndent()
492 private fun ResultSet.extractIssue(): Issue {
493 val proj = Project(getInt("project")).apply {
494 name = getString("projectname")
495 node = getString("projectnode")
496 }
497 val comp = getInt("component").let {
498 if (wasNull()) null else
499 Component(it, proj.id).apply {
500 name = getString("componentname")
501 node = getString("componentnode")
502 }
503 }
504 val issue = Issue(getInt("issueid"), proj).apply {
505 component = comp
506 status = getEnum("status")
507 category = getEnum("category")
508 subject = getString("subject")
509 description = getString("description")
510 assignee = extractOptionalUser()
511 created = getTimestamp("created")
512 updated = getTimestamp("updated")
513 eta = getDate("eta")
514 affected = getInt("affected").takeIf { it > 0 }?.let { findVersion(it) }
515 resolved = getInt("resolved").takeIf { it > 0 }?.let { findVersion(it) }
516 }
518 return issue
519 }
521 private fun PreparedStatement.setIssue(index: Int, issue: Issue): Int {
522 var i = index
523 with(issue) {
524 setIntOrNull(i++, component?.id)
525 setEnum(i++, status)
526 setEnum(i++, category)
527 setStringSafe(i++, subject)
528 setStringOrNull(i++, description)
529 setIntOrNull(i++, assignee?.id)
530 setDateOrNull(i++, eta)
531 setIntOrNull(i++, affected?.id)
532 setIntOrNull(i++, resolved?.id)
533 }
534 return i
535 }
537 override fun listIssues(project: Project): List<Issue> =
538 withStatement("$issueQuery where i.project = ?") {
539 setInt(1, project.id)
540 queryAll { it.extractIssue() }
541 }
543 override fun listIssues(project: Project, version: Version?, component: Component?): List<Issue> =
544 withStatement(
545 """$issueQuery where i.project = ? and
546 (not ? or ? in (resolved, affected)) and (not ? or (resolved is null and affected is null)) and
547 (not ? or component = ?) and (not ? or component is null)
548 """.trimIndent()
549 ) {
550 fun <T : Entity> applyFilter(search: T?, fflag: Int, nflag: Int, idcol: Int) {
551 if (search == null) {
552 setBoolean(fflag, false)
553 setBoolean(nflag, false)
554 setInt(idcol, 0)
555 } else {
556 setBoolean(fflag, true)
557 setBoolean(nflag, false)
558 setInt(idcol, search.id)
559 }
560 }
561 setInt(1, project.id)
562 applyFilter(version, 2, 4, 3)
563 applyFilter(component, 5, 7, 6)
565 queryAll { it.extractIssue() }
566 }
568 override fun findIssue(id: Int): Issue? =
569 withStatement("$issueQuery where issueid = ?") {
570 setInt(1, id)
571 querySingle { it.extractIssue() }
572 }
574 override fun insertIssue(issue: Issue): Int {
575 val id = withStatement(
576 """
577 insert into lpit_issue (component, status, category, subject, description, assignee, eta, affected, resolved, project)
578 values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?, ?)
579 returning issueid
580 """.trimIndent()
581 ) {
582 val col = setIssue(1, issue)
583 setInt(col, issue.project.id)
584 querySingle { it.getInt(1) }!!
585 }
586 return id
587 }
589 override fun updateIssue(issue: Issue) {
590 withStatement(
591 """
592 update lpit_issue set updated = now(),
593 component = ?, status = ?::issue_status, category = ?::issue_category, subject = ?,
594 description = ?, assignee = ?, eta = ?, affected = ?, resolved = ?
595 where issueid = ?
596 """.trimIndent()
597 ) {
598 val col = setIssue(1, issue)
599 setInt(col, issue.id)
600 executeUpdate()
601 }
602 }
604 override fun insertHistoryEvent(issue: Issue, newId: Int) {
605 val type = if (newId > 0) IssueHistoryType.New else IssueHistoryType.Update
606 val issueid = if (newId > 0) newId else issue.id
608 val eventid =
609 withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") {
610 setInt(1, issueid)
611 setString(2, issue.subject)
612 setEnum(3, type)
613 querySingle { it.getInt(1) }!!
614 }
615 withStatement(
616 """
617 insert into lpit_issue_history_data (component, status, category, description, assignee, eta, affected, resolved, eventid)
618 values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?)
619 """.trimIndent()
620 ) {
621 setStringOrNull(1, issue.component?.name)
622 setEnum(2, issue.status)
623 setEnum(3, issue.category)
624 setStringOrNull(4, issue.description)
625 setStringOrNull(5, issue.assignee?.shortDisplayname)
626 setDateOrNull(6, issue.eta)
627 setStringOrNull(7, issue.affected?.name)
628 setStringOrNull(8, issue.resolved?.name)
629 setInt(9, eventid)
630 executeUpdate()
631 }
632 }
634 //</editor-fold>
636 //<editor-fold desc="Issue Relations">
637 override fun insertIssueRelation(rel: IssueRelation) {
638 withStatement(
639 """
640 insert into lpit_issue_relation (from_issue, to_issue, type)
641 values (?, ?, ?::relation_type)
642 on conflict do nothing
643 """.trimIndent()
644 ) {
645 if (rel.reverse) {
646 setInt(2, rel.from.id)
647 setInt(1, rel.to.id)
648 } else {
649 setInt(1, rel.from.id)
650 setInt(2, rel.to.id)
651 }
652 setEnum(3, rel.type)
653 executeUpdate()
654 }
655 }
657 override fun deleteIssueRelation(rel: IssueRelation) {
658 withStatement("delete from lpit_issue_relation where from_issue = ? and to_issue = ? and type=?::relation_type") {
659 if (rel.reverse) {
660 setInt(2, rel.from.id)
661 setInt(1, rel.to.id)
662 } else {
663 setInt(1, rel.from.id)
664 setInt(2, rel.to.id)
665 }
666 setEnum(3, rel.type)
667 executeUpdate()
668 }
669 }
671 override fun listIssueRelations(issue: Issue): List<IssueRelation> = buildList {
672 withStatement("select to_issue, type from lpit_issue_relation where from_issue = ?") {
673 setInt(1, issue.id)
674 queryAll { IssueRelation(issue, findIssue(it.getInt("to_issue"))!!, it.getEnum("type"), false) }
675 }.forEach(this::add)
676 withStatement("select from_issue, type from lpit_issue_relation where to_issue = ?") {
677 setInt(1, issue.id)
678 queryAll { IssueRelation(issue, findIssue(it.getInt("from_issue"))!!, it.getEnum("type"), true) }
679 }.forEach(this::add)
680 }
681 //</editor-fold>
683 //<editor-fold desc="IssueComment">
685 private fun ResultSet.extractIssueComment() =
686 IssueComment(getInt("commentid"), getInt("issueid")).apply {
687 created = getTimestamp("created")
688 updated = getTimestamp("updated")
689 updateCount = getInt("updatecount")
690 comment = getString("comment")
691 author = extractOptionalUser()
692 }
694 override fun listComments(issue: Issue): List<IssueComment> =
695 withStatement("select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created") {
696 setInt(1, issue.id)
697 queryAll { it.extractIssueComment() }
698 }
700 override fun findComment(id: Int): IssueComment? =
701 withStatement("select * from lpit_issue_comment left join lpit_user using (userid) where commentid = ?") {
702 setInt(1, id)
703 querySingle { it.extractIssueComment() }
704 }
706 override fun insertComment(issueComment: IssueComment): Int =
707 useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate ->
708 withStatement("insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?) returning commentid") {
709 with(issueComment) {
710 updateIssueDate.setInt(1, issueid)
711 setInt(1, issueid)
712 setStringSafe(2, comment)
713 setIntOrNull(3, author?.id)
714 }
715 val commentid = querySingle { it.getInt(1) }!!
716 updateIssueDate.executeUpdate()
717 commentid
718 }
719 }
721 override fun updateComment(issueComment: IssueComment) {
722 useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate ->
723 withStatement("update lpit_issue_comment set comment = ?, updatecount = updatecount + 1, updated = now() where commentid = ?") {
724 with(issueComment) {
725 updateIssueDate.setInt(1, issueid)
726 setStringSafe(1, comment)
727 setInt(2, id)
728 }
729 executeUpdate()
730 updateIssueDate.executeUpdate()
731 }
732 }
733 }
736 override fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int) {
737 val type = if (newId > 0) IssueHistoryType.NewComment else IssueHistoryType.UpdateComment
738 val commentid = if (newId > 0) newId else issueComment.id
740 val eventid =
741 withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") {
742 setInt(1, issueComment.issueid)
743 setString(2, issue.subject)
744 setEnum(3, type)
745 querySingle { it.getInt(1) }!!
746 }
747 withStatement("insert into lpit_issue_comment_history (commentid, eventid, comment) values (?,?,?)") {
748 setInt(1, commentid)
749 setInt(2, eventid)
750 setString(3, issueComment.comment)
751 executeUpdate()
752 }
753 }
755 //</editor-fold>
757 //<editor-fold desc="Issue History">
759 override fun listIssueHistory(projectId: Int, days: Int) =
760 withStatement(
761 """
762 select u.username as current_assignee, evt.*, evtdata.*
763 from lpit_issue_history_event evt
764 join lpit_issue issue using (issueid)
765 left join lpit_user u on u.userid = issue.assignee
766 join lpit_issue_history_data evtdata using (eventid)
767 where project = ?
768 and time > now() - (? * interval '1' day)
769 order by time desc
770 """.trimIndent()
771 ) {
772 setInt(1, projectId)
773 setInt(2, days)
774 queryAll { rs->
775 with(rs) {
776 IssueHistoryEntry(
777 subject = getString("subject"),
778 time = getTimestamp("time"),
779 type = getEnum("type"),
780 currentAssignee = getString("current_assignee"),
781 issueid = getInt("issueid"),
782 component = getString("component") ?: "",
783 status = getEnum("status"),
784 category = getEnum("category"),
785 description = getString("description") ?: "",
786 assignee = getString("assignee") ?: "",
787 eta = getDate("eta"),
788 affected = getString("affected") ?: "",
789 resolved = getString("resolved") ?: ""
790 )
791 }
792 }
793 }
795 override fun listIssueCommentHistory(projectId: Int, days: Int) =
796 withStatement(
797 """
798 select u.username as current_assignee, evt.*, evtdata.*
799 from lpit_issue_history_event evt
800 join lpit_issue issue using (issueid)
801 left join lpit_user u on u.userid = issue.assignee
802 join lpit_issue_comment_history evtdata using (eventid)
803 where project = ?
804 and time > now() - (? * interval '1' day)
805 order by time desc
806 """.trimIndent()
807 ) {
808 setInt(1, projectId)
809 setInt(2, days)
810 queryAll { rs->
811 with(rs) {
812 IssueCommentHistoryEntry(
813 subject = getString("subject"),
814 time = getTimestamp("time"),
815 type = getEnum("type"),
816 currentAssignee = getString("current_assignee"),
817 issueid = getInt("issueid"),
818 commentid = getInt("commentid"),
819 comment = getString("comment")
820 )
821 }
822 }
823 }
825 //</editor-fold>
826 }