Skip to content

Commit f7d49f6

Browse files
authored
feat(jdbc): reintroduce improved support for Statement#getGeneratedKeys
1 parent 4e3520c commit f7d49f6

File tree

7 files changed

+144
-33
lines changed

7 files changed

+144
-33
lines changed

src/main/java/org/sqlite/core/CoreStatement.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.sql.ResultSet;
1919
import java.sql.SQLException;
20+
import java.sql.Statement;
2021
import org.sqlite.SQLiteConnection;
2122
import org.sqlite.SQLiteConnectionConfig;
2223
import org.sqlite.jdbc3.JDBC3Connection;
@@ -33,6 +34,9 @@ public abstract class CoreStatement implements Codes {
3334
protected Object[] batch = null;
3435
protected boolean resultsWaiting = false;
3536

37+
private Statement generatedKeysStat = null;
38+
private ResultSet generatedKeysRs = null;
39+
3640
protected CoreStatement(SQLiteConnection c) {
3741
conn = c;
3842
rs = new JDBC4ResultSet(this);
@@ -149,4 +153,48 @@ protected void checkIndex(int index) throws SQLException {
149153
throw new SQLException("Parameter index is invalid");
150154
}
151155
}
156+
157+
protected void clearGeneratedKeys() throws SQLException {
158+
if (generatedKeysRs != null && !generatedKeysRs.isClosed()) {
159+
generatedKeysRs.close();
160+
}
161+
generatedKeysRs = null;
162+
if (generatedKeysStat != null && !generatedKeysStat.isClosed()) {
163+
generatedKeysStat.close();
164+
}
165+
generatedKeysStat = null;
166+
}
167+
168+
/**
169+
* SQLite's last_insert_rowid() function is DB-specific. However, in this implementation we
170+
* ensure the Generated Key result set is statement-specific by executing the query immediately
171+
* after an insert operation is performed. The caller is simply responsible for calling
172+
* updateGeneratedKeys on the statement object right after execute in a synchronized(connection)
173+
* block.
174+
*/
175+
public void updateGeneratedKeys() throws SQLException {
176+
clearGeneratedKeys();
177+
if (sql != null && sql.toLowerCase().startsWith("insert")) {
178+
generatedKeysStat = conn.createStatement();
179+
generatedKeysRs = generatedKeysStat.executeQuery("SELECT last_insert_rowid();");
180+
}
181+
}
182+
183+
/**
184+
* This implementation uses SQLite's last_insert_rowid function to obtain the row ID. It cannot
185+
* provide multiple values when inserting multiple rows. Suggestion is to use a <a
186+
* href=https://www.sqlite.org/lang_returning.html>RETURNING</a> clause instead.
187+
*
188+
* @see java.sql.Statement#getGeneratedKeys()
189+
*/
190+
public ResultSet getGeneratedKeys() throws SQLException {
191+
// getGeneratedKeys is required to return an EmptyResult set if the statement
192+
// did not generate any keys. Thus, if the generateKeysResultSet is NULL, spin
193+
// up a new result set without any contents by issuing a query with a false where condition
194+
if (generatedKeysRs == null) {
195+
generatedKeysStat = conn.createStatement();
196+
generatedKeysRs = generatedKeysStat.executeQuery("SELECT 1 WHERE 1 = 2;");
197+
}
198+
return generatedKeysRs;
199+
}
152200
}

src/main/java/org/sqlite/jdbc3/JDBC3DatabaseMetaData.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ public boolean supportsFullOuterJoins() throws SQLException {
559559

560560
/** @see java.sql.DatabaseMetaData#supportsGetGeneratedKeys() */
561561
public boolean supportsGetGeneratedKeys() {
562-
return false;
562+
return true;
563563
}
564564

565565
/** @see java.sql.DatabaseMetaData#supportsGroupBy() */

src/main/java/org/sqlite/jdbc3/JDBC3PreparedStatement.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,13 @@ public boolean execute() throws SQLException {
5454
() -> {
5555
boolean success = false;
5656
try {
57-
resultsWaiting =
58-
conn.getDatabase().execute(JDBC3PreparedStatement.this, batch);
59-
success = true;
60-
updateCount = getDatabase().changes();
57+
synchronized (conn) {
58+
resultsWaiting =
59+
conn.getDatabase().execute(JDBC3PreparedStatement.this, batch);
60+
updateGeneratedKeys();
61+
success = true;
62+
updateCount = getDatabase().changes();
63+
}
6164
return 0 != columnCount;
6265
} finally {
6366
if (!success && !pointer.isClosed()) pointer.safeRunConsume(DB::reset);
@@ -119,7 +122,15 @@ public long executeLargeUpdate() throws SQLException {
119122
}
120123

121124
return this.withConnectionTimeout(
122-
() -> conn.getDatabase().executeUpdate(JDBC3PreparedStatement.this, batch));
125+
() -> {
126+
synchronized (conn) {
127+
long rc =
128+
conn.getDatabase()
129+
.executeUpdate(JDBC3PreparedStatement.this, batch);
130+
updateGeneratedKeys();
131+
return rc;
132+
}
133+
});
123134
}
124135

125136
/** @see java.sql.PreparedStatement#addBatch() */

src/main/java/org/sqlite/jdbc3/JDBC3Statement.java

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ protected JDBC3Statement(SQLiteConnection conn) {
3232

3333
/** @see java.sql.Statement#close() */
3434
public void close() throws SQLException {
35+
clearGeneratedKeys();
3536
internalClose();
3637
}
3738

@@ -49,12 +50,14 @@ public boolean execute(final String sql) throws SQLException {
4950
}
5051

5152
JDBC3Statement.this.sql = sql;
52-
53-
conn.getDatabase().prepare(JDBC3Statement.this);
54-
boolean result = exec();
55-
updateCount = getDatabase().changes();
56-
exhaustedResults = false;
57-
return result;
53+
synchronized (conn) {
54+
conn.getDatabase().prepare(JDBC3Statement.this);
55+
boolean result = exec();
56+
updateGeneratedKeys();
57+
updateCount = getDatabase().changes();
58+
exhaustedResults = false;
59+
return result;
60+
}
5861
});
5962
}
6063

@@ -126,13 +129,16 @@ public long executeLargeUpdate(String sql) throws SQLException {
126129
ext.execute(db);
127130
} else {
128131
try {
129-
changes = db.total_changes();
130-
131-
// directly invokes the exec API to support multiple SQL statements
132-
int statusCode = db._exec(sql);
133-
if (statusCode != SQLITE_OK) throw DB.newSQLException(statusCode, "");
132+
synchronized (db) {
133+
changes = db.total_changes();
134+
// directly invokes the exec API to support multiple SQL statements
135+
int statusCode = db._exec(sql);
136+
if (statusCode != SQLITE_OK)
137+
throw DB.newSQLException(statusCode, "");
138+
updateGeneratedKeys();
139+
changes = db.total_changes() - changes;
140+
}
134141

135-
changes = db.total_changes() - changes;
136142
} finally {
137143
internalClose();
138144
}
@@ -350,17 +356,6 @@ public void setFetchDirection(int direction) throws SQLException {
350356
}
351357
}
352358

353-
/**
354-
* SQLite's last_insert_rowid() function is DB-specific, not statement specific, and cannot
355-
* provide multiple values when inserting multiple rows. Suggestion is to use a <a
356-
* href=https://www.sqlite.org/lang_returning.html>RETURNING</a> clause instead.
357-
*
358-
* @see java.sql.Statement#getGeneratedKeys()
359-
*/
360-
public ResultSet getGeneratedKeys() throws SQLException {
361-
throw unsupported();
362-
}
363-
364359
/**
365360
* SQLite does not support multiple results from execute().
366361
*

src/test/java/org/sqlite/DBMetaDataTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ public void close() throws SQLException {
5151
public void getTables() throws SQLException {
5252
ResultSet rs = meta.getTables(null, null, null, null);
5353
assertThat(rs).isNotNull();
54-
5554
stat.close();
5655

5756
assertThat(rs.next()).isTrue();

src/test/java/org/sqlite/PrepStmtTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,13 @@ public void update() throws SQLException {
6767
assertThat(prep.executeUpdate()).isEqualTo(1);
6868
prep.setInt(1, 7);
6969
assertThat(prep.executeUpdate()).isEqualTo(1);
70-
prep.close();
7170

71+
ResultSet rsgk = prep.getGeneratedKeys();
72+
assertThat(rsgk.next()).isTrue();
73+
assertThat(rsgk.getInt(1)).isEqualTo(3);
74+
rsgk.close();
75+
76+
prep.close();
7277
// check results with normal statement
7378
ResultSet rs = stat.executeQuery("select sum(c1) from s1;");
7479
assertThat(rs.next()).isTrue();

src/test/java/org/sqlite/StatementTest.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,61 @@ public void getGeneratedKeys() throws SQLException {
307307
stat.executeUpdate("create table t1 (c1 integer primary key, v);");
308308
stat.executeUpdate("insert into t1 (v) values ('red');");
309309

310-
assertThatExceptionOfType(SQLFeatureNotSupportedException.class)
311-
.isThrownBy(() -> stat.getGeneratedKeys());
310+
rs = stat.getGeneratedKeys();
311+
assertThat(rs.next()).isTrue();
312+
assertThat(rs.getInt(1)).isEqualTo(1);
313+
rs.close();
314+
stat.executeUpdate("insert into t1 (v) values ('blue');");
315+
rs = stat.getGeneratedKeys();
316+
assertThat(rs.next()).isTrue();
317+
assertThat(rs.getInt(1)).isEqualTo(2);
318+
rs.close();
319+
320+
// generated keys are now attached to the statement. calling getGeneratedKeys
321+
// on a statement that has not generated any should return an empty result set
322+
stat.close();
323+
Statement stat2 = conn.createStatement();
324+
rs = stat2.getGeneratedKeys();
325+
assertThat(rs).isNotNull();
326+
assertThat(rs.next()).isFalse();
327+
stat2.close();
328+
}
329+
330+
@Test
331+
public void getGeneratedKeysIsStatementSpecific() throws SQLException {
332+
/* this test ensures that the results of getGeneratedKeys are tied to
333+
a specific statement. To verify this, we create two separate Statement
334+
objects and then execute inserts on both. We then make getGeneratedKeys()
335+
calls and verify that the two separate expected values are returned.
336+
337+
Note that the old implementation of getGeneratedKeys was called lazily, so
338+
the result of both getGeneratedKeys calls would be the same value, the row ID
339+
of the last insert on the connection. As a result it was unsafe to use
340+
with multiple statements or in a multithreaded application.
341+
*/
342+
stat.executeUpdate("create table t1 (c1 integer primary key, v);");
343+
344+
ResultSet rs1;
345+
Statement stat1 = conn.createStatement();
346+
ResultSet rs2;
347+
Statement stat2 = conn.createStatement();
348+
349+
stat1.executeUpdate("insert into t1 (v) values ('red');");
350+
stat2.executeUpdate("insert into t1 (v) values ('blue');");
351+
352+
rs2 = stat2.getGeneratedKeys();
353+
rs1 = stat1.getGeneratedKeys();
354+
355+
assertThat(rs1.next()).isTrue();
356+
assertThat(rs1.getInt(1)).isEqualTo(1);
357+
rs1.close();
358+
359+
assertThat(rs2.next()).isTrue();
360+
assertThat(rs2.getInt(1)).isEqualTo(2);
361+
rs2.close();
362+
363+
stat1.close();
364+
stat2.close();
312365
}
313366

314367
@Test

0 commit comments

Comments
 (0)