Repository: xerial/sqlite-jdbc
Pull request #757 (2022-08-03 18:53:20)

Fix memory leak #423


Patch:
diff --git a/jreleaser.yml b/jreleaser.yml
new file mode 100644
index 00000000..3b1b374a
--- /dev/null
+++ b/jreleaser.yml
@@ -0,0 +1,24 @@
+project:
+  versionPattern: CUSTOM
+release:
+  github:
+    discussionCategoryName: Announcements
+    tagName: '{{projectVersion}}'
+    changelog:
+      links: true
+      formatted: ALWAYS
+      format: '- {{commitShortHash}} {{commitTitle}}'
+      preset: 'conventional-commits'
+      excludeLabels:
+        - 'chore'
+        - 'merge'
+      labelers:
+        - label: 'chore'
+          title: '[maven-release-plugin] prepare'
+          order: 1
+      categories:
+        - title: '🏎 Perf'
+          key: 'perf'
+          labels:
+            - 'perf'
+          order: 25
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index f91cdff7..daf8de00 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.xerial</groupId>
     <artifactId>sqlite-jdbc</artifactId>
-    <version>3.39.2-SNAPSHOT</version>
+    <version>3.39.2.0-SNAPSHOT</version>
     <name>SQLite JDBC</name>
     <description>SQLite JDBC library</description>
 
@@ -116,18 +116,6 @@
                 <version>${surefire.version}</version>
             </plugin>
 
-            <plugin>
-                <artifactId>maven-release-plugin</artifactId>
-                <version>2.1</version>
-                <configuration>
-                    <!--  do not run site-deploy goal, included in the default settings  -->
-                    <goals>deploy</goals>
-                    <pushChanges>false</pushChanges>
-                    <localCheckout>true</localCheckout>
-                    <connectionUrl>scm:hg:ssh://hg@bitbucket.org/xerial/sqlite-jdbc</connectionUrl>
-                </configuration>
-            </plugin>
-
             <plugin>
                 <artifactId>maven-jar-plugin</artifactId>
                 <version>3.2.0</version>
@@ -181,6 +169,15 @@
                     <autoReleaseAfterClose>true</autoReleaseAfterClose>
                 </configuration>
             </plugin>
+
+            <plugin>
+                <groupId>org.jreleaser</groupId>
+                <artifactId>jreleaser-maven-plugin</artifactId>
+                <version>1.1.0</version>
+                <configuration>
+                    <configFile>jreleaser.yml</configFile>
+                </configuration>
+            </plugin>
         </plugins>
 
         <pluginManagement>
diff --git a/src/main/java/org/sqlite/core/CoreResultSet.java b/src/main/java/org/sqlite/core/CoreResultSet.java
index d7c226d3..f6c8ebfd 100644
--- a/src/main/java/org/sqlite/core/CoreResultSet.java
+++ b/src/main/java/org/sqlite/core/CoreResultSet.java
@@ -25,6 +25,7 @@ import org.sqlite.SQLiteConnectionConfig;
 public abstract class CoreResultSet implements Codes {
     protected final CoreStatement stmt;
 
+    public boolean emptyResultSet = false; // false means we don't have results
     public boolean open = false; // true means have results and can iterate them
     public int maxRows; // max. number of rows as set by a Statement
     public String[] cols = null; // if null, the RS is closed()
@@ -120,8 +121,9 @@ public abstract class CoreResultSet implements Codes {
         row = 0;
         lastCol = -1;
         columnNameToIndex = null;
+        emptyResultSet = false;
 
-        if (!open) {
+        if (stmt.pointer.isClosed() || (!open && !closeStmt)) {
             return;
         }
 
diff --git a/src/main/java/org/sqlite/jdbc3/JDBC3ResultSet.java b/src/main/java/org/sqlite/jdbc3/JDBC3ResultSet.java
index 1a76f62b..a8017e46 100644
--- a/src/main/java/org/sqlite/jdbc3/JDBC3ResultSet.java
+++ b/src/main/java/org/sqlite/jdbc3/JDBC3ResultSet.java
@@ -121,7 +121,7 @@ public abstract class JDBC3ResultSet extends CoreResultSet {
 
     /** @see java.sql.ResultSet#isAfterLast() */
     public boolean isAfterLast() throws SQLException {
-        return !open;
+        return !open && !emptyResultSet;
     }
 
     /** @see java.sql.ResultSet#isBeforeFirst() */
diff --git a/src/main/java/org/sqlite/jdbc3/JDBC3Statement.java b/src/main/java/org/sqlite/jdbc3/JDBC3Statement.java
index c137af84..a79ad243 100644
--- a/src/main/java/org/sqlite/jdbc3/JDBC3Statement.java
+++ b/src/main/java/org/sqlite/jdbc3/JDBC3Statement.java
@@ -116,6 +116,7 @@ public abstract class JDBC3Statement extends CoreStatement {
         }
 
         rs.cols = rs.colsMeta;
+        rs.emptyResultSet = !resultsWaiting;
         rs.open = resultsWaiting;
         resultsWaiting = false;
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml
similarity index 72%
rename from .github/workflows/test.yml
rename to .github/workflows/ci.yml
index 53fc8363..be8e020e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/ci.yml
@@ -1,6 +1,7 @@
 name: CI
 
 on:
+  workflow_dispatch:
   pull_request:
     paths:
       - '**.java'
@@ -148,6 +149,12 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
+        if: ${{ github.event_name == 'push' }}
+      # a workflow dispatch indicates a release, we need the full git history to generate the changelog
+      - uses: actions/checkout@v3
+        if: ${{ github.event_name == 'workflow_dispatch' }}
+        with:
+          fetch-depth: 0
       - name: Set up Apache Maven Central
         uses: actions/setup-java@v2
         with: # running setup-java again overwrites the settings.xml
@@ -161,11 +168,43 @@ jobs:
           gpg-passphrase: MAVEN_CENTRAL_GPG_PASSPHRASE # env variable for GPG private key passphrase (3)
       - name: Get Project Version
         run: |
-          echo "PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV
+          echo "PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" | tee -a $GITHUB_ENV
+      - name: Cancel action for non SNAPSHOT push
+        if: ${{ github.event_name == 'push' && !endsWith(env.PROJECT_VERSION, '-SNAPSHOT')}}
+        uses: andymckay/cancel-action@0.2
+      # from that point, push means we deploy snapshot, workflow_dispatch means we perform a release
+      - name: Prepare release
+        if: ${{ github.event_name == 'workflow_dispatch' }}
+        run: |
+          mvn --no-transfer-progress versions:set -DremoveSnapshot -DgenerateBackupPoms=false
+          echo "RELEASE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" | tee -a $GITHUB_ENV
+      - name: Release commit and push
+        if: ${{ github.event_name == 'workflow_dispatch' }}
+        uses: EndBug/add-and-commit@v9
+        with:
+          message: 'chore(release): ${{ env.RELEASE_VERSION }} [skip ci]'
+          tag: '${{ env.RELEASE_VERSION }}'
+          default_author: github_actions
+      # The sonatype maven plugin will decide the right destination depending on whether the project is SNAPSHOT or not
       - name: Publish to Apache Maven Central
-        if: ${{ endsWith(env.PROJECT_VERSION, '-SNAPSHOT') }}
         run: mvn --batch-mode --no-transfer-progress deploy -P release -DskipTests
         env:
           MAVEN_CENTRAL_USERNAME: ${{ secrets.SONATYPE_USERNAME }} # must be the same env variable name as (1)
           MAVEN_CENTRAL_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} # must be the same env variable name as (2)
-          MAVEN_CENTRAL_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} # must be the same env variable name as (3)
\ No newline at end of file
+          MAVEN_CENTRAL_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} # must be the same env variable name as (3)
+      - name: Create Github release with JReleaser
+        if: ${{ github.event_name == 'workflow_dispatch' }}
+        env:
+          JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          mvn --batch-mode --no-transfer-progress -DskipTests jreleaser:full-release
+      - name: Prepare next snapshot
+        if: ${{ github.event_name == 'workflow_dispatch' }}
+        run: |
+          mvn versions:set -DnextSnapshot -DgenerateBackupPoms=false
+      - name: Next snapshot commit and push
+        if: ${{ github.event_name == 'workflow_dispatch' }}
+        uses: EndBug/add-and-commit@v9
+        with:
+          message: 'chore(release): prepare next snapshot [skip ci]'
+          default_author: github_actions
diff --git a/src/test/java/org/sqlite/MetadataLeakTest.java b/src/test/java/org/sqlite/MetadataLeakTest.java
new file mode 100644
index 00000000..54cd59ac
--- /dev/null
+++ b/src/test/java/org/sqlite/MetadataLeakTest.java
@@ -0,0 +1,34 @@
+package org.sqlite;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import org.junit.jupiter.api.Test;
+
+/** A test to make sure that getMetadata is properly freed */
+public class MetadataLeakTest {
+    /**
+     * Check that statement is properly closed when result set is closed. See
+     * https://github.com/xerial/sqlite-jdbc/issues/423
+     *
+     * @throws Exception on failure
+     */
+    @Test
+    void testMemoryIsFreed() throws Exception {
+        try (Connection con = DriverManager.getConnection("jdbc:sqlite::memory:")) {
+            DatabaseMetaData meta = con.getMetaData();
+
+            Statement statement;
+            try (ResultSet tables = meta.getTables(null, null, null, null); ) {
+                statement = tables.getStatement();
+                assertFalse(statement.isClosed());
+            }
+            assertTrue(statement.isClosed());
+        }
+    }
+}
diff --git a/src/test/java/org/sqlite/ResultSetWithoutResultsTest.java b/src/test/java/org/sqlite/ResultSetWithoutResultsTest.java
new file mode 100644
index 00000000..751d805c
--- /dev/null
+++ b/src/test/java/org/sqlite/ResultSetWithoutResultsTest.java
@@ -0,0 +1,55 @@
+package org.sqlite;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.junit.jupiter.api.Test;
+
+/** Tests to make sure we properly reset a query when there are no results */
+public class ResultSetWithoutResultsTest {
+
+    /**
+     * Validates that Statements can be reused as expected even when the result set has no results
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testQueryIsReset() throws Exception {
+        try (Connection con = DriverManager.getConnection("jdbc:sqlite::memory:")) {
+            try (Statement statement = con.createStatement()) {
+                // run a few queries with no results
+                for (int i = 0; i < 3; i++) {
+                    runEmptyStatement(statement);
+                }
+                // test a query that has a result.
+                testStmtWithResult(statement);
+            }
+        }
+    }
+
+    private void testStmtWithResult(Statement statement) throws SQLException {
+        try (ResultSet rs = statement.executeQuery("select 123")) {
+            assertTrue(rs.isBeforeFirst());
+            assertFalse(rs.isAfterLast());
+            rs.next();
+            assertFalse(rs.isBeforeFirst());
+            assertFalse(rs.isAfterLast());
+            assertEquals(123, rs.getInt(1));
+            rs.next();
+            assertTrue(rs.isAfterLast());
+        }
+    }
+
+    private void runEmptyStatement(Statement statement) throws SQLException {
+        try (ResultSet rs = statement.executeQuery("select 1 where 1=0")) {
+            assertFalse(rs.isBeforeFirst());
+            assertFalse(rs.isAfterLast());
+        }
+    }
+}