diff --git a/google-cloud-bigquery/pom.xml b/google-cloud-bigquery/pom.xml index 6796b91c8b..1c3f4a9bac 100644 --- a/google-cloud-bigquery/pom.xml +++ b/google-cloud-bigquery/pom.xml @@ -81,6 +81,10 @@ org.threeten threetenbp + + com.google.code.gson + gson + diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/LegacySQLTypeName.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/LegacySQLTypeName.java index 56d66cb1a1..1c2a3d884a 100644 --- a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/LegacySQLTypeName.java +++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/LegacySQLTypeName.java @@ -93,6 +93,9 @@ public LegacySQLTypeName apply(String constant) { /** A record type with a nested schema. */ public static final LegacySQLTypeName RECORD = type.createAndRegister("RECORD").setStandardType(StandardSQLTypeName.STRUCT); + /** Represents JSON data */ + public static final LegacySQLTypeName JSON = + type.createAndRegister("JSON").setStandardType(StandardSQLTypeName.JSON); private static Map standardToLegacyMap = new HashMap<>(); diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryParameterValue.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryParameterValue.java index 76e521d561..f7f7e384f6 100644 --- a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryParameterValue.java +++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryParameterValue.java @@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.io.BaseEncoding; +import com.google.gson.JsonObject; import java.io.Serializable; import java.math.BigDecimal; import java.util.ArrayList; @@ -61,6 +62,7 @@ *
  • Float: StandardSQLTypeName.FLOAT64 *
  • BigDecimal: StandardSQLTypeName.NUMERIC *
  • BigNumeric: StandardSQLTypeName.BIGNUMERIC + *
  • JSON: StandardSQLTypeName.JSON * * *

    No other types are supported through that entry point. The other types can be created by @@ -254,6 +256,22 @@ public static QueryParameterValue string(String value) { return of(value, StandardSQLTypeName.STRING); } + /** + * Creates a {@code QueryParameterValue} object with a type of JSON. Currently, this is only + * supported in INSERT, not in query as a filter + */ + public static QueryParameterValue json(String value) { + return of(value, StandardSQLTypeName.JSON); + } + + /** + * Creates a {@code QueryParameterValue} object with a type of JSON. Currently, this is only + * supported in INSERT, not in query as a filter + */ + public static QueryParameterValue json(JsonObject value) { + return of(value, StandardSQLTypeName.JSON); + } + /** Creates a {@code QueryParameterValue} object with a type of BYTES. */ public static QueryParameterValue bytes(byte[] value) { return of(value, StandardSQLTypeName.BYTES); @@ -347,6 +365,10 @@ private static StandardSQLTypeName classToType(Class type) { return StandardSQLTypeName.NUMERIC; } else if (Date.class.isAssignableFrom(type)) { return StandardSQLTypeName.DATE; + } else if (String.class.isAssignableFrom(type)) { + return StandardSQLTypeName.JSON; + } else if (JsonObject.class.isAssignableFrom(type)) { + return StandardSQLTypeName.JSON; } throw new IllegalArgumentException("Unsupported object type for QueryParameter: " + type); } @@ -384,6 +406,9 @@ private static String valueToStringOrNull(T value, StandardSQLTypeName type) break; case STRING: return value.toString(); + case JSON: + if (value instanceof String || value instanceof JsonObject) return value.toString(); + break; case STRUCT: throw new IllegalArgumentException("Cannot convert STRUCT to String value"); case ARRAY: diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/StandardSQLTypeName.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/StandardSQLTypeName.java index d618b76568..57152a2a6c 100644 --- a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/StandardSQLTypeName.java +++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/StandardSQLTypeName.java @@ -56,5 +56,7 @@ public enum StandardSQLTypeName { /** Represents a year, month, day, hour, minute, second, and subsecond (microsecond precision). */ DATETIME, /** Represents a set of geographic points, represented as a Well Known Text (WKT) string. */ - GEOGRAPHY + GEOGRAPHY, + /** Represents JSON data */ + JSON } diff --git a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/QueryParameterValueTest.java b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/QueryParameterValueTest.java index b643ae5808..679b6ec5c9 100644 --- a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/QueryParameterValueTest.java +++ b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/QueryParameterValueTest.java @@ -24,6 +24,7 @@ import com.google.api.services.bigquery.model.QueryParameterType; import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonObject; import java.math.BigDecimal; import java.text.ParseException; import java.util.Date; @@ -193,6 +194,24 @@ public void testString() { assertThat(value.getArrayValues()).isNull(); } + @Test + public void testJson() { + QueryParameterValue value = + QueryParameterValue.json("{\"class\" : {\"students\" : [{\"name\" : \"Jane\"}]}}"); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("class", "student"); + QueryParameterValue value1 = QueryParameterValue.json(jsonObject); + assertThat(value.getValue()) + .isEqualTo("{\"class\" : {\"students\" : [{\"name\" : \"Jane\"}]}}"); + assertThat(value1.getValue()).isEqualTo("{\"class\":\"student\"}"); + assertThat(value.getType()).isEqualTo(StandardSQLTypeName.JSON); + assertThat(value1.getType()).isEqualTo(StandardSQLTypeName.JSON); + assertThat(value.getArrayType()).isNull(); + assertThat(value1.getArrayType()).isNull(); + assertThat(value.getArrayValues()).isNull(); + assertThat(value1.getArrayType()).isNull(); + } + @Test public void testBytes() { QueryParameterValue value = QueryParameterValue.bytes(new byte[] {1, 3}); diff --git a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java index 479a80e89f..09c9b7a7b7 100644 --- a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java +++ b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java @@ -122,6 +122,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.common.io.BaseEncoding; +import com.google.gson.JsonObject; import java.io.IOException; import java.math.BigDecimal; import java.nio.ByteBuffer; @@ -707,6 +708,113 @@ public void testCreateTableWithRangePartitioning() { } } + @Test + public void testJsonType() throws InterruptedException { + String tableName = "test_create_table_jsontype"; + TableId tableId = TableId.of(DATASET, tableName); + Schema schema = Schema.of(Field.of("jsonField", StandardSQLTypeName.JSON)); + StandardTableDefinition standardTableDefinition = StandardTableDefinition.of(schema); + try { + // Create a table with a JSON column + Table createdTable = bigquery.create(TableInfo.of(tableId, standardTableDefinition)); + assertNotNull(createdTable); + + // Insert 4 rows of JSON data into the JSON column + Map jsonRow1 = + Collections.singletonMap( + "jsonField", "{\"student\" : {\"name\" : \"Jane\", \"id\": 10}}"); + Map jsonRow2 = + Collections.singletonMap("jsonField", "{\"student\" : {\"name\" : \"Joy\", \"id\": 11}}"); + Map jsonRow3 = + Collections.singletonMap( + "jsonField", "{\"student\" : {\"name\" : \"Alice\", \"id\": 12}}"); + Map jsonRow4 = + Collections.singletonMap( + "jsonField", "{\"student\" : {\"name\" : \"Bijoy\", \"id\": 14}}"); + InsertAllRequest request = + InsertAllRequest.newBuilder(tableId) + .addRow(jsonRow1) + .addRow(jsonRow2) + .addRow(jsonRow3) + .addRow(jsonRow4) + .build(); + InsertAllResponse response = bigquery.insertAll(request); + assertFalse(response.hasErrors()); + assertEquals(0, response.getInsertErrors().size()); + + // Query the JSON column with string positional query parameter + String sql = + "SELECT jsonField.class.student.id FROM " + + tableId.getTable() + + " WHERE JSON_VALUE(jsonField, \"$.class.student.name\") = ? "; + QueryParameterValue stringParameter = QueryParameterValue.string("Jane"); + QueryJobConfiguration queryJobConfiguration = + QueryJobConfiguration.newBuilder(sql) + .setDefaultDataset(DatasetId.of(DATASET)) + .setUseLegacySql(false) + .addPositionalParameter(stringParameter) + .build(); + TableResult result = bigquery.query(queryJobConfiguration); + for (FieldValueList values : result.iterateAll()) { + assertEquals("10", values.get(0).getValue()); + } + + // Insert another JSON row parsed from a String with json positional query parameter + String dml = "INSERT INTO " + tableId.getTable() + " (jsonField) VALUES(?)"; + QueryParameterValue jsonParameter = + QueryParameterValue.json("{\"class\" : {\"student\" : [{\"name\" : \"Amy\"}]}}"); + QueryJobConfiguration dmlQueryJobConfiguration = + QueryJobConfiguration.newBuilder(dml) + .setDefaultDataset(DatasetId.of(DATASET)) + .setUseLegacySql(false) + .addPositionalParameter(jsonParameter) + .build(); + bigquery.query(dmlQueryJobConfiguration); + Page rows = bigquery.listTableData(tableId); + assertEquals(5, Iterables.size(rows.getValues())); + + // Insert another JSON row parsed from a JsonObject with json positional query parameter + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("class", "student"); + QueryParameterValue jsonParameter1 = QueryParameterValue.json(jsonObject); + QueryJobConfiguration dmlQueryJobConfiguration1 = + QueryJobConfiguration.newBuilder(dml) + .setDefaultDataset(DatasetId.of(DATASET)) + .setUseLegacySql(false) + .addPositionalParameter(jsonParameter1) + .build(); + bigquery.query(dmlQueryJobConfiguration1); + Page rows1 = bigquery.listTableData(tableId); + assertEquals(6, Iterables.size(rows1.getValues())); + int rowCount = 0; + for (FieldValueList row : rows1.iterateAll()) { + FieldValue jsonCell = row.get(0); + if (rowCount == 1) assertEquals("{\"class\":\"student\"}", jsonCell.getStringValue()); + rowCount++; + } + + // Try inserting a malformed JSON + QueryParameterValue badJsonParameter = + QueryParameterValue.json("{\"class\" : {\"student\" : [{\"name\" : \"BadBoy\"}}"); + QueryJobConfiguration dmlQueryJobConfiguration2 = + QueryJobConfiguration.newBuilder(dml) + .setDefaultDataset(DatasetId.of(DATASET)) + .setUseLegacySql(false) + .addPositionalParameter(badJsonParameter) + .build(); + try { + bigquery.query(dmlQueryJobConfiguration2); + fail("Querying with malformed JSON shouldn't work"); + } catch (BigQueryException e) { + BigQueryError error = e.getError(); + assertNotNull(error); + assertEquals("invalidQuery", error.getReason()); + } + } finally { + assertTrue(bigquery.delete(tableId)); + } + } + @Test public void testCreateTableWithConstraints() { String tableName = "test_create_table_with_constraints"; diff --git a/pom.xml b/pom.xml index 30898a62df..8a55b2ed67 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,12 @@ ${google-api-services-bigquery.version} + + com.google.code.gson + gson + 2.8.9 + + junit