diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java index 7ba6af89c7b6..014717fd0646 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java @@ -34,6 +34,7 @@ import com.google.cloud.bigquery.storage.v1.TableName; import com.google.gson.Gson; import com.google.gson.JsonArray; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.protobuf.Descriptors.DescriptorValidationException; import java.io.IOException; @@ -67,6 +68,7 @@ import java.util.Arrays; import java.util.Calendar; import java.util.LinkedList; +import java.util.List; import java.util.Queue; class BigQueryPreparedStatement extends BigQueryStatement implements PreparedStatement { @@ -389,15 +391,7 @@ private long bulkInsertWithWriteAPI(BigQueryWriteClient bigQueryWriteClient) FieldList fieldLists = this.insertSchema.getFields(); if (fieldLists.size() == parameterList.size()) { - JsonObject rowObject = new JsonObject(); - for (int j = 0; j < parameterList.size(); j++) { - BigQueryJdbcParameter parameter = parameterList.get(j); - if (parameter.getSqlType() == StandardSQLTypeName.STRING) { - rowObject.addProperty(fieldLists.get(j).getName(), parameter.getValue().toString()); - } else { - rowObject.addProperty(fieldLists.get(j).getName(), gson.toJson(parameter.getValue())); - } - } + JsonObject rowObject = createJsonRow(fieldLists, parameterList, gson); jsonArray.add(rowObject); if (jsonArray.size() == this.querySettings.getWriteAPIAppendRowCount() @@ -431,6 +425,22 @@ private long bulkInsertWithWriteAPI(BigQueryWriteClient bigQueryWriteClient) return rowCount; } + static JsonObject createJsonRow( + FieldList fieldLists, List parameterList, Gson gson) { + JsonObject rowObject = new JsonObject(); + for (int j = 0; j < parameterList.size(); j++) { + BigQueryJdbcParameter parameter = parameterList.get(j); + if (parameter.getValue() == null) { + rowObject.add(fieldLists.get(j).getName(), JsonNull.INSTANCE); + } else if (parameter.getSqlType() == StandardSQLTypeName.STRING) { + rowObject.addProperty(fieldLists.get(j).getName(), parameter.getValue().toString()); + } else { + rowObject.addProperty(fieldLists.get(j).getName(), gson.toJson(parameter.getValue())); + } + } + return rowObject; + } + private void setInsertMetadata(QueryStatistics statistics) throws SQLException { LOG.finer("++enter++"); if (!statistics.getStatementType().equals(StatementType.INSERT) diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java index 29175fee5122..466d21b870d4 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementSettersTest.java @@ -21,11 +21,18 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.cloud.bigquery.Field; +import com.google.cloud.bigquery.FieldList; +import com.google.cloud.bigquery.QueryJobConfiguration; import com.google.cloud.bigquery.Schema; import com.google.cloud.bigquery.StandardSQLTypeName; +import com.google.gson.Gson; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; import java.sql.Array; import java.sql.Date; import java.sql.ParameterMetaData; @@ -38,6 +45,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.ArrayList; import java.util.Calendar; import java.util.TimeZone; import org.junit.jupiter.api.BeforeEach; @@ -51,6 +59,7 @@ public class BigQueryPreparedStatementSettersTest { @BeforeEach public void setUp() throws Exception { connection = mock(BigQueryConnection.class); + when(connection.getQueryDialect()).thenReturn("SQL"); preparedStatement = new BigQueryPreparedStatement(connection, "SELECT ?, ?, ?, ?, ?"); } @@ -269,4 +278,51 @@ public void testSetObjectWithJavaTime() throws Exception { preparedStatement.setObject(4, instant); assertEquals(Timestamp.class, preparedStatement.parameterHandler.getType(4)); } + + // Verifies standard DML QueryJobConfiguration creates positional parameters with null values when + // setObject is called with null. + @Test + public void testBatchJobConfigurationWithSetObjectNull() throws Exception { + preparedStatement.setObject(1, null); + preparedStatement.setInt(2, 42); + preparedStatement.setString(3, "test"); + preparedStatement.setDouble(4, 3.14); + preparedStatement.setBoolean(5, true); + preparedStatement.addBatch(); + + ArrayList batchParams = + preparedStatement.parameterHandler.parametersList; + QueryJobConfiguration config = preparedStatement.getWriteBatchJobConfiguration(batchParams); + assertNotNull(config); + assertEquals(5, config.getPositionalParameters().size()); + assertNull(config.getPositionalParameters().get(0).getValue()); + assertEquals(StandardSQLTypeName.STRING, config.getPositionalParameters().get(0).getType()); + } + + // Verifies Storage Write API JSON row serialization maps null parameter values to + // JsonNull.INSTANCE. + @Test + public void testCreateJsonRowWithSetObjectNull() throws Exception { + preparedStatement.setObject(1, null); + preparedStatement.setInt(2, 42); + preparedStatement.setString(3, "test"); + preparedStatement.setDouble(4, 3.14); + preparedStatement.setBoolean(5, true); + + FieldList fieldList = + FieldList.of( + Field.of("col1", StandardSQLTypeName.STRING), + Field.of("col2", StandardSQLTypeName.INT64), + Field.of("col3", StandardSQLTypeName.STRING), + Field.of("col4", StandardSQLTypeName.FLOAT64), + Field.of("col5", StandardSQLTypeName.BOOL)); + + ArrayList params = preparedStatement.parameterHandler.parametersList; + JsonObject jsonRow = BigQueryPreparedStatement.createJsonRow(fieldList, params, new Gson()); + + assertNotNull(jsonRow); + assertEquals(JsonNull.INSTANCE, jsonRow.get("col1")); + assertTrue(jsonRow.get("col1").isJsonNull()); + assertEquals("42", jsonRow.get("col2").getAsString()); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITNightlyBigQueryTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITNightlyBigQueryTest.java index 8d51cf08df9f..1af8131f5587 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITNightlyBigQueryTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITNightlyBigQueryTest.java @@ -1197,6 +1197,57 @@ public void testBulkInsertOperation() throws SQLException { } } + // Verifies batch inserts via BigQuery Storage Write API succeed when parameters are bound to + // null. + @Test + public void testBulkInsertOperationWithSetObjectNull() throws SQLException { + String TABLE_NAME = "JDBC_BULK_INSERT_NULL_TABLE_" + randomNumber; + String createQuery = + String.format( + "CREATE OR REPLACE TABLE %s.%s (`StringField` STRING,\n" + + " `IntegerField` INTEGER," + + " `FloatField` FLOAT64," + + " `NumericField` NUMERIC," + + " `BigNumericField` BIGNUMERIC," + + " `BooleanField` BOOLEAN" + + " );", + DATASET, TABLE_NAME); + String insertQuery = + String.format("INSERT INTO %s.%s VALUES(?, ?, ?, ?, ?, ?);", DATASET, TABLE_NAME); + String dropQuery = String.format("DROP TABLE %s.%s", DATASET, TABLE_NAME); + String selectQuery = String.format("SELECT * FROM %s.%s", DATASET, TABLE_NAME); + + String connection_uri = + ITNightlyBigQueryTest.connection_uri + + "EnableWriteAPI=1;" + + "SWA_ActivationRowCount=5;" + + "SWA_AppendRowCount=500"; + + try (Connection connection = DriverManager.getConnection(connection_uri)) { + bigQueryStatement.execute(createQuery); + PreparedStatement statement = connection.prepareStatement(insertQuery); + for (int i = 0; i < 20; ++i) { + statement.setObject(1, null); + statement.setInt(2, i); + statement.setFloat(3, (float) (i + .6)); + statement.setInt(4, random.nextInt()); + statement.setInt(5, random.nextInt()); + statement.setBoolean(6, true); + + statement.addBatch(); + } + int[] result = statement.executeBatch(); + + ResultSet resultSet = bigQueryStatement.executeQuery(selectQuery); + assertEquals(result.length, resultSetRowCount(resultSet)); + + bigQueryStatement.execute(dropQuery); + + } catch (SQLException e) { + throw new BigQueryJdbcException(e); + } + } + @Test public void testBulkInsertOperationStandard() throws SQLException { String TABLE_NAME = "JDBC_BULK_INSERT_STANDARD_TABLE_" + randomNumber;