Skip to content

Commit

Permalink
Add mug-bigquery artifact, with SafeBigQuery facade class providing t…
Browse files Browse the repository at this point in the history
…emplating support using StringFormat.template() SPI
  • Loading branch information
xingyutangyuan committed Dec 3, 2023
1 parent a5008d1 commit e99cff7
Show file tree
Hide file tree
Showing 8 changed files with 525 additions and 0 deletions.
1 change: 1 addition & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ maven_install(
"com.google.protobuf:protobuf-java:[3.0.0,)",
"com.google.protobuf:protobuf-java-util:[3.0.0,)",
"com.google.code.findbugs:jsr305:3.0.2",
"com.google.cloud:google-cloud-bigquery:[2.34.2,)",
],
repositories = [
"https://repo1.maven.org/maven2",
Expand Down
31 changes: 31 additions & 0 deletions mug-bigquery/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
load("@com_googlesource_gerrit_bazlets//tools:junit.bzl", "junit_tests")

java_library(
name = "template",
srcs = glob(["src/main/java/**/*.java"]),
deps = [
"//mug:base",
"//mug:format",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_cloud_google_cloud_bigquery",
]
)

junit_tests(
name = "AllTests",
srcs = glob(["src/test/java/**/*Test.java"]),
deps = [
":template",
"//mug:base",
"//mug:format",
"//mug-guava",
"@maven//:com_google_guava_guava",
"@maven//:com_google_guava_guava_testlib",
"@maven//:com_google_truth_truth",
"@maven//:com_google_truth_extensions_truth_java8_extension",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_cloud_google_cloud_bigquery",
"@maven//:junit_junit",
"@maven//:org_junit_jupiter_junit_jupiter_api",
],
)
93 changes: 93 additions & 0 deletions mug-bigquery/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.google.mug</groupId>
<artifactId>mug-root</artifactId>
<version>7.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>mug-bigquery</artifactId>
<packaging>jar</packaging>
<name>BigQuery Utils</name>

<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.23.0</version>
</path>
<path>
<groupId>${project.groupId}</groupId>
<artifactId>mug-errorprone</artifactId>
<version>${project.version}</version>
</path>
<!-- Other annotation processors go here.
If 'annotationProcessorPaths' is set, processors will no longer be
discovered on the regular -classpath; see also 'Using Error Prone
together with other annotation processors' below. -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mug</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mug-guava</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mug-errorprone</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.truth</groupId>
<artifactId>truth</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.truth.extensions</groupId>
<artifactId>truth-java8-extension</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-testlib</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-bigquery</artifactId>
<version>2.34.2</version>
</dependency>
</dependencies>

</project>
143 changes: 143 additions & 0 deletions mug-bigquery/src/main/java/com/google/mu/bigquery/SafeBigQuery.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.google.mu.bigquery;

import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import com.google.cloud.bigquery.QueryJobConfiguration;
import com.google.cloud.bigquery.QueryParameterValue;
import com.google.errorprone.annotations.CompileTimeConstant;
import com.google.mu.util.StringFormat;
import com.google.mu.util.stream.BiStream;

/**
* Facade class to create templates of {@link QueryJobConfiguration} using the BigQuery
* parameterized query API to prevent SQL injection.
*
* <p>The string template syntax is defined by {@link StringFormat} and protected by the same
* compile-time checks.
*
* @since 7.1
*/
public final class SafeBigQuery {
private static final DateTimeFormatter TIMESTAMP_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSZZ");

/**
* Returns a template of {@iink QueryJobConfiguration} based on the {@code template} string.
*
* <p>For example: <pre>{@code
* private static final StringFormat.To<QueryJobConfiguration> GET_JOB_IDS_BY_QUERY =
* SafeBigQuery.template(
* """
* SELECT job_id from INFORMATION_SCHEMA.JOBS_BY_PROJECT
* WHERE configuration.query LIKE '%{keyword}%'
* """);
*
* QueryJobConfiguration query = GET_JOB_IDS_BY_QUERY.with("sensitive word");
* }</pre>
*
* <p>Except {@link TrustedSql} (which are directly substituted into the query,
* all other placeholder arguments are passed into the QueryJobConfiguration as query parameters.
*
* <p>Placeholder types supported:
* <ul>
* <li>CharSequence
* <li>Enum
* <li>java.time.Instant (translated to TIMESTAMP)
* <li>java.time.LocalDate (translated to DATE)
* <li>Integer
* <li>Long
* <li>BigDecimal
* <li>Double
* <li>Float
* </ul>
*
* If you need to supply other types, consider to wrap them explicitly using one of the static
* factory methods of {@link QueryParameterValue}.
*/
public static StringFormat.To<QueryJobConfiguration> template(
@CompileTimeConstant String template) {
return StringFormat.template(
template,
(fragments, placeholders) -> {
Iterator<String> it = fragments.iterator();
Set<String> paramNames = new HashSet<>();
BiStream.Builder<String, QueryParameterValue> parameters = BiStream.builder();
StringBuilder queryText = new StringBuilder();
placeholders.forEachOrdered(
(placeholder, value) -> {
queryText.append(it.next());
if (value == null) {
queryText.append("NULL");
} else if (value instanceof TrustedSql) {
queryText.append(value);
} else {
String paramName = placeholder.skip(1, 1).toString().trim();
if (!paramNames.add(paramName)) {
throw new IllegalArgumentException("Duplicate placeholder name " + placeholder);
}
queryText.append("@" + paramName);
parameters.add(paramName, toQueryParameter(value));
}
});
queryText.append(it.next());
return parameters
.build()
.collect(
QueryJobConfiguration.newBuilder(queryText.toString()),
QueryJobConfiguration.Builder::addNamedParameter)
.build();
});
}

private static QueryParameterValue toQueryParameter(Object value) {
if (value instanceof CharSequence) {
return QueryParameterValue.string(value.toString());
}
if (value instanceof Instant) {
Instant time = (Instant) value;
return QueryParameterValue.timestamp(
time.atZone(ZoneId.of("UTC")).format(TIMESTAMP_FORMATTER));
}
if (value instanceof LocalDate) {
return QueryParameterValue.date(((LocalDate) value).toString());
}
if (value instanceof Boolean) {
return QueryParameterValue.bool((Boolean) value);
}
if (value instanceof Integer) {
return QueryParameterValue.int64((Integer) value);
}
if (value instanceof Long) {
return QueryParameterValue.int64((Long) value);
}
if (value instanceof Double) {
return QueryParameterValue.float64((Double) value);
}
if (value instanceof Float) {
return QueryParameterValue.float64((Float) value);
}
if (value instanceof BigDecimal) {
return QueryParameterValue.bigNumeric((BigDecimal) value);
}
if (value instanceof byte[]) {
return QueryParameterValue.bytes((byte[]) value);
}
if (value instanceof QueryParameterValue) {
return (QueryParameterValue) value;
}
if (value instanceof Enum) {
return QueryParameterValue.string(((Enum<?>) value).name());
}
throw new IllegalArgumentException(
"Unsupported parameter type: "
+ value.getClass().getName()
+ ". Consider manually converting it to QueryParameterValue.");
}
}
50 changes: 50 additions & 0 deletions mug-bigquery/src/main/java/com/google/mu/bigquery/TrustedSql.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.google.mu.bigquery;

import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.mapping;

import java.util.stream.Collector;
import java.util.stream.Collectors;

import com.google.errorprone.annotations.CompileTimeConstant;

/**
* A SQL snippet that's trusted to be safe against injection.
*
* @since 7.1
*/
public final class TrustedSql {
private String sql;

private TrustedSql(String sql) {
this.sql = requireNonNull(sql);
}

public static TrustedSql of(@CompileTimeConstant String constant) {
return new TrustedSql(constant);
}

/** Returns a joiner that joins TrustedSql elements using {@code delim}. */
public static Collector<TrustedSql, ?, TrustedSql> joining(@CompileTimeConstant String delim) {
return collectingAndThen(
mapping(TrustedSql::toString, Collectors.joining(delim)),
TrustedSql::new);
}

@Override public boolean equals(Object obj) {
if (obj instanceof TrustedSql) {
TrustedSql that = (TrustedSql) obj;
return sql.equals(that.sql);
}
return false;
}

@Override public int hashCode() {
return sql.hashCode();
}

@Override public String toString() {
return sql;
}
}
Loading

0 comments on commit e99cff7

Please sign in to comment.