Skip to content

Commit

Permalink
feat: add JSON type support (#1799)
Browse files Browse the repository at this point in the history
Fixes b/213373577

cc @nageshs
  • Loading branch information
stephaniewang526 committed Jan 27, 2022
1 parent ca344f5 commit 73c4a73
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 1 deletion.
4 changes: 4 additions & 0 deletions google-cloud-bigquery/pom.xml
Expand Up @@ -81,6 +81,10 @@
<groupId>org.threeten</groupId>
<artifactId>threetenbp</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

<!-- auto-value creates a class that uses an annotation from error_prone_annotations -->
<dependency>
Expand Down
Expand Up @@ -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<StandardSQLTypeName, LegacySQLTypeName> standardToLegacyMap = new HashMap<>();

Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -61,6 +62,7 @@
* <li>Float: StandardSQLTypeName.FLOAT64
* <li>BigDecimal: StandardSQLTypeName.NUMERIC
* <li>BigNumeric: StandardSQLTypeName.BIGNUMERIC
* <li>JSON: StandardSQLTypeName.JSON
* </ul>
*
* <p>No other types are supported through that entry point. The other types can be created by
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -347,6 +365,10 @@ private static <T> StandardSQLTypeName classToType(Class<T> 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);
}
Expand Down Expand Up @@ -384,6 +406,9 @@ private static <T> 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:
Expand Down
Expand Up @@ -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
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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});
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object> jsonRow1 =
Collections.singletonMap(
"jsonField", "{\"student\" : {\"name\" : \"Jane\", \"id\": 10}}");
Map<String, Object> jsonRow2 =
Collections.singletonMap("jsonField", "{\"student\" : {\"name\" : \"Joy\", \"id\": 11}}");
Map<String, Object> jsonRow3 =
Collections.singletonMap(
"jsonField", "{\"student\" : {\"name\" : \"Alice\", \"id\": 12}}");
Map<String, Object> 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<FieldValueList> 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<FieldValueList> 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";
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Expand Up @@ -93,6 +93,12 @@
<version>${google-api-services-bigquery.version}</version>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
Expand Down

0 comments on commit 73c4a73

Please sign in to comment.