From 840edc11c73f60e2993f04f0a2883ca19c8cfb4d Mon Sep 17 00:00:00 2001 From: Mike Jarmy Date: Thu, 30 Jul 2026 12:21:26 -0400 Subject: [PATCH 1/6] sql: add Statement.generatedKeys --- src/sql/fan/Statement.fan | 12 ++++ src/sql/java/StatementPeer.java | 69 +++++++++++++++++-- src/sql/test/SqlTest.fan | 114 ++++++++++++++++++++++++++++++-- 3 files changed, 187 insertions(+), 8 deletions(-) diff --git a/src/sql/fan/Statement.fan b/src/sql/fan/Statement.fan index 6d9f5269a..6b54fd5fa 100644 --- a/src/sql/fan/Statement.fan +++ b/src/sql/fan/Statement.fan @@ -81,8 +81,20 @@ class Statement ** the batch, consistent with the underlying DBMS -- either always continuing ** to process commands or never continuing to process commands. ** + ** If the driver returns auto-generated keys for some but not all of the + ** commands, throw SqlErr since the keys cannot be correlated to their + ** commands. See `generatedKeys`. + ** native Int?[] executeBatch([Str:Obj]?[] paramsList) + ** + ** Keys, if any, from the most recent `executeBatch`, with one entry + ** per batch command. An entry is null if the driver did not return a key + ** for that command. Return an empty list if executeBatch has not been + ** called on this statement. + ** + native Obj?[] generatedKeys() + ** ** If the last execute has more results from a multi-result stored ** procedure, then return the next batch of results as Row[]. Otherwise diff --git a/src/sql/java/StatementPeer.java b/src/sql/java/StatementPeer.java index c394a94ca..1dba124e8 100644 --- a/src/sql/java/StatementPeer.java +++ b/src/sql/java/StatementPeer.java @@ -305,10 +305,7 @@ private Object executeResult(Statement self, boolean isResultSet) List keys = null; while (rs.next()) { - // get key as Long or String - Object key; - try { key = rs.getLong(1); } - catch (Exception e) { key = rs.getString(1); } + Object key = readGenKey(rs); // lazily create keys list with proper type if (keys == null) @@ -330,12 +327,67 @@ private Object executeResult(Statement self, boolean isResultSet) return Long.valueOf(-1); } + /** + * Read the auto-generated key for the current row as a Long, or as a + * String for databases like Oracle which do not allow access as an Int. + */ + private static Object readGenKey(ResultSet rs) + throws SQLException + { + try { return rs.getLong(1); } + catch (Exception e) { return rs.getString(1); } + } + + /** Empty auto-generated key list. */ + private static List emptyGenKeys() + { + return List.make(Sys.ObjType.toNullable(), 0); + } + + /** + * Auto-generated keys for the batch just executed, with one entry per + * command. Entries are null if the driver returned no keys at all. If + * the driver returns keys for some but not all of the commands they cannot + * be correlated to their commands, so that case is an error. + */ + private List readBatchGenKeys(PreparedStatement pstmt, int numCommands) + throws SQLException + { + List keys = List.make(Sys.ObjType.toNullable(), numCommands); + + if (isAutoKeys) + { + ResultSet rs = pstmt.getGeneratedKeys(); + try + { + while (rs.next()) keys.add(readGenKey(rs)); + } + finally + { + rs.close(); + } + + if (keys.size() == numCommands) return keys; + + if (keys.size() != 0) + throw SqlErr.make("Driver returned " + keys.size() + + " auto-generated keys for " + numCommands + " batch commands"); + } + + // no keys returned: one null per command + for (int i = 0; i < numCommands; i++) keys.add(null); + return keys; + } + public List executeBatch(Statement self, List paramsList) { if (!prepared) throw SqlErr.make("Statement has not been prepared."); PreparedStatement pstmt = (PreparedStatement)stmt; + // discard keys from any previous batch + this.genKeys = emptyGenKeys(); + try { // add batch @@ -348,6 +400,9 @@ public List executeBatch(Statement self, List paramsList) // execute batch int[] exec = pstmt.executeBatch(); + // gather auto-generated keys before the statement is reused + this.genKeys = readBatchGenKeys(pstmt, (int)paramsList.size()); + // process result List result = List.make(Sys.IntType, exec.length); for (int i = 0; i < exec.length; i++) @@ -368,6 +423,11 @@ public List executeBatch(Statement self, List paramsList) } } + public List generatedKeys(Statement self) + { + return genKeys; + } + public List more(Statement self) { try @@ -468,6 +528,7 @@ private void createStatement(Statement self) private java.sql.Statement stmt; private Map paramMap; private int limit = 0; // limit field value + private List genKeys = emptyGenKeys(); // keys from last executeBatch // These are set during init(): private boolean isInsert; // does sql contain insert keyword diff --git a/src/sql/test/SqlTest.fan b/src/sql/test/SqlTest.fan index b36342320..8aad4e17c 100644 --- a/src/sql/test/SqlTest.fan +++ b/src/sql/test/SqlTest.fan @@ -48,8 +48,10 @@ class SqlTest : Test transactions preparedStmts executeStmts - batchExecute - batchExecutor + batchUpdate + batchInsert + batchInsertAuto + batchExecutorUpdate withPrepare mysqlVariable postgresBuf @@ -530,7 +532,7 @@ class SqlTest : Test // Batch execution ////////////////////////////////////////////////////////////////////////// - Void batchExecute() + Void batchUpdate() { params := [Str:Obj][,] stmt := db.sql("select farmer_id from farmers") @@ -548,10 +550,15 @@ class SqlTest : Test // executeBatch stmt.prepare res := stmt.executeBatch(params) + // The result will be an array filled with '1', // indicating that each row was updated. verifyEq(res, Int[,].fill(1, params.size)) + // The keys will all be null, because nothing was autogenerated + keys := stmt.generatedKeys + verifyEq(keys, Obj?[,].fill(null, params.size)) + // double check db.commit stmt = db.sql("select farmer_id, age from farmers") @@ -562,7 +569,106 @@ class SqlTest : Test } } - Void batchExecutor() + Void batchInsert() + { + // drop and re-create + if (db.meta.tableExists("batch_insert")) + db.sql("drop table batch_insert").execute + + if (dbType == DbType.postgres) + { + db.sql( + "create table batch_insert ( + id int, + name text not null)").execute + } + else + { + db.sql( + "create table batch_insert ( + id int not null, + name varchar(255) not null, + primary key (id))").execute + } + + // batch insert + stmt := db.sql( + "insert into batch_insert (id, name) values (@id, @name)").prepare + stmt.executeBatch([ + ["id": 1, "name":"aaa"], + ["id": 2, "name":"bbb"], + ["id": 3, "name":"ccc"], + ["id": 4, "name":"ddd"], + ]) + + // check keys. Note that postgres populates the keys even + // though they weren't auto-generated + keys := stmt.generatedKeys + if (dbType == DbType.postgres) + verifyEq(keys, Obj?[1,2,3,4]) + else + verifyEq(keys, Obj?[null,null,null,null]) + + // make sure records were created + stmt = db.sql("select * from batch_insert order by id") + n := 0 + names := ["aaa", "bbb", "ccc", "ddd"] + stmt.queryEach(null) |r| { + verifyEq(r->id, n+1) + verifyEq(r->name, names[n]) + n++ + } + } + + Void batchInsertAuto() + { + + // drop and re-create + if (db.meta.tableExists("batch_insert_auto")) + db.sql("drop table batch_insert_auto").execute + + if (dbType == DbType.postgres) + { + db.sql( + "create table batch_insert_auto ( + id serial, + name text not null)").execute + } + else + { + db.sql( + "create table batch_insert_auto ( + id int auto_increment not null, + name varchar(255) not null, + primary key (id))").execute + } + + // batch insert + stmt := db.sql( + "insert into batch_insert_auto (name) values (@name)").prepare + stmt.executeBatch([ + ["name":"aaa"], + ["name":"bbb"], + ["name":"ccc"], + ["name":"ddd"], + ]) + + // check auto-generated keys + keys := stmt.generatedKeys + verifyEq(keys, Obj?[1,2,3,4]) + + // make sure records were created + stmt = db.sql("select * from batch_insert_auto order by id") + n := 0 + names := ["aaa", "bbb", "ccc", "ddd"] + stmt.queryEach(null) |r| { + verifyEq(r->id, n+1) + verifyEq(r->name, names[n]) + n++ + } + } + + Void batchExecutorUpdate() { ids := Int[,] stmt := db.sql("select farmer_id from farmers") From 5ef06c7a76c236df22c47017ec1d30533ddd40af Mon Sep 17 00:00:00 2001 From: Mike Jarmy Date: Thu, 30 Jul 2026 12:23:13 -0400 Subject: [PATCH 2/6] sql: rename generatedKeys to batchKeys --- src/sql/fan/Statement.fan | 4 ++-- src/sql/java/StatementPeer.java | 2 +- src/sql/test/SqlTest.fan | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sql/fan/Statement.fan b/src/sql/fan/Statement.fan index 6b54fd5fa..401ca2e0c 100644 --- a/src/sql/fan/Statement.fan +++ b/src/sql/fan/Statement.fan @@ -83,7 +83,7 @@ class Statement ** ** If the driver returns auto-generated keys for some but not all of the ** commands, throw SqlErr since the keys cannot be correlated to their - ** commands. See `generatedKeys`. + ** commands. See `batchKeys`. ** native Int?[] executeBatch([Str:Obj]?[] paramsList) @@ -93,7 +93,7 @@ class Statement ** for that command. Return an empty list if executeBatch has not been ** called on this statement. ** - native Obj?[] generatedKeys() + native Obj?[] batchKeys() ** ** If the last execute has more results from a multi-result stored diff --git a/src/sql/java/StatementPeer.java b/src/sql/java/StatementPeer.java index 1dba124e8..e33e9f38c 100644 --- a/src/sql/java/StatementPeer.java +++ b/src/sql/java/StatementPeer.java @@ -423,7 +423,7 @@ public List executeBatch(Statement self, List paramsList) } } - public List generatedKeys(Statement self) + public List batchKeys(Statement self) { return genKeys; } diff --git a/src/sql/test/SqlTest.fan b/src/sql/test/SqlTest.fan index 8aad4e17c..12546a180 100644 --- a/src/sql/test/SqlTest.fan +++ b/src/sql/test/SqlTest.fan @@ -556,7 +556,7 @@ class SqlTest : Test verifyEq(res, Int[,].fill(1, params.size)) // The keys will all be null, because nothing was autogenerated - keys := stmt.generatedKeys + keys := stmt.batchKeys verifyEq(keys, Obj?[,].fill(null, params.size)) // double check @@ -603,7 +603,7 @@ class SqlTest : Test // check keys. Note that postgres populates the keys even // though they weren't auto-generated - keys := stmt.generatedKeys + keys := stmt.batchKeys if (dbType == DbType.postgres) verifyEq(keys, Obj?[1,2,3,4]) else @@ -654,7 +654,7 @@ class SqlTest : Test ]) // check auto-generated keys - keys := stmt.generatedKeys + keys := stmt.batchKeys verifyEq(keys, Obj?[1,2,3,4]) // make sure records were created From 502b94ebc3bf0b19945fd5ac1e257b3a0d49aadc Mon Sep 17 00:00:00 2001 From: Mike Jarmy Date: Thu, 30 Jul 2026 12:46:47 -0400 Subject: [PATCH 3/6] sql: add BatchExecutor.keys() --- src/sql/fan/BatchExecutor.fan | 12 ++++ src/sql/fan/Statement.fan | 6 +- src/sql/test/SqlTest.fan | 108 +++++++++++++++++++++++++++++++++- 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/sql/fan/BatchExecutor.fan b/src/sql/fan/BatchExecutor.fan index 0b85e1e93..261413018 100644 --- a/src/sql/fan/BatchExecutor.fan +++ b/src/sql/fan/BatchExecutor.fan @@ -29,6 +29,7 @@ class BatchExecutor if (queued.size > maxChunkSize) { results.addAll(stmt.executeBatch(queued)) + keyList.addAll(stmt.batchKeys) queued.clear } } @@ -40,11 +41,21 @@ class BatchExecutor if (queued.size > 0) { results.addAll(stmt.executeBatch(queued)) + keyList.addAll(stmt.batchKeys) queued.clear } return results } + ** The keys, if any, from the the finished BatchExecutor, with one + ** entry per call to add(). This method should not be called until + ** finish() has been called -- if you call it before then, it will + ** return an incomplete list of keys. + Obj?[] keys() + { + return keyList + } + ////////////////////////////////////////////////////////////////////////// // Fields ////////////////////////////////////////////////////////////////////////// @@ -54,5 +65,6 @@ class BatchExecutor private [Str:Obj][] queued := [Str:Obj][,] private Int?[] results := [,] + private Obj?[] keyList := [,] } diff --git a/src/sql/fan/Statement.fan b/src/sql/fan/Statement.fan index 401ca2e0c..7f3ccb002 100644 --- a/src/sql/fan/Statement.fan +++ b/src/sql/fan/Statement.fan @@ -88,9 +88,9 @@ class Statement native Int?[] executeBatch([Str:Obj]?[] paramsList) ** - ** Keys, if any, from the most recent `executeBatch`, with one entry - ** per batch command. An entry is null if the driver did not return a key - ** for that command. Return an empty list if executeBatch has not been + ** The keys, if any, from the most recent call to `executeBatch()`, with one + ** entry per batch command. An entry is null if the driver did not return a + ** key for that command. Return an empty list if executeBatch has not been ** called on this statement. ** native Obj?[] batchKeys() diff --git a/src/sql/test/SqlTest.fan b/src/sql/test/SqlTest.fan index 12546a180..c1d140a6d 100644 --- a/src/sql/test/SqlTest.fan +++ b/src/sql/test/SqlTest.fan @@ -48,10 +48,15 @@ class SqlTest : Test transactions preparedStmts executeStmts + batchUpdate batchInsert batchInsertAuto + batchExecutorUpdate + batchExecutorInsert + batchExecutorInsertAuto + withPrepare mysqlVariable postgresBuf @@ -622,7 +627,6 @@ class SqlTest : Test Void batchInsertAuto() { - // drop and re-create if (db.meta.tableExists("batch_insert_auto")) db.sql("drop table batch_insert_auto").execute @@ -695,6 +699,108 @@ class SqlTest : Test } } + Void batchExecutorInsert() + { + // drop and re-create + if (db.meta.tableExists("batch_insert")) + db.sql("drop table batch_insert").execute + + if (dbType == DbType.postgres) + { + db.sql( + "create table batch_insert ( + id int, + name text not null)").execute + } + else + { + db.sql( + "create table batch_insert ( + id int not null, + name varchar(255) not null, + primary key (id))").execute + } + + // batch insert + stmt := db.sql( + "insert into batch_insert (id, name) values (@id, @name)").prepare + batch := BatchExecutor(stmt, 3) + [ + ["id": 1, "name":"aaa"], + ["id": 2, "name":"bbb"], + ["id": 3, "name":"ccc"], + ["id": 4, "name":"ddd"], + ].each |x| { batch.add(x) } + batch.finish + + // check keys. Note that postgres populates the keys even + // though they weren't auto-generated + keys := batch.keys + if (dbType == DbType.postgres) + verifyEq(keys, Obj?[1,2,3,4]) + else + verifyEq(keys, Obj?[null,null,null,null]) + + // make sure records were created + stmt = db.sql("select * from batch_insert order by id") + n := 0 + names := ["aaa", "bbb", "ccc", "ddd"] + stmt.queryEach(null) |r| { + verifyEq(r->id, n+1) + verifyEq(r->name, names[n]) + n++ + } + } + + Void batchExecutorInsertAuto() + { + // drop and re-create + if (db.meta.tableExists("batch_insert_auto")) + db.sql("drop table batch_insert_auto").execute + + if (dbType == DbType.postgres) + { + db.sql( + "create table batch_insert_auto ( + id serial, + name text not null)").execute + } + else + { + db.sql( + "create table batch_insert_auto ( + id int auto_increment not null, + name varchar(255) not null, + primary key (id))").execute + } + + // batch insert + stmt := db.sql( + "insert into batch_insert_auto (name) values (@name)").prepare + batch := BatchExecutor(stmt, 3) + [ + ["name":"aaa"], + ["name":"bbb"], + ["name":"ccc"], + ["name":"ddd"], + ].each |x| { batch.add(x) } + batch.finish + + // check auto-generated keys + keys := batch.keys + verifyEq(keys, Obj?[1,2,3,4]) + + // make sure records were created + stmt = db.sql("select * from batch_insert_auto order by id") + n := 0 + names := ["aaa", "bbb", "ccc", "ddd"] + stmt.queryEach(null) |r| { + verifyEq(r->id, n+1) + verifyEq(r->name, names[n]) + n++ + } + } + ////////////////////////////////////////////////////////////////////////// // Pool ////////////////////////////////////////////////////////////////////////// From d382c7fd5e0a4e5d4dea21cd66153f67263c4050 Mon Sep 17 00:00:00 2001 From: Mike Jarmy Date: Thu, 30 Jul 2026 13:17:50 -0400 Subject: [PATCH 4/6] sql: fix typo in doc.md --- src/sql/doc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sql/doc.md b/src/sql/doc.md index 8beb54e1a..eb090ea2c 100644 --- a/src/sql/doc.md +++ b/src/sql/doc.md @@ -203,5 +203,5 @@ For postgres, setup the fantest database and user account via: In addition, both JDBC drivers must be installed, and "etc/sql/config.props" must have a reference to the classpath of both drivers, e.g. - java.drivers=java.drivers=com.mysql.cj.jdbc.Driver,org.postgresql.Driver + java.drivers=com.mysql.cj.jdbc.Driver,org.postgresql.Driver From 79d113ba325bdc302ebcb00a8f67e9a672333375 Mon Sep 17 00:00:00 2001 From: Mike Jarmy Date: Fri, 31 Jul 2026 09:11:53 -0400 Subject: [PATCH 5/6] sql: return BatchResult from Statement.executeBatch() --- src/sql/fan/BatchExecutor.fan | 30 +++++++----------- src/sql/fan/Statement.fan | 55 +++++++++++++++++++++------------ src/sql/java/StatementPeer.java | 26 +++++----------- src/sql/test/SqlTest.fan | 31 ++++++++----------- 4 files changed, 67 insertions(+), 75 deletions(-) diff --git a/src/sql/fan/BatchExecutor.fan b/src/sql/fan/BatchExecutor.fan index 261413018..82a19b53a 100644 --- a/src/sql/fan/BatchExecutor.fan +++ b/src/sql/fan/BatchExecutor.fan @@ -28,32 +28,25 @@ class BatchExecutor queued.add(params) if (queued.size > maxChunkSize) { - results.addAll(stmt.executeBatch(queued)) - keyList.addAll(stmt.batchKeys) + r := stmt.executeBatch(queued) + result.updateCounts.addAll(r.updateCounts) + result.keys.addAll(r.keys) queued.clear } } - ** Finish execution, and return the concatenated results from each - ** call to Statement.executeBatch(). - Int?[] finish() + ** Finish execution, and return the collated results from each call + ** to Statement.executeBatch(). + BatchResult finish() { if (queued.size > 0) { - results.addAll(stmt.executeBatch(queued)) - keyList.addAll(stmt.batchKeys) + r := stmt.executeBatch(queued) + result.updateCounts.addAll(r.updateCounts) + result.keys.addAll(r.keys) queued.clear } - return results - } - - ** The keys, if any, from the the finished BatchExecutor, with one - ** entry per call to add(). This method should not be called until - ** finish() has been called -- if you call it before then, it will - ** return an incomplete list of keys. - Obj?[] keys() - { - return keyList + return result } ////////////////////////////////////////////////////////////////////////// @@ -64,7 +57,6 @@ class BatchExecutor private Int maxChunkSize private [Str:Obj][] queued := [Str:Obj][,] - private Int?[] results := [,] - private Obj?[] keyList := [,] + private BatchResult result := BatchResult(Int?[,], Obj?[,]) } diff --git a/src/sql/fan/Statement.fan b/src/sql/fan/Statement.fan index 7f3ccb002..f5d45e1ee 100644 --- a/src/sql/fan/Statement.fan +++ b/src/sql/fan/Statement.fan @@ -66,14 +66,23 @@ class Statement ** ** Execute a batch of commands on a prepared Statement. If all commands - ** execute successfully, returns an array of update counts. + ** execute successfully, return a BatchResult containing an array of update + ** counts, and an array of auto-generated keys. ** - ** For each element in the array, if the element is non-null, then it - ** represents an update count giving the number of rows in the database that - ** were affected by the command's execution. + ** For each element in the update counts array, if the element is non-null, + ** then it represents an update count giving the number of rows in the + ** database that were affected by the command's execution. If a given array + ** element is null, it indicates that the command was processed successfully + ** but that the number of rows affected is unknown. ** - ** If a given array element is null, it indicates that the command was - ** processed successfully but that the number of rows affected is unknown. + ** The keys array contains the auto-generated keys, if any, for each command. + ** An element is null if the driver did not return a key for that command. + ** Note that some drivers return the keys even if they were not + ** auto-generated. + ** + ** If the driver returns auto-generated keys for some but not all of the + ** commands, throw SqlErr since the keys cannot be correlated to their + ** commands. See `batchKeys`. ** ** If one of the commands in a batch update fails to execute properly, this ** method throws a SqlErr that wraps a java.sql.BatchUpdateException, and a @@ -81,19 +90,7 @@ class Statement ** the batch, consistent with the underlying DBMS -- either always continuing ** to process commands or never continuing to process commands. ** - ** If the driver returns auto-generated keys for some but not all of the - ** commands, throw SqlErr since the keys cannot be correlated to their - ** commands. See `batchKeys`. - ** - native Int?[] executeBatch([Str:Obj]?[] paramsList) - - ** - ** The keys, if any, from the most recent call to `executeBatch()`, with one - ** entry per batch command. An entry is null if the driver did not return a - ** key for that command. Return an empty list if executeBatch has not been - ** called on this statement. - ** - native Obj?[] batchKeys() + native BatchResult executeBatch([Str:Obj]?[] paramsList) ** ** If the last execute has more results from a multi-result stored @@ -130,3 +127,23 @@ class Statement } +************************************************************************** +** BatchResult +************************************************************************** + +** BatchResult contains the result of a call to Statement.executeBatch(). +class BatchResult +{ + new make(Int?[] updateCounts, Obj?[] keys) + { + this.updateCounts = updateCounts + this.keys = keys + } + + ** The number of rows that were affected by each command's execution. + Int?[] updateCounts + + ** The keys that were auto-generated by each command. + Obj?[] keys +} + diff --git a/src/sql/java/StatementPeer.java b/src/sql/java/StatementPeer.java index e33e9f38c..ce5ccc8c1 100644 --- a/src/sql/java/StatementPeer.java +++ b/src/sql/java/StatementPeer.java @@ -338,12 +338,6 @@ private static Object readGenKey(ResultSet rs) catch (Exception e) { return rs.getString(1); } } - /** Empty auto-generated key list. */ - private static List emptyGenKeys() - { - return List.make(Sys.ObjType.toNullable(), 0); - } - /** * Auto-generated keys for the batch just executed, with one entry per * command. Entries are null if the driver returned no keys at all. If @@ -379,14 +373,13 @@ private List readBatchGenKeys(PreparedStatement pstmt, int numCommands) return keys; } - public List executeBatch(Statement self, List paramsList) + public BatchResult executeBatch(Statement self, List paramsList) { if (!prepared) throw SqlErr.make("Statement has not been prepared."); PreparedStatement pstmt = (PreparedStatement)stmt; - // discard keys from any previous batch - this.genKeys = emptyGenKeys(); + List genKeys = List.make(Sys.ObjType.toNullable(), 0); try { @@ -401,10 +394,10 @@ public List executeBatch(Statement self, List paramsList) int[] exec = pstmt.executeBatch(); // gather auto-generated keys before the statement is reused - this.genKeys = readBatchGenKeys(pstmt, (int)paramsList.size()); + genKeys = readBatchGenKeys(pstmt, (int)paramsList.size()); // process result - List result = List.make(Sys.IntType, exec.length); + List updateCounts = List.make(Sys.IntType, exec.length); for (int i = 0; i < exec.length; i++) { int n = exec[i]; @@ -413,9 +406,10 @@ public List executeBatch(Statement self, List paramsList) // java.sql.Statement.SUCCESS_NO_INFO. We treat that as a null, // indicating that the command was processed successfully but that the // number of rows affected is unknown. - result.add(n < 0 ? null : (long)n); + updateCounts.add(n < 0 ? null : (long)n); } - return result; + + return BatchResult.make(updateCounts, genKeys); } catch (SQLException ex) { @@ -423,11 +417,6 @@ public List executeBatch(Statement self, List paramsList) } } - public List batchKeys(Statement self) - { - return genKeys; - } - public List more(Statement self) { try @@ -528,7 +517,6 @@ private void createStatement(Statement self) private java.sql.Statement stmt; private Map paramMap; private int limit = 0; // limit field value - private List genKeys = emptyGenKeys(); // keys from last executeBatch // These are set during init(): private boolean isInsert; // does sql contain insert keyword diff --git a/src/sql/test/SqlTest.fan b/src/sql/test/SqlTest.fan index c1d140a6d..ff6edc5d9 100644 --- a/src/sql/test/SqlTest.fan +++ b/src/sql/test/SqlTest.fan @@ -558,11 +558,10 @@ class SqlTest : Test // The result will be an array filled with '1', // indicating that each row was updated. - verifyEq(res, Int[,].fill(1, params.size)) + verifyEq(res.updateCounts, Int[,].fill(1, params.size)) // The keys will all be null, because nothing was autogenerated - keys := stmt.batchKeys - verifyEq(keys, Obj?[,].fill(null, params.size)) + verifyEq(res.keys, Obj?[,].fill(null, params.size)) // double check db.commit @@ -599,7 +598,7 @@ class SqlTest : Test // batch insert stmt := db.sql( "insert into batch_insert (id, name) values (@id, @name)").prepare - stmt.executeBatch([ + res := stmt.executeBatch([ ["id": 1, "name":"aaa"], ["id": 2, "name":"bbb"], ["id": 3, "name":"ccc"], @@ -608,11 +607,10 @@ class SqlTest : Test // check keys. Note that postgres populates the keys even // though they weren't auto-generated - keys := stmt.batchKeys if (dbType == DbType.postgres) - verifyEq(keys, Obj?[1,2,3,4]) + verifyEq(res.keys, Obj?[1,2,3,4]) else - verifyEq(keys, Obj?[null,null,null,null]) + verifyEq(res.keys, Obj?[null,null,null,null]) // make sure records were created stmt = db.sql("select * from batch_insert order by id") @@ -650,7 +648,7 @@ class SqlTest : Test // batch insert stmt := db.sql( "insert into batch_insert_auto (name) values (@name)").prepare - stmt.executeBatch([ + res := stmt.executeBatch([ ["name":"aaa"], ["name":"bbb"], ["name":"ccc"], @@ -658,8 +656,7 @@ class SqlTest : Test ]) // check auto-generated keys - keys := stmt.batchKeys - verifyEq(keys, Obj?[1,2,3,4]) + verifyEq(res.keys, Obj?[1,2,3,4]) // make sure records were created stmt = db.sql("select * from batch_insert_auto order by id") @@ -687,7 +684,7 @@ class SqlTest : Test res := batch.finish // The result will be an array filled with '1', // indicating that each row was updated. - verifyEq(res, Int?[,].fill(1, ids.size)) + verifyEq(res.updateCounts, Int?[,].fill(1, ids.size)) // double check db.commit @@ -731,15 +728,14 @@ class SqlTest : Test ["id": 3, "name":"ccc"], ["id": 4, "name":"ddd"], ].each |x| { batch.add(x) } - batch.finish + res := batch.finish // check keys. Note that postgres populates the keys even // though they weren't auto-generated - keys := batch.keys if (dbType == DbType.postgres) - verifyEq(keys, Obj?[1,2,3,4]) + verifyEq(res.keys, Obj?[1,2,3,4]) else - verifyEq(keys, Obj?[null,null,null,null]) + verifyEq(res.keys, Obj?[null,null,null,null]) // make sure records were created stmt = db.sql("select * from batch_insert order by id") @@ -784,11 +780,10 @@ class SqlTest : Test ["name":"ccc"], ["name":"ddd"], ].each |x| { batch.add(x) } - batch.finish + res := batch.finish // check auto-generated keys - keys := batch.keys - verifyEq(keys, Obj?[1,2,3,4]) + verifyEq(res.keys, Obj?[1,2,3,4]) // make sure records were created stmt = db.sql("select * from batch_insert_auto order by id") From e86859d31f105067d3015512853bb45ef5f3490b Mon Sep 17 00:00:00 2001 From: Mike Jarmy Date: Fri, 31 Jul 2026 09:13:32 -0400 Subject: [PATCH 6/6] sql: fix typo in docs --- src/sql/fan/Statement.fan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sql/fan/Statement.fan b/src/sql/fan/Statement.fan index f5d45e1ee..6f2365ba2 100644 --- a/src/sql/fan/Statement.fan +++ b/src/sql/fan/Statement.fan @@ -82,7 +82,7 @@ class Statement ** ** If the driver returns auto-generated keys for some but not all of the ** commands, throw SqlErr since the keys cannot be correlated to their - ** commands. See `batchKeys`. + ** commands. ** ** If one of the commands in a batch update fails to execute properly, this ** method throws a SqlErr that wraps a java.sql.BatchUpdateException, and a