diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbResultDml.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbResultDml.java new file mode 100644 index 00000000000..4e92fd2a616 --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbResultDml.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.dbclient; + +import java.util.stream.Stream; + +/** + * Result of DML statement execution. + */ +public interface DbResultDml extends AutoCloseable { + + /** + * Retrieves any auto-generated keys created as a result of executing this DML statement. + * + * @return the auto-generated keys + */ + Stream generatedKeys(); + + /** + * Retrieve statement execution result. + * + * @return row count for Data Manipulation Language (DML) statements or {@code 0} + * for statements that return nothing. + */ + long result(); + + /** + * Create new instance of DML statement execution result. + * + * @param generatedKeys the auto-generated keys + * @param result the statement execution result + * @return new instance of DML statement execution result + */ + static DbResultDml create(Stream generatedKeys, long result) { + return new DbResultDmlImpl(generatedKeys, result); + } + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbResultDmlImpl.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbResultDmlImpl.java new file mode 100644 index 00000000000..c2659b860bd --- /dev/null +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbResultDmlImpl.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.dbclient; + +import java.util.Objects; +import java.util.stream.Stream; + +// DbResultDml implementation +record DbResultDmlImpl(Stream generatedKeys, long result) implements DbResultDml { + + DbResultDmlImpl { + Objects.requireNonNull(generatedKeys, "List of auto-generated keys value is null"); + if (result < 0) { + throw new IllegalArgumentException("Statement execution result value is less than 0"); + } + } + + @Override + public void close() throws Exception { + generatedKeys.close(); + } + +} diff --git a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementDml.java b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementDml.java index 70532049f3d..37f1c11fe61 100644 --- a/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementDml.java +++ b/dbclient/dbclient/src/main/java/io/helidon/dbclient/DbStatementDml.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ package io.helidon.dbclient; +import java.util.List; + /** * Data Manipulation Language (DML) database statement. * A DML statement modifies records in the database and returns the number of modified records. @@ -24,7 +26,35 @@ public interface DbStatementDml extends DbStatement { /** * Execute this statement using the parameters configured with {@code params} and {@code addParams} methods. * - * @return The result of this statement. + * @return the result of this statement */ long execute(); + + /** + * Execute {@code INSERT} statement using the parameters configured with {@code params} and {@code addParams} methods + * and return compound result with generated keys. + * + * @return the result of this statement with generated keys + */ + DbResultDml insert(); + + /** + * Set auto-generated keys to be returned from the statement execution using {@link #insert()}. + * Only one method from {@link #returnGeneratedKeys()} and {@link #returnColumns(List)} may be used. + * This feature is database provider specific and some databases require specific columns to be set. + * + * @return updated db statement + */ + DbStatementDml returnGeneratedKeys(); + + /** + * Set column names to be returned from the inserted row or rows from the statement execution using {@link #insert()}. + * Only one method from {@link #returnGeneratedKeys()} and {@link #returnColumns(List)} may be used. + * This feature is database provider specific. + * + * @param columnNames an array of column names indicating the columns that should be returned from the inserted row or rows + * @return updated db statement + */ + DbStatementDml returnColumns(List columnNames); + } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java index f109f58e346..ebab7240d37 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java @@ -79,6 +79,15 @@ private JdbcExecuteContext jdbcContext() { return context(JdbcExecuteContext.class); } + /** + * Set the connection. + * + * @param connection the database connection + */ + protected void connection(Connection connection) { + this.connection = connection; + } + /** * Create the {@link PreparedStatement}. * @@ -120,10 +129,10 @@ protected PreparedStatement prepareStatement(String stmtName, String stmt) { /** * Create the {@link PreparedStatement}. * - * @param connection connection + * @param connection the database connection * @param stmtName statement name * @param stmt statement text - * @return statement + * @return new instance of {@link PreparedStatement} */ protected PreparedStatement prepareStatement(Connection connection, String stmtName, String stmt) { try { diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java index 65bd3b94e45..51cc405b481 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementDml.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,22 @@ */ package io.helidon.dbclient.jdbc; +import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; +import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import io.helidon.dbclient.DbClientException; import io.helidon.dbclient.DbClientServiceContext; +import io.helidon.dbclient.DbResultDml; +import io.helidon.dbclient.DbRow; import io.helidon.dbclient.DbStatementDml; import io.helidon.dbclient.DbStatementException; import io.helidon.dbclient.DbStatementType; @@ -29,7 +40,17 @@ */ class JdbcStatementDml extends JdbcStatement implements DbStatementDml { + static final String[] EMPTY_STRING_ARRAY = new String[0]; + private final DbStatementType type; + // Column names to be returned from the inserted row or rows from the statement execution. + // Value of null (default) indicates no columns are set. + private List columnNames = List.of(); + // Whether PreparedStatement shall be created with Statement.RETURN_GENERATED_KEYS: + // - value of false (default) indicates that autoGeneratedKeys won't be passed to PreparedStatement creation + // - value of true indicates that Statement.RETURN_GENERATED_KEYS as autoGeneratedKeys will be passed + // to PreparedStatement creation + private boolean returnGeneratedKeys; /** * Create a new instance. @@ -58,6 +79,45 @@ public long execute() { }); } + @Override + public DbResultDml insert() { + return doExecute((future, context) -> doInsert(this, future, context, this::closeConnection)); + } + + @Override + public DbStatementDml returnGeneratedKeys() { + if (!columnNames.isEmpty()) { + throw new IllegalStateException("Method returnColumns(String[]) was already called to set specific column names."); + } + returnGeneratedKeys = true; + return this; + } + + @Override + public DbStatementDml returnColumns(List columnNames) { + if (returnGeneratedKeys) { + throw new IllegalStateException("Method returnGeneratedKeys() was already called."); + } + Objects.requireNonNull(columnNames, "List of column names value is null"); + this.columnNames = Collections.unmodifiableList(columnNames); + return this; + } + + @Override + protected PreparedStatement prepareStatement(Connection connection, String stmtName, String stmt) { + try { + connection(connection); + if (returnGeneratedKeys) { + return connection.prepareStatement(stmt, Statement.RETURN_GENERATED_KEYS); + } else if (!columnNames.isEmpty()) { + return connection.prepareStatement(stmt, columnNames.toArray(EMPTY_STRING_ARRAY)); + } + return connection.prepareStatement(stmt); + } catch (SQLException e) { + throw new DbClientException(String.format("Failed to prepare statement: %s", stmtName), e); + } + } + /** * Execute the given statement. * @@ -75,7 +135,41 @@ static long doExecute(JdbcStatement dbStmt, future.complete(result); return result; } catch (SQLException ex) { + dbStmt.closeConnection(); throw new DbStatementException("Failed to execute statement", dbStmt.context().statement(), ex); } } + + /** + * Execute the given insert statement. + * + * @param dbStmt db statement + * @param future query future + * @param context service context + * @return query result + */ + static DbResultDml doInsert(JdbcStatement dbStmt, + CompletableFuture future, + DbClientServiceContext context, + Runnable onClose) { + PreparedStatement statement; + try { + statement = dbStmt.prepareStatement(context); + long result = statement.executeUpdate(); + ResultSet rs = statement.getGeneratedKeys(); + JdbcRow.Spliterator spliterator = new JdbcRow.Spliterator(rs, statement, dbStmt.context(), future); + Stream generatedKeys = autoClose(StreamSupport.stream(spliterator, false) + .onClose(() -> { + spliterator.close(); + if (onClose != null) { + onClose.run(); + } + })); + return DbResultDml.create(generatedKeys, result); + } catch (SQLException ex) { + dbStmt.closeConnection(); + throw new DbStatementException("Failed to execute statement", dbStmt.context().statement(), ex); + } + } + } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java index 3938f476c42..f917830495a 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatementQuery.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,7 +82,7 @@ static Stream doExecute(JdbcStatement dbStmt, })); } catch (SQLException ex) { dbStmt.closeConnection(); - throw new DbStatementException("Failed to create Statement", dbStmt.context().statement(), ex); + throw new DbStatementException("Failed to execute Statement", dbStmt.context().statement(), ex); } } } diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatementDml.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatementDml.java index 9f33049e4fd..96d8846e84f 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatementDml.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatementDml.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,16 @@ */ package io.helidon.dbclient.jdbc; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import io.helidon.dbclient.DbClientException; +import io.helidon.dbclient.DbResultDml; import io.helidon.dbclient.DbStatementDml; import io.helidon.dbclient.DbStatementType; @@ -24,6 +34,14 @@ class JdbcTransactionStatementDml extends JdbcTransactionStatement implements DbStatementDml { private final DbStatementType type; + // Column names to be returned from the inserted row or rows from the statement execution. + // Value of null (default) indicates no columns are set. + private List columnNames = List.of(); + // Whether PreparedStatement shall be created with Statement.RETURN_GENERATED_KEYS: + // - value of false (default) indicates that autoGeneratedKeys won't be passed to PreparedStatement creation + // - value of true indicates that Statement.RETURN_GENERATED_KEYS as autoGeneratedKeys will be passed + // to PreparedStatement creation + private boolean returnGeneratedKeys; /** * Create a new instance. @@ -49,4 +67,44 @@ public DbStatementType statementType() { public long execute() { return doExecute((future, context) -> JdbcStatementDml.doExecute(this, future, context)); } + + @Override + public DbResultDml insert() { + return doExecute((future, context) -> JdbcStatementDml.doInsert(this, future, context, null)); + } + + @Override + public DbStatementDml returnGeneratedKeys() { + if (!columnNames.isEmpty()) { + throw new IllegalStateException("Method returnColumns(String[]) was already called to set specific column names."); + } + returnGeneratedKeys = true; + return this; + } + + @Override + public DbStatementDml returnColumns(List columnNames) { + if (returnGeneratedKeys) { + throw new IllegalStateException("Method returnGeneratedKeys() was already called."); + } + Objects.requireNonNull(columnNames, "List of column names value is null"); + this.columnNames = Collections.unmodifiableList(columnNames); + return this; + } + + @Override + protected PreparedStatement prepareStatement(Connection connection, String stmtName, String stmt) { + try { + connection(connection); + if (returnGeneratedKeys) { + return connection.prepareStatement(stmt, Statement.RETURN_GENERATED_KEYS); + } else if (!columnNames.isEmpty()) { + return connection.prepareStatement(stmt, columnNames.toArray(JdbcStatementDml.EMPTY_STRING_ARRAY)); + } + return connection.prepareStatement(stmt); + } catch (SQLException e) { + throw new DbClientException(String.format("Failed to prepare statement: %s", stmtName), e); + } + } + } diff --git a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementDml.java b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementDml.java index 0b1f52d49f5..2920bcfebc2 100644 --- a/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementDml.java +++ b/dbclient/mongodb/src/main/java/io/helidon/dbclient/mongodb/MongoDbStatementDml.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,25 @@ */ package io.helidon.dbclient.mongodb; +import java.util.List; +import java.util.stream.Stream; + import io.helidon.dbclient.DbExecuteContext; +import io.helidon.dbclient.DbResultDml; import io.helidon.dbclient.DbStatementDml; import io.helidon.dbclient.DbStatementType; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import com.mongodb.client.result.DeleteResult; +import com.mongodb.client.result.InsertOneResult; import com.mongodb.client.result.UpdateResult; +import org.bson.BsonDocument; +import org.bson.BsonDocumentReader; +import org.bson.BsonValue; import org.bson.Document; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.DocumentCodec; /** * MongoDB {@link DbStatementDml} implementation. @@ -31,8 +41,11 @@ public class MongoDbStatementDml extends MongoDbStatement implements DbStatementDml { private static final System.Logger LOGGER = System.getLogger(MongoDbStatementDml.class.getName()); - + // Whether generated ID shall be returned + private boolean returnGeneratedKeys; private final DbStatementType type; + private final DocumentCodec codec = new DocumentCodec(); + private final DecoderContext decoderContext = DecoderContext.builder().build(); /** * Create a new instance. @@ -80,12 +93,73 @@ public long execute() { }); } + @Override + @SuppressWarnings("resource") // DbResultDml life-cycle is handled by user of this method + public DbResultDml insert() { + return doExecute((future, context) -> { + MongoStatement stmt = new MongoStatement(type, prepareStatement(context)); + try { + DbResultDml result = switch (type) { + case INSERT -> executeInsertAsDbResultDml(stmt); + case UPDATE -> executeUpdateAsDbResultDml(stmt); + case DELETE -> executeDeleteAsDbResultDml(stmt); + default -> throw new UnsupportedOperationException(String.format( + "Statement operation not yet supported: %s", + type.name())); + }; + LOGGER.log(System.Logger.Level.DEBUG, () -> String.format( + "%s DML %s execution succeeded", + type.name(), + context().statementName())); + return result; + } catch (UnsupportedOperationException ex) { + throw ex; + } catch (Throwable throwable) { + LOGGER.log(System.Logger.Level.DEBUG, () -> String.format( + "%s DML %s execution failed", + type.name(), + context().statementName())); + throw throwable; + } + }); + } + + @Override + public DbStatementDml returnGeneratedKeys() { + returnGeneratedKeys = true; + return this; + } + + @Override + public DbStatementDml returnColumns(List columnNames) { + throw new UnsupportedOperationException("Retrieval of specific auto-generated columns is not supported for Mongo"); + } + private Long executeInsert(MongoStatement stmt) { MongoCollection mc = db().getCollection(stmt.getCollection()); mc.insertOne(stmt.getValue()); return 1L; } + private DbResultDml executeInsertAsDbResultDml(MongoStatement stmt) { + MongoCollection mc = db().getCollection(stmt.getCollection()); + InsertOneResult result = mc.insertOne(stmt.getValue()); + if (returnGeneratedKeys && result.wasAcknowledged()) { + BsonValue insertedId = result.getInsertedId(); + if (insertedId != null) { + return DbResultDml.create( + Stream.of( + new MongoDbRow( + codec.decode( + new BsonDocumentReader(new BsonDocument("_id", insertedId)), + decoderContext), + context())), + 1L); + } + } + return DbResultDml.create(Stream.of(), 1L); + } + private Long executeUpdate(MongoStatement stmt) { MongoCollection mc = db().getCollection(stmt.getCollection()); Document query = stmt.getQuery(); @@ -93,10 +167,35 @@ private Long executeUpdate(MongoStatement stmt) { return updateResult.getModifiedCount(); } + private DbResultDml executeUpdateAsDbResultDml(MongoStatement stmt) { + MongoCollection mc = db().getCollection(stmt.getCollection()); + Document query = stmt.getQuery(); + UpdateResult result = mc.updateMany(query, stmt.getValue()); + if (returnGeneratedKeys && result.wasAcknowledged()) { + BsonValue upsertedId = result.getUpsertedId(); + if (upsertedId != null) { + return DbResultDml.create( + Stream.of( + new MongoDbRow( + codec.decode( + new BsonDocumentReader(new BsonDocument("_id", upsertedId)), + decoderContext), + context())), + 1L); + } + } + return DbResultDml.create(Stream.of(), 1L); + } + private Long executeDelete(MongoStatement stmt) { MongoCollection mc = db().getCollection(stmt.getCollection()); Document query = stmt.getQuery(); DeleteResult deleteResult = mc.deleteMany(query); return deleteResult.getDeletedCount(); } + + private DbResultDml executeDeleteAsDbResultDml(MongoStatement stmt) { + return DbResultDml.create(Stream.of(), executeDelete(stmt)); + } + } diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/SimpleInsertIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/SimpleInsertIT.java index de26e70a6e7..daddd5c8894 100644 --- a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/SimpleInsertIT.java +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/SimpleInsertIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,14 @@ */ package io.helidon.tests.integration.dbclient.common.tests; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + import io.helidon.config.Config; import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbColumn; +import io.helidon.dbclient.DbResultDml; +import io.helidon.dbclient.DbRow; import io.helidon.tests.integration.dbclient.common.model.Pokemon; import io.helidon.tests.integration.dbclient.common.utils.TestConfig; @@ -25,6 +31,9 @@ import static io.helidon.tests.integration.dbclient.common.model.Type.TYPES; import static io.helidon.tests.integration.dbclient.common.utils.VerifyData.verifyInsertPokemon; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; /** * Test set of basic JDBC inserts. @@ -50,7 +59,7 @@ public SimpleInsertIT(DbClient dbClient, Config config) { * Verify {@code createNamedInsert(String, String)} API method with named parameters. */ @Test - public void testCreateNamedInsertStrStrNamedArgs() { + void testCreateNamedInsertStrStrNamedArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 1, "Bulbasaur", TYPES.get(4), TYPES.get(12)); String stmt = config.get("db.statements.insert-pokemon-named-arg").asString().get(); long result = dbClient.execute() @@ -64,7 +73,7 @@ public void testCreateNamedInsertStrStrNamedArgs() { * Verify {@code createNamedInsert(String)} API method with named parameters. */ @Test - public void testCreateNamedInsertStrNamedArgs() { + void testCreateNamedInsertStrNamedArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 2, "Ivysaur", TYPES.get(4), TYPES.get(12)); long result = dbClient.execute() .createNamedInsert("insert-pokemon-named-arg") @@ -77,7 +86,7 @@ public void testCreateNamedInsertStrNamedArgs() { * Verify {@code createNamedInsert(String)} API method with ordered parameters. */ @Test - public void testCreateNamedInsertStrOrderArgs() { + void testCreateNamedInsertStrOrderArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 3, "Venusaur", TYPES.get(4), TYPES.get(12)); long result = dbClient.execute() .createNamedInsert("insert-pokemon-order-arg") @@ -90,7 +99,7 @@ public void testCreateNamedInsertStrOrderArgs() { * Verify {@code createInsert(String)} API method with named parameters. */ @Test - public void testCreateInsertNamedArgs() { + void testCreateInsertNamedArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 4, "Magby", TYPES.get(10)); String stmt = config.get("db.statements.insert-pokemon-named-arg").asString().get(); long result = dbClient.execute() @@ -104,7 +113,7 @@ public void testCreateInsertNamedArgs() { * Verify {@code createInsert(String)} API method with ordered parameters. */ @Test - public void testCreateInsertOrderArgs() { + void testCreateInsertOrderArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 5, "Magmar", TYPES.get(10)); String stmt = config.get("db.statements.insert-pokemon-order-arg").asString().get(); long result = dbClient.execute() @@ -118,7 +127,7 @@ public void testCreateInsertOrderArgs() { * Verify {@code namedInsert(String)} API method with ordered parameters passed directly to the {@code insert} method. */ @Test - public void testNamedInsertOrderArgs() { + void testNamedInsertOrderArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 6, "Rattata", TYPES.get(1)); long result = dbClient.execute().namedInsert("insert-pokemon-order-arg", pokemon.getId(), pokemon.getName()); verifyInsertPokemon(dbClient, result, pokemon); @@ -128,11 +137,62 @@ public void testNamedInsertOrderArgs() { * Verify {@code insert(String)} API method with ordered parameters passed directly to the {@code insert} method. */ @Test - public void testInsertOrderArgs() { + void testInsertOrderArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 7, "Raticate", TYPES.get(1)); String stmt = config.get("db.statements.insert-pokemon-order-arg").asString().get(); long result = dbClient.execute().insert(stmt, pokemon.getId(), pokemon.getName()); verifyInsertPokemon(dbClient, result, pokemon); } + /** + * Verify {@code namedInsert(String)} API method with named parameters and returned generated keys. + */ + @Test + void testInsertNamedArgsReturnedKeys() throws Exception { + try (DbResultDml result = dbClient.execute().createNamedInsert("insert-match") + .addParam("red", Pokemon.POKEMONS.get(2).getId()) + .addParam("blue", Pokemon.POKEMONS.get(3).getId()) + .returnGeneratedKeys() + .insert()) { + List keys = result.generatedKeys().toList(); + long records = result.result(); + assertThat(records, equalTo(1L)); + assertThat(keys, hasSize(1)); + DbRow keysRow = keys.getFirst(); + AtomicInteger columnsCount = new AtomicInteger(0); + keysRow.forEach(dbColumn -> columnsCount.incrementAndGet()); + assertThat(columnsCount.get(), equalTo(1)); + DbColumn keyByName = keysRow.column("id"); + DbColumn keyByIndex = keysRow.column(1); + assertThat(keyByName, equalTo(keyByIndex)); + } + } + + /** + * Verify {@code namedInsert(String)} API method with named parameters and returned insert columns. + */ + @Test + void testInsertNamedArgsReturnedColumns() throws Exception { + try (DbResultDml result = dbClient.execute().createNamedInsert("insert-match") + .addParam("red", Pokemon.POKEMONS.get(4).getId()) + .addParam("blue", Pokemon.POKEMONS.get(5).getId()) + .returnColumns(List.of("id", "red")) + .insert()) { + List keys = result.generatedKeys().toList(); + long records = result.result(); + assertThat(records, equalTo(1L)); + assertThat(keys, hasSize(1)); + DbRow keysRow = keys.getFirst(); + AtomicInteger columnsCount = new AtomicInteger(0); + keysRow.forEach(dbColumn -> columnsCount.incrementAndGet()); + assertThat(columnsCount.get(), equalTo(2)); + DbColumn idByName = keysRow.column("id"); + DbColumn idByIndex = keysRow.column(1); + assertThat(idByName, equalTo(idByIndex)); + DbColumn redByName = keysRow.column("red"); + DbColumn redByIndex = keysRow.column(2); + assertThat(redByName, equalTo(redByIndex)); + } + } + } diff --git a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/TransactionInsertIT.java b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/TransactionInsertIT.java index a8d19199f40..540ea1504cd 100644 --- a/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/TransactionInsertIT.java +++ b/tests/integration/dbclient/common/src/main/java/io/helidon/tests/integration/dbclient/common/tests/TransactionInsertIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,14 @@ */ package io.helidon.tests.integration.dbclient.common.tests; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + import io.helidon.config.Config; import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbColumn; +import io.helidon.dbclient.DbResultDml; +import io.helidon.dbclient.DbRow; import io.helidon.dbclient.DbTransaction; import io.helidon.tests.integration.dbclient.common.model.Pokemon; import io.helidon.tests.integration.dbclient.common.utils.TestConfig; @@ -26,6 +32,9 @@ import static io.helidon.tests.integration.dbclient.common.model.Type.TYPES; import static io.helidon.tests.integration.dbclient.common.utils.VerifyData.verifyInsertPokemon; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; /** * Test set of basic JDBC inserts in transaction. @@ -48,7 +57,7 @@ public TransactionInsertIT(DbClient dbClient, Config config) { * Verify {@code createNamedInsert(String, String)} API method with named parameters. */ @Test - public void testCreateNamedInsertStrStrNamedArgs() { + void testCreateNamedInsertStrStrNamedArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 1, "Sentret", TYPES.get(1)); String stmt = config.get("db.statements.insert-pokemon-named-arg").asString().get(); DbTransaction tx = dbClient.transaction(); @@ -63,7 +72,7 @@ public void testCreateNamedInsertStrStrNamedArgs() { * Verify {@code createNamedInsert(String)} API method with named parameters. */ @Test - public void testCreateNamedInsertStrNamedArgs() { + void testCreateNamedInsertStrNamedArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 2, "Furret", TYPES.get(1)); DbTransaction tx = dbClient.transaction(); long result = tx @@ -77,7 +86,7 @@ public void testCreateNamedInsertStrNamedArgs() { * Verify {@code createNamedInsert(String)} API method with ordered parameters. */ @Test - public void testCreateNamedInsertStrOrderArgs() { + void testCreateNamedInsertStrOrderArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 3, "Chinchou", TYPES.get(11), TYPES.get(13)); DbTransaction tx = dbClient.transaction(); long result = tx @@ -91,7 +100,7 @@ public void testCreateNamedInsertStrOrderArgs() { * Verify {@code createInsert(String)} API method with named parameters. */ @Test - public void testCreateInsertNamedArgs() { + void testCreateInsertNamedArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 4, "Lanturn", TYPES.get(11), TYPES.get(13)); String stmt = config.get("db.statements.insert-pokemon-named-arg").asString().get(); DbTransaction tx = dbClient.transaction(); @@ -106,7 +115,7 @@ public void testCreateInsertNamedArgs() { * Verify {@code createInsert(String)} API method with ordered parameters. */ @Test - public void testCreateInsertOrderArgs() { + void testCreateInsertOrderArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 5, "Swinub", TYPES.get(5), TYPES.get(15)); String stmt = config.get("db.statements.insert-pokemon-order-arg").asString().get(); DbTransaction tx = dbClient.transaction(); @@ -121,7 +130,7 @@ public void testCreateInsertOrderArgs() { * Verify {@code namedInsert(String)} API method with ordered parameters passed directly to the {@code insert} method. */ @Test - public void testNamedInsertOrderArgs() { + void testNamedInsertOrderArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 6, "Piloswine", TYPES.get(5), TYPES.get(15)); DbTransaction tx = dbClient.transaction(); long result = tx @@ -134,7 +143,7 @@ public void testNamedInsertOrderArgs() { * Verify {@code insert(String)} API method with ordered parameters passed directly to the {@code insert} method. */ @Test - public void testInsertOrderArgs() { + void testInsertOrderArgs() { Pokemon pokemon = new Pokemon(BASE_ID + 7, "Mamoswine", TYPES.get(5), TYPES.get(15)); String stmt = config.get("db.statements.insert-pokemon-order-arg").asString().get(); DbTransaction tx = dbClient.transaction(); @@ -143,4 +152,64 @@ public void testInsertOrderArgs() { tx.commit(); verifyInsertPokemon(dbClient, result, pokemon); } + + /** + * Verify {@code namedInsert(String)} API method with named parameters and returned generated keys. + */ + @Test + void testInsertNamedArgsReturnedKeys() throws Exception { + DbTransaction tx = dbClient.transaction(); + try (DbResultDml result = tx.createNamedInsert("insert-match") + .addParam("red", Pokemon.POKEMONS.get(2).getId()) + .addParam("blue", Pokemon.POKEMONS.get(3).getId()) + .returnGeneratedKeys() + .insert()) { + List keys = result.generatedKeys().toList(); + long records = result.result(); + assertThat(records, equalTo(1L)); + assertThat(keys, hasSize(1)); + DbRow keysRow = keys.getFirst(); + AtomicInteger columnsCount = new AtomicInteger(0); + keysRow.forEach(dbColumn -> columnsCount.incrementAndGet()); + assertThat(columnsCount.get(), equalTo(1)); + DbColumn keyByName = keysRow.column("id"); + DbColumn keyByIndex = keysRow.column(1); + assertThat(keyByName, equalTo(keyByIndex)); + tx.commit(); + } catch (Exception ex) { + tx.rollback(); + } + } + + /** + * Verify {@code namedInsert(String)} API method with named parameters and returned insert columns. + */ + @Test + void testInsertNamedArgsReturnedColumns() throws Exception { + DbTransaction tx = dbClient.transaction(); + try (DbResultDml result = tx.createNamedInsert("insert-match") + .addParam("red", Pokemon.POKEMONS.get(4).getId()) + .addParam("blue", Pokemon.POKEMONS.get(5).getId()) + .returnColumns(List.of("id", "red")) + .insert()) { + List keys = result.generatedKeys().toList(); + long records = result.result(); + assertThat(records, equalTo(1L)); + assertThat(keys, hasSize(1)); + DbRow keysRow = keys.getFirst(); + AtomicInteger columnsCount = new AtomicInteger(0); + keysRow.forEach(dbColumn -> columnsCount.incrementAndGet()); + assertThat(columnsCount.get(), equalTo(2)); + DbColumn idByName = keysRow.column("id"); + DbColumn idByIndex = keysRow.column(1); + assertThat(idByName, equalTo(idByIndex)); + DbColumn redByName = keysRow.column("red"); + DbColumn redByIndex = keysRow.column(2); + assertThat(redByName, equalTo(redByIndex)); + tx.commit(); + } catch (Exception ex) { + tx.rollback(); + } + } + } diff --git a/tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/jdbc/H2SetupProvider.java b/tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/jdbc/H2SetupProvider.java index ddb6508c451..cde1985592a 100644 --- a/tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/jdbc/H2SetupProvider.java +++ b/tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/jdbc/H2SetupProvider.java @@ -103,6 +103,7 @@ private static void initSchema(DbClient dbClient) { exec.namedDml("create-types"); exec.namedDml("create-pokemons"); exec.namedDml("create-poketypes"); + exec.namedDml("create-matches"); } private static void initData(DbClient dbClient) { diff --git a/tests/integration/dbclient/h2/src/test/resources/h2.yaml b/tests/integration/dbclient/h2/src/test/resources/h2.yaml index 8e8ab7dc122..ecf0df19140 100644 --- a/tests/integration/dbclient/h2/src/test/resources/h2.yaml +++ b/tests/integration/dbclient/h2/src/test/resources/h2.yaml @@ -31,12 +31,15 @@ db: create-types: "CREATE TABLE Types (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" create-pokemons: "CREATE TABLE Pokemons (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" create-poketypes: "CREATE TABLE PokemonTypes (id_pokemon INTEGER NOT NULL REFERENCES Pokemons(id), id_type INTEGER NOT NULL REFERENCES Types(id))" + create-matches: "CREATE TABLE Matches (id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, red INTEGER NOT NULL REFERENCES Pokemons(id), blue INTEGER NOT NULL REFERENCES Pokemons(id))" drop-types: "DROP TABLE Types" drop-pokemons: "DROP TABLE Pokemons" drop-poketypes: "DROP TABLE PokemonTypes" + drop-matches: "DROP TABLE Matches" insert-type: "INSERT INTO Types(id, name) VALUES(?, ?)" insert-pokemon: "INSERT INTO Pokemons(id, name) VALUES(?, ?)" insert-poketype: "INSERT INTO PokemonTypes(id_pokemon, id_type) VALUES(?, ?)" + insert-match: "INSERT INTO Matches(red, blue) VALUES (:red, :blue)" select-types: "SELECT id, name FROM Types" select-pokemons: "SELECT id, name FROM Pokemons" select-poketypes: "SELECT id_pokemon, id_type FROM PokemonTypes p WHERE id_pokemon = ?"