Tue, 11 May 2021 16:00:28 +0200
fixes #137 - leaking prepared statements
src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt | file | annotate | diff | comparison | revisions |
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Tue Apr 06 09:08:54 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Tue May 11 16:00:28 2021 +0200 @@ -31,147 +31,126 @@ import de.uapcore.lightpit.viewmodel.ComponentSummary import de.uapcore.lightpit.viewmodel.IssueSummary import de.uapcore.lightpit.viewmodel.VersionSummary +import org.intellij.lang.annotations.Language import java.sql.Connection import java.sql.PreparedStatement import java.sql.ResultSet class PostgresDataAccessObject(private val connection: Connection) : DataAccessObject { - //<editor-fold desc="User"> - private fun selectUserInfo( - rs: ResultSet, - idColumn: String = "userid", - usernameColumn: String = "username", - givennameColumn: String = "givenname", - lastnameColumn: String = "lastname", - mailColumn: String = "mail" - ): User? { - val idval = rs.getInt(idColumn) - return if (rs.wasNull()) null else { - User(idval).apply { - username = rs.getString(usernameColumn) - givenname = rs.getString(givennameColumn) - lastname = rs.getString(lastnameColumn) - mail = rs.getString(mailColumn) + /** + * Prepares the given [sql] statement and executes the [block] function with the prepared statement as receiver. + * The statement is then closed properly. + */ + private fun <R> withStatement(@Language("SQL") sql: String, block: PreparedStatement.() -> R) = + connection.prepareStatement(sql).use(block) + + /** + * Prepares the given [sql] statement and executes the [block] function on that statement. + * The statement is then closed properly. + */ + private fun <R> useStatement(@Language("SQL") sql: String, block: (PreparedStatement) -> R) = + connection.prepareStatement(sql).use(block) + + /** + * Executes the statement and iterates the whole result set extracting the rows with the given [extractor] function. + */ + private fun <T> PreparedStatement.queryAll(extractor: (ResultSet) -> T): List<T> = executeQuery().use { + sequence { + while (it.next()) { + yield(extractor(it)) } - } + }.toList() } - private fun selectUsers(stmt: PreparedStatement) = sequence { - stmt.executeQuery().use { rs -> - while (rs.next()) selectUserInfo(rs)?.let { yield(it) } - } + /** + * Executes the statement and extracts a single row with the given [extractor] function. + * If the result set is empty, null is returned. + */ + private fun <T> PreparedStatement.querySingle(extractor: (ResultSet) -> T): T? = executeQuery().use { + return if (it.next()) extractor(it) else null } + //<editor-fold desc="User"> //language=SQL private val userQuery = "select userid, username, lastname, givenname, mail from lpit_user" - private val stmtUsers by lazy { - connection.prepareStatement( - """${userQuery} - where userid > 0 - order by username - """ - ) - } - private val stmtUserByID by lazy { - connection.prepareStatement( - """${userQuery} - where userid = ? - """ - ) + private fun ResultSet.extractUser() = User(getInt("userid")).apply { + username = getString("username") + givenname = getString("givenname") + lastname = getString("lastname") + mail = getString("mail") } - private val stmtUserByName by lazy { - connection.prepareStatement( - """${userQuery} - where lower(username) = lower(?) - """ - ) - } - private val stmtInsertUser by lazy { - connection.prepareStatement( - "insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)" - ) - } - private val stmtUpdateUser by lazy { - connection.prepareStatement( - "update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?" - ) + + private fun ResultSet.containsUserInfo(): Boolean { + getInt("userid") + return !wasNull() } - override fun listUsers() = selectUsers(stmtUsers).toList() - override fun findUser(id: Int): User? { - stmtUserByID.setInt(1, id) - return selectUsers(stmtUserByID).firstOrNull() - } + private fun ResultSet.extractOptionalUser() = if (containsUserInfo()) extractUser() else null + + override fun listUsers() = + withStatement("$userQuery where userid > 0 order by username") { + queryAll { it.extractUser() } + } - override fun findUserByName(username: String): User? { - stmtUserByName.setString(1, username) - return selectUsers(stmtUserByName).firstOrNull() - } + override fun findUser(id: Int): User? = + withStatement("$userQuery where userid = ?") { + setInt(1, id) + querySingle { it.extractUser() } + } + + override fun findUserByName(username: String): User? = + withStatement("$userQuery where lower(username) = lower(?)") { + setString(1, username) + querySingle { it.extractUser() } + } override fun insertUser(user: User) { - with(user) { - stmtInsertUser.setStringSafe(1, username) - stmtInsertUser.setStringOrNull(2, lastname) - stmtInsertUser.setStringOrNull(3, givenname) - stmtInsertUser.setStringOrNull(4, mail) + withStatement("insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)") { + with(user) { + setStringSafe(1, username) + setStringOrNull(2, lastname) + setStringOrNull(3, givenname) + setStringOrNull(4, mail) + } + executeUpdate() } - stmtInsertUser.execute() } override fun updateUser(user: User) { - with(user) { - stmtUpdateUser.setStringOrNull(1, lastname) - stmtUpdateUser.setStringOrNull(2, givenname) - stmtUpdateUser.setStringOrNull(3, mail) - stmtUpdateUser.setInt(4, id) + withStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?") { + with(user) { + setStringOrNull(1, lastname) + setStringOrNull(2, givenname) + setStringOrNull(3, mail) + setInt(4, id) + } + executeUpdate() } - stmtUpdateUser.execute() } //</editor-fold> //<editor-fold desc="Version"> - - private fun obtainVersion(rs: ResultSet) = - Version(rs.getInt("versionid"), rs.getInt("project")).apply { - name = rs.getString("name") - node = rs.getString("node") - ordinal = rs.getInt("ordinal") - status = rs.getEnum("status") - } - - private fun selectVersions(stmt: PreparedStatement) = sequence { - stmt.executeQuery().use { rs -> - while (rs.next()) { - yield(obtainVersion(rs)) - } - } - } - - private fun setVersionFields(stmt: PreparedStatement, obj: Version): Int { - with(obj) { - stmt.setStringSafe(1, name) - stmt.setStringSafe(2, node) - stmt.setInt(3, ordinal) - stmt.setEnum(4, status) - } - return 5 - } - //language=SQL private val versionQuery = "select versionid, project, name, node, ordinal, status from lpit_version" - private val stmtVersions by lazy { - connection.prepareStatement( - """${versionQuery} - where project = ? - order by ordinal desc, lower(name) desc - """ - ) - } - private val stmtVersionSummaries by lazy { - connection.prepareStatement( + private fun ResultSet.extractVersion() = + Version(getInt("versionid"), getInt("project")).apply { + name = getString("name") + node = getString("node") + ordinal = getInt("ordinal") + status = getEnum("status") + } + + override fun listVersions(project: Project): List<Version> = + withStatement("$versionQuery where project = ? order by ordinal desc, lower(name) desc") { + setInt(1, project.id) + queryAll { it.extractVersion() } + } + + override fun listVersionSummaries(project: Project): List<VersionSummary> = + withStatement( """ with version_map(issueid, versionid, isresolved) as ( select issueid, versionid, 1 @@ -197,51 +176,13 @@ from lpit_version join summary using (versionid) order by ordinal, name - """ - ) - } - private val stmtVersionByID by lazy { - connection.prepareStatement( - """${versionQuery} - where versionid = ? - """ - ) - } - private val stmtVersionByNode by lazy { - connection.prepareStatement( - """${versionQuery} - where project = ? and node = ? - """ - ) - } - private val stmtInsertVersion by lazy { - connection.prepareStatement( - """ - insert into lpit_version (name, node, ordinal, status, project) - values (?, ?, ?, ?::version_status, ?) - """ - ) - } - private val stmtUpdateVersion by lazy { - connection.prepareStatement( - """ - update lpit_version set name = ?, node = ?, ordinal = ?, status = ?::version_status - where versionid = ? - """ - ) - } - - override fun listVersions(project: Project): List<Version> { - stmtVersions.setInt(1, project.id) - return selectVersions(stmtVersions).toList() - } - - override fun listVersionSummaries(project: Project): List<VersionSummary> { - stmtVersionSummaries.setInt(1, project.id) - return sequence { - stmtVersionSummaries.executeQuery().use { rs -> - while (rs.next()) { - val versionSummary = VersionSummary(obtainVersion(rs)) + """.trimIndent() + ) { + setInt(1, project.id) + executeQuery().use { rs -> + sequence { + // TODO: fix bug: this extractor is not grouping the results + val versionSummary = VersionSummary(rs.extractVersion()) val phase = rs.getInt("phase") val total = rs.getInt("total") val issueSummary = @@ -252,71 +193,52 @@ 2 -> issueSummary.done = total } yield(versionSummary) - } + }.toList() } - }.toList() - } + } - override fun findVersion(id: Int): Version? { - stmtVersionByID.setInt(1, id) - return selectVersions(stmtVersionByID).firstOrNull() - } + override fun findVersion(id: Int): Version? = + withStatement("$versionQuery where versionid = ?") { + setInt(1, id) + querySingle { it.extractVersion() } + } - override fun findVersionByNode(project: Project, node: String): Version? { - stmtVersionByNode.setInt(1, project.id) - stmtVersionByNode.setString(2, node) - return selectVersions(stmtVersionByNode).firstOrNull() - } + override fun findVersionByNode(project: Project, node: String): Version? = + withStatement("$versionQuery where project = ? and node = ?") { + setInt(1, project.id) + setString(2, node) + querySingle { it.extractVersion() } + } override fun insertVersion(version: Version) { - val col = setVersionFields(stmtInsertVersion, version) - stmtInsertVersion.setInt(col, version.projectid) - stmtInsertVersion.execute() + withStatement("insert into lpit_version (name, node, ordinal, status, project) values (?, ?, ?, ?::version_status, ?)") { + with(version) { + setStringSafe(1, name) + setStringSafe(2, node) + setInt(3, ordinal) + setEnum(4, status) + setInt(5, version.projectid) + } + executeUpdate() + } + } override fun updateVersion(version: Version) { - val col = setVersionFields(stmtUpdateVersion, version) - stmtUpdateVersion.setInt(col, version.id) - stmtUpdateVersion.execute() + withStatement("update lpit_version set name = ?, node = ?, ordinal = ?, status = ?::version_status where versionid = ?") { + with(version) { + setStringSafe(1, name) + setStringSafe(2, node) + setInt(3, ordinal) + setEnum(4, status) + setInt(5, id) + } + executeUpdate() + } } //</editor-fold> //<editor-fold desc="Component"> - - private fun obtainComponent(rs: ResultSet): Component = - Component(rs.getInt("id"), rs.getInt("project")).apply { - name = rs.getString("name") - node = rs.getString("node") - color = try { - WebColor(rs.getString("color")) - } catch (ex: IllegalArgumentException) { - WebColor("000000") - } - ordinal = rs.getInt("ordinal") - description = rs.getString("description") - lead = selectUserInfo(rs) - } - - private fun selectComponents(stmt: PreparedStatement) = sequence { - stmt.executeQuery().use { rs -> - while (rs.next()) { - yield(obtainComponent(rs)) - } - } - } - - private fun setComponentFields(stmt: PreparedStatement, obj: Component): Int { - with(obj) { - stmt.setStringSafe(1, name) - stmt.setStringSafe(2, node) - stmt.setStringSafe(3, color.hex) - stmt.setInt(4, ordinal) - stmt.setStringOrNull(5, description) - stmt.setIntOrNull(6, obj.lead?.id) - } - return 7 - } - //language=SQL private val componentQuery = """ @@ -324,18 +246,43 @@ userid, username, givenname, lastname, mail from lpit_component left join lpit_user on lead = userid - """ + """.trimIndent() + + private fun ResultSet.extractComponent(): Component = + Component(getInt("id"), getInt("project")).apply { + name = getString("name") + node = getString("node") + color = try { + WebColor(getString("color")) + } catch (ex: IllegalArgumentException) { + WebColor("000000") + } + ordinal = getInt("ordinal") + description = getString("description") + lead = extractOptionalUser() + } - private val stmtComponents by lazy { - connection.prepareStatement( - """${componentQuery} - where project = ? - order by ordinal, lower(name) - """ - ) + private fun PreparedStatement.setComponent(index: Int, component: Component): Int { + with(component) { + var i = index + setStringSafe(i++, name) + setStringSafe(i++, node) + setStringSafe(i++, color.hex) + setInt(i++, ordinal) + setStringOrNull(i++, description) + setIntOrNull(i++, lead?.id) + return i + } } - private val stmtComponentSummaries by lazy { - connection.prepareStatement( + + override fun listComponents(project: Project): List<Component> = + withStatement("$componentQuery where project = ? order by ordinal, lower(name)") { + setInt(1, project.id) + queryAll { it.extractComponent() } + } + + override fun listComponentSummaries(project: Project): List<ComponentSummary> = + withStatement( """ with issues as ( select component, phase, count(issueid) as total @@ -355,114 +302,58 @@ left join lpit_user on lead = userid join summary s on c.id = s.id order by ordinal, name - """ - ) - } - private val stmtComponentById by lazy { - connection.prepareStatement( - """${componentQuery} - where id = ? - """ - ) - } - private val stmtComponentByNode by lazy { - connection.prepareStatement( - """${componentQuery} - where project = ? and node = ? - """ - ) - } - private val stmtInsertComponent by lazy { - connection.prepareStatement( - """ - insert into lpit_component (name, node, color, ordinal, description, lead, project) - values (?, ?, ?, ?, ?, ?, ?) - """ - ) - } - private val stmtUpdateComponent by lazy { - connection.prepareStatement( - "update lpit_component set name = ?, node = ?, color = ?, ordinal = ?, description = ?, lead = ? where id = ?" - ) - } - - override fun listComponents(project: Project): List<Component> { - stmtComponents.setInt(1, project.id) - return selectComponents(stmtComponents).toList() - } - - override fun listComponentSummaries(project: Project): List<ComponentSummary> { - stmtComponentSummaries.setInt(1, project.id) - return sequence { - stmtComponentSummaries.executeQuery().use { rs -> - while (rs.next()) { - val componentSummary = ComponentSummary(obtainComponent(rs)) - val phase = rs.getInt("phase") - val total = rs.getInt("total") - when (phase) { - 0 -> componentSummary.issueSummary.open = total - 1 -> componentSummary.issueSummary.active = total - 2 -> componentSummary.issueSummary.done = total + """.trimIndent() + ) { + setInt(1, project.id) + executeQuery().use { rs -> + // TODO: fix bug: this extractor is not grouping the results + sequence { + val componentSummary = ComponentSummary(rs.extractComponent()).also { + val phase = rs.getInt("phase") + val total = rs.getInt("total") + when (phase) { + 0 -> it.issueSummary.open = total + 1 -> it.issueSummary.active = total + 2 -> it.issueSummary.done = total + } } yield(componentSummary) - } + }.toList() } - }.toList() - } + } - override fun findComponent(id: Int): Component? { - stmtComponentById.setInt(1, id) - return selectComponents(stmtComponentById).firstOrNull() - } + override fun findComponent(id: Int): Component? = + withStatement("$componentQuery where id = ?") { + setInt(1, id) + querySingle { it.extractComponent() } + } - override fun findComponentByNode(project: Project, node: String): Component? { - stmtComponentByNode.setInt(1, project.id) - stmtComponentByNode.setString(2, node) - return selectComponents(stmtComponentByNode).firstOrNull() - } + override fun findComponentByNode(project: Project, node: String): Component? = + withStatement("$componentQuery where project = ? and node = ?") { + setInt(1, project.id) + setString(2, node) + querySingle { it.extractComponent() } + } override fun insertComponent(component: Component) { - val col = setComponentFields(stmtInsertComponent, component) - stmtInsertComponent.setInt(col, component.projectid) - stmtInsertComponent.execute() + withStatement("insert into lpit_component (name, node, color, ordinal, description, lead, project) values (?, ?, ?, ?, ?, ?, ?)") { + val col = setComponent(1, component) + setInt(col, component.projectid) + executeUpdate() + } } override fun updateComponent(component: Component) { - val col = setComponentFields(stmtUpdateComponent, component) - stmtUpdateComponent.setInt(col, component.id) - stmtUpdateComponent.execute() - } - - //</editor-fold> - - //<editor-fold desc="Project"> - - private fun selectProjects(stmt: PreparedStatement) = sequence { - stmt.executeQuery().use { rs -> - while (rs.next()) { - yield(Project(rs.getInt("projectid")).apply { - name = rs.getString("name") - node = rs.getString("node") - ordinal = rs.getInt("ordinal") - description = rs.getString("description") - repoUrl = rs.getString("repourl") - owner = selectUserInfo(rs) - }) - } + withStatement("update lpit_component set name = ?, node = ?, color = ?, ordinal = ?, description = ?, lead = ? where id = ?") { + val col = setComponent(1, component) + setInt(col, component.id) + executeUpdate() } } - private fun setProjectFields(stmt: PreparedStatement, obj: Project): Int { - with(obj) { - stmt.setStringSafe(1, name) - stmt.setStringSafe(2, node) - stmt.setInt(3, ordinal) - stmt.setStringOrNull(4, description) - stmt.setStringOrNull(5, repoUrl) - stmt.setIntOrNull(6, owner?.id) - } - return 7 - } +//</editor-fold> + +//<editor-fold desc="Project"> //language=SQL private val projectQuery = @@ -471,143 +362,92 @@ userid, username, lastname, givenname, mail from lpit_project left join lpit_user owner on lpit_project.owner = owner.userid - """ + """.trimIndent() + + private fun ResultSet.extractProject() = + Project(getInt("projectid")).apply { + name = getString("name") + node = getString("node") + ordinal = getInt("ordinal") + description = getString("description") + repoUrl = getString("repourl") + owner = extractOptionalUser() + } - private val stmtProjects by lazy { - connection.prepareStatement( - """${projectQuery} - order by ordinal, lower(name) - """ - ) - } - private val stmtProjectByID by lazy { - connection.prepareStatement( - """${projectQuery} - where projectid = ? - """ - ) + private fun PreparedStatement.setProject(index: Int, project: Project): Int { + var i = index + with(project) { + setStringSafe(i++, name) + setStringSafe(i++, node) + setInt(i++, ordinal) + setStringOrNull(i++, description) + setStringOrNull(i++, repoUrl) + setIntOrNull(i++, owner?.id) + } + return i } - private val stmtProjectByNode by lazy { - connection.prepareStatement( - """${projectQuery} - where node = ? - """ - ) + + override fun listProjects(): List<Project> = + withStatement("$projectQuery order by ordinal, lower(name)") { + queryAll { it.extractProject() } + } + + override fun findProject(id: Int): Project? = + withStatement("$projectQuery where projectid = ?") { + setInt(1, id) + querySingle { it.extractProject() } + } + + override fun findProjectByNode(node: String): Project? = + withStatement("$projectQuery where node = ?") { + setString(1, node) + querySingle { it.extractProject() } + } + + override fun insertProject(project: Project) { + withStatement("insert into lpit_project (name, node, ordinal, description, repourl, owner) values (?, ?, ?, ?, ?, ?)") { + setProject(1, project) + executeUpdate() + } } - private val stmtInsertProject by lazy { - connection.prepareStatement( - "insert into lpit_project (name, node, ordinal, description, repourl, owner) values (?, ?, ?, ?, ?, ?)" - ) + + override fun updateProject(project: Project) { + withStatement("update lpit_project set name = ?, node = ?, ordinal = ?, description = ?, repourl = ?, owner = ? where projectid = ?") { + val col = setProject(1, project) + setInt(col, project.id) + executeUpdate() + } } - private val stmtUpdateProject by lazy { - connection.prepareStatement( - "update lpit_project set name = ?, node = ?, ordinal = ?, description = ?, repourl = ?, owner = ? where projectid = ?" - ) - } - private val stmtIssueSummary by lazy { - connection.prepareStatement( + + override fun collectIssueSummary(project: Project): IssueSummary = + withStatement( """ select phase, count(*) as total from lpit_issue join lpit_issue_phases using(status) where project = ? group by phase - """ - ) - } - - override fun listProjects(): List<Project> { - return selectProjects(stmtProjects).toList() - } - - override fun findProject(id: Int): Project? { - stmtProjectByID.setInt(1, id) - return selectProjects(stmtProjectByID).firstOrNull() - } - - override fun findProjectByNode(node: String): Project? { - stmtProjectByNode.setString(1, node) - return selectProjects(stmtProjectByNode).firstOrNull() - } - - override fun insertProject(project: Project) { - setProjectFields(stmtInsertProject, project) - stmtInsertProject.execute() - } - - override fun updateProject(project: Project) { - val col = setProjectFields(stmtUpdateProject, project) - stmtUpdateProject.setInt(col, project.id) - stmtUpdateProject.execute() - } - - override fun collectIssueSummary(project: Project): IssueSummary { - stmtIssueSummary.setInt(1, project.id) - return stmtIssueSummary.executeQuery().use { rs -> - val summary = IssueSummary() - while (rs.next()) { - val phase = rs.getInt("phase") - val total = rs.getInt("total") - when (phase) { - 0 -> summary.open = total - 1 -> summary.active = total - 2 -> summary.done = total + """.trimIndent() + ) { + setInt(1, project.id) + executeQuery().use { + val summary = IssueSummary() + while (it.next()) { + val phase = it.getInt("phase") + val total = it.getInt("total") + when (phase) { + 0 -> summary.open = total + 1 -> summary.active = total + 2 -> summary.done = total + } } - } - summary - } - } - - //</editor-fold> - - //<editor-fold desc="Issue"> - - private fun selectIssues(stmt: PreparedStatement) = sequence { - stmt.executeQuery().use { rs -> - while (rs.next()) { - val proj = Project(rs.getInt("project")).apply { - name = rs.getString("projectname") - node = rs.getString("projectnode") - } - val comp = rs.getInt("component").let { - if (rs.wasNull()) null else - Component(it, proj.id).apply { - name = rs.getString("componentname") - node = rs.getString("componentnode") - } - } - val issue = Issue(rs.getInt("issueid"), proj).apply { - component = comp - status = rs.getEnum("status") - category = rs.getEnum("category") - subject = rs.getString("subject") - description = rs.getString("description") - assignee = selectUserInfo(rs) - created = rs.getTimestamp("created") - updated = rs.getTimestamp("updated") - eta = rs.getDate("eta") - } - queryAffectedVersions.setInt(1, issue.id) - issue.affectedVersions = selectVersions(queryAffectedVersions).toList() - queryResolvedVersions.setInt(1, issue.id) - issue.resolvedVersions = selectVersions(queryResolvedVersions).toList() - yield(issue) + summary } } - } - private fun setIssueFields(stmt: PreparedStatement, obj: Issue): Int { - with(obj) { - stmt.setIntOrNull(1, component?.id) - stmt.setEnum(2, status) - stmt.setEnum(3, category) - stmt.setStringSafe(4, subject) - stmt.setStringOrNull(5, description) - stmt.setIntOrNull(6, assignee?.id) - stmt.setDateOrNull(7, eta) - } - return 8 - } +//</editor-fold> + +//<editor-fold desc="Issue"> //language=SQL private val issueQuery = @@ -622,38 +462,73 @@ join lpit_project p on i.project = projectid left join lpit_component c on component = c.id left join lpit_user on userid = assignee - """ + """.trimIndent() - private val queryResolvedVersions by lazy { - connection.prepareStatement( + private fun ResultSet.extractIssue(): Issue { + val proj = Project(getInt("project")).apply { + name = getString("projectname") + node = getString("projectnode") + } + val comp = getInt("component").let { + if (wasNull()) null else + Component(it, proj.id).apply { + name = getString("componentname") + node = getString("componentnode") + } + } + val issue = Issue(getInt("issueid"), proj).apply { + component = comp + status = getEnum("status") + category = getEnum("category") + subject = getString("subject") + description = getString("description") + assignee = extractOptionalUser() + created = getTimestamp("created") + updated = getTimestamp("updated") + eta = getDate("eta") + } + + fun versionQuery(table: String) = """ select versionid, project, name, status, ordinal, node - from lpit_version v join lpit_issue_resolved_version using (versionid) + from lpit_version join $table using (versionid) where issueid = ? order by ordinal, name - """ - ) + """.trimIndent() + + issue.affectedVersions = withStatement(versionQuery("lpit_issue_affected_version")) { + setInt(1, issue.id) + queryAll { it.extractVersion() } + } + issue.resolvedVersions = withStatement(versionQuery("lpit_issue_resolved_version")) { + setInt(1, issue.id) + queryAll { it.extractVersion() } + } + return issue } - private val queryAffectedVersions by lazy { - connection.prepareStatement( - """ - select versionid, project, name, status, ordinal, node - from lpit_version join lpit_issue_affected_version using (versionid) - where issueid = ? - order by ordinal, name - """ - ) + private fun PreparedStatement.setIssue(index: Int, issue: Issue): Int { + var i = index + with(issue) { + setIntOrNull(i++, component?.id) + setEnum(i++, status) + setEnum(i++, category) + setStringSafe(i++, subject) + setStringOrNull(i++, description) + setIntOrNull(i++, assignee?.id) + setDateOrNull(i++, eta) + } + return i } - private val stmtIssues by lazy { - connection.prepareStatement( + override fun listIssues(filter: IssueFilter): List<Issue> = + withStatement( """ with issue_version as ( select issueid, versionid from lpit_issue_affected_version union select issueid, versionid from lpit_issue_resolved_version ), - filteterd_issues as ( + filtered_issues as ( select distinct issueid from lpit_issue left join issue_version using (issueid) where @@ -661,193 +536,145 @@ (not ? or versionid = ?) and (not ? or versionid is null) and (not ? or component = ?) and (not ? or component is null) ) - ${issueQuery} join filteterd_issues using (issueid) - """ - ) - } - - private val fproj = 1 - private val projectid = 2 - private val fversion = 3 - private val versionid = 4 - private val nversion = 5 - private val fcomp = 6 - private val component = 7 - private val ncomp = 8 + $issueQuery join filtered_issues using (issueid) + """.trimIndent() + ) { + fun <T : Entity> applyFilter(filter: Filter<T>, fflag: Int, nflag: Int, idcol: Int) { + when (filter) { + is AllFilter -> { + setBoolean(fflag, false) + setBoolean(nflag, false) + setInt(idcol, 0) + } + is NoneFilter -> { + setBoolean(fflag, false) + setBoolean(nflag, true) + setInt(idcol, 0) + } + is SpecificFilter -> { + setBoolean(fflag, true) + setBoolean(nflag, false) + setInt(idcol, filter.obj.id) + } + else -> { + TODO("Implement range filter.") + } + } + } + when (filter.project) { + is AllFilter -> { + setBoolean(1, false) + setInt(2, 0) + } + is SpecificFilter -> { + setBoolean(1, true) + setInt(2, filter.project.obj.id) + } + else -> throw IllegalArgumentException() + } + applyFilter(filter.version, 3, 5, 4) + applyFilter(filter.component, 6, 8, 7) - private fun <T : Entity> applyFilter(filter: Filter<T>, fflag: Int, nflag: Int, idcol: Int) { - when (filter) { - is AllFilter -> { - stmtIssues.setBoolean(fflag, false) - stmtIssues.setBoolean(nflag, false) - stmtIssues.setInt(idcol, 0) + queryAll { it.extractIssue() } + } + + override fun findIssue(id: Int): Issue? = + withStatement("$issueQuery where issueid = ?") { + setInt(1, id) + querySingle { it.extractIssue() } + } + + private fun insertVersionInfo(id: Int, issue: Issue) { + withStatement("insert into lpit_issue_affected_version (issueid, versionid) values (?,?)") { + setInt(1, id) + issue.affectedVersions.forEach { + setInt(2, it.id) + executeUpdate() } - is NoneFilter -> { - stmtIssues.setBoolean(fflag, false) - stmtIssues.setBoolean(nflag, true) - stmtIssues.setInt(idcol, 0) - } - is SpecificFilter -> { - stmtIssues.setBoolean(fflag, true) - stmtIssues.setBoolean(nflag, false) - stmtIssues.setInt(idcol, filter.obj.id) - } - else -> { - TODO("Implement range filter.") + } + withStatement("insert into lpit_issue_resolved_version (issueid, versionid) values (?,?)") { + setInt(1, id) + issue.resolvedVersions.forEach { + setInt(2, it.id) + executeUpdate() } } } - override fun listIssues(filter: IssueFilter): List<Issue> { - when (filter.project) { - is AllFilter -> { - stmtIssues.setBoolean(fproj, false) - stmtIssues.setInt(projectid, 0) - } - is SpecificFilter -> { - stmtIssues.setBoolean(fproj, true) - stmtIssues.setInt(projectid, filter.project.obj.id) - } - else -> throw IllegalArgumentException() - } - applyFilter(filter.version, fversion, nversion, versionid) - applyFilter(filter.component, fcomp, ncomp, component) - - return selectIssues(stmtIssues).toList() - } - - private val stmtFindIssueByID by lazy { - connection.prepareStatement( - """${issueQuery} - where issueid = ? - """ - ) - } - private val stmtInsertIssue by lazy { - connection.prepareStatement( + override fun insertIssue(issue: Issue): Int { + val id = withStatement( """ insert into lpit_issue (component, status, category, subject, description, assignee, eta, project) values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?) returning issueid - """ - ) - } - private val stmtUpdateIssue by lazy { - connection.prepareStatement( - """ - update lpit_issue set updated = now(), - component = ?, status = ?::issue_status, category = ?::issue_category, subject = ?, - description = ?, assignee = ?, eta = ? - where issueid = ? - """ - ) - } - private val stmtInsertAffectedVersion by lazy { - connection.prepareStatement( - "insert into lpit_issue_affected_version (issueid, versionid) values (?,?)" - ) - } - private val stmtInsertResolvedVersion by lazy { - connection.prepareStatement( - "insert into lpit_issue_resolved_version (issueid, versionid) values (?,?)" - ) - } - private val stmtClearAffectedVersions by lazy { - connection.prepareStatement("delete from lpit_issue_affected_version where issueid = ?") - } - private val stmtClearResolvedVersions by lazy { - connection.prepareStatement("delete from lpit_issue_resolved_version where issueid = ?") - } - - override fun findIssue(id: Int): Issue? { - stmtFindIssueByID.setInt(1, id) - return selectIssues(stmtFindIssueByID).firstOrNull() - } - - private fun insertVersionInfo(id: Int, issue: Issue) { - stmtInsertAffectedVersion.setInt(1, id) - stmtInsertResolvedVersion.setInt(1, id) - issue.affectedVersions.forEach { - stmtInsertAffectedVersion.setInt(2, it.id) - stmtInsertAffectedVersion.execute() - } - issue.resolvedVersions.forEach { - stmtInsertResolvedVersion.setInt(2, it.id) - stmtInsertResolvedVersion.execute() - } - } - - override fun insertIssue(issue: Issue): Int { - val col = setIssueFields(stmtInsertIssue, issue) - stmtInsertIssue.setInt(col, issue.project.id) - val id = stmtInsertIssue.executeQuery().use { rs -> - rs.next() - rs.getInt(1) + """.trimIndent() + ) { + val col = setIssue(1, issue) + setInt(col, issue.project.id) + querySingle { it.getInt(1) }!! } insertVersionInfo(id, issue) return id } override fun updateIssue(issue: Issue) { - val col = setIssueFields(stmtUpdateIssue, issue) - stmtUpdateIssue.setInt(col, issue.id) - stmtUpdateIssue.execute() + withStatement( + """ + update lpit_issue set updated = now(), + component = ?, status = ?::issue_status, category = ?::issue_category, subject = ?, + description = ?, assignee = ?, eta = ? + where issueid = ? + """.trimIndent() + ) { + val col = setIssue(1, issue) + setInt(col, issue.id) + executeUpdate() + } + // TODO: improve by only inserting / deleting changed version information - stmtClearAffectedVersions.setInt(1, issue.id) - stmtClearResolvedVersions.setInt(1, issue.id) - stmtClearAffectedVersions.execute() - stmtClearResolvedVersions.execute() + withStatement("delete from lpit_issue_affected_version where issueid = ?") { + setInt(1, issue.id) + executeUpdate() + } + withStatement("delete from lpit_issue_resolved_version where issueid = ?") { + setInt(1, issue.id) + executeUpdate() + } insertVersionInfo(issue.id, issue) } - //</editor-fold> +//</editor-fold> + +//<editor-fold desc="IssueComment"> - //<editor-fold desc="IssueComment"> + private fun ResultSet.extractIssueComment() = + IssueComment(getInt("commentid"), getInt("issueid")).apply { + created = getTimestamp("created") + updated = getTimestamp("updated") + updateCount = getInt("updatecount") + comment = getString("comment") + author = extractOptionalUser() + } - private fun selectComments(stmt: PreparedStatement) = sequence { - stmt.executeQuery().use { rs -> - while (rs.next()) { - yield(IssueComment(rs.getInt("commentid"), rs.getInt("issueid")).apply { - created = rs.getTimestamp("created") - updated = rs.getTimestamp("updated") - updateCount = rs.getInt("updatecount") - comment = rs.getString("comment") - author = selectUserInfo(rs) - }) + override fun listComments(issue: Issue): List<IssueComment> = + withStatement("select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created") { + setInt(1, issue.id) + queryAll { it.extractIssueComment() } + } + + override fun insertComment(issueComment: IssueComment) { + useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate -> + withStatement("insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?)") { + with(issueComment) { + updateIssueDate.setInt(1, issueid) + setInt(1, issueid) + setStringSafe(2, comment) + setIntOrNull(3, author?.id) + } + executeUpdate() + updateIssueDate.executeUpdate() } } } - - private val stmtComments by lazy { - connection.prepareStatement( - "select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created" - ) - } - private val stmtInsertComment by lazy { - connection.prepareStatement( - "insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?)" - ) - } - private val stmtUpdateIssueDate by lazy { - connection.prepareStatement( - "update lpit_issue set updated = now() where issueid = ?" - ) - } - - override fun listComments(issue: Issue): List<IssueComment> { - stmtComments.setInt(1, issue.id) - return selectComments(stmtComments).toList() - } - - override fun insertComment(issueComment: IssueComment) { - with(issueComment) { - stmtUpdateIssueDate.setInt(1, issueid) - stmtInsertComment.setInt(1, issueid) - stmtInsertComment.setStringSafe(2, comment) - stmtInsertComment.setIntOrNull(3, author?.id) - } - stmtInsertComment.execute() - stmtUpdateIssueDate.execute() - } - //</editor-fold> +//</editor-fold> } \ No newline at end of file