From 9d154319af4ce09cfa15970edff656676627b034 Mon Sep 17 00:00:00 2001
From: xingyutangyuan <147447743+xingyutangyuan@users.noreply.github.com>
Date: Sun, 3 Dec 2023 08:35:52 -0800
Subject: [PATCH] Renamed SafeBigQuery to ParameterizedQuery and delete
TrustedSql
---
.../mu/bigquery/ParameterizedQuery.java | 261 ++++++++++++++++++
.../com/google/mu/bigquery/SafeBigQuery.java | 143 ----------
.../com/google/mu/bigquery/TrustedSql.java | 50 ----
.../mu/bigquery/ParameterizedQueryTest.java | 238 ++++++++++++++++
.../google/mu/bigquery/SafeBigQueryTest.java | 179 ------------
.../google/mu/bigquery/TrustedSqlTest.java | 25 --
6 files changed, 499 insertions(+), 397 deletions(-)
create mode 100644 mug-bigquery/src/main/java/com/google/mu/bigquery/ParameterizedQuery.java
delete mode 100644 mug-bigquery/src/main/java/com/google/mu/bigquery/SafeBigQuery.java
delete mode 100644 mug-bigquery/src/main/java/com/google/mu/bigquery/TrustedSql.java
create mode 100644 mug-bigquery/src/test/java/com/google/mu/bigquery/ParameterizedQueryTest.java
delete mode 100644 mug-bigquery/src/test/java/com/google/mu/bigquery/SafeBigQueryTest.java
delete mode 100644 mug-bigquery/src/test/java/com/google/mu/bigquery/TrustedSqlTest.java
diff --git a/mug-bigquery/src/main/java/com/google/mu/bigquery/ParameterizedQuery.java b/mug-bigquery/src/main/java/com/google/mu/bigquery/ParameterizedQuery.java
new file mode 100644
index 0000000000..df2be805cf
--- /dev/null
+++ b/mug-bigquery/src/main/java/com/google/mu/bigquery/ParameterizedQuery.java
@@ -0,0 +1,261 @@
+package com.google.mu.bigquery;
+
+import static java.util.Objects.requireNonNull;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collector;
+
+import com.google.cloud.bigquery.BigQueryOptions;
+import com.google.cloud.bigquery.JobException;
+import com.google.cloud.bigquery.QueryJobConfiguration;
+import com.google.cloud.bigquery.QueryParameterValue;
+import com.google.cloud.bigquery.TableResult;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.CompileTimeConstant;
+import com.google.errorprone.annotations.Immutable;
+import com.google.mu.util.StringFormat;
+import com.google.mu.util.stream.BiStream;
+
+/**
+ * Facade class to create BigQuery parameterized queries using a template string and parameters.
+ *
+ *
The string template syntax is defined by {@link StringFormat} and protected by the same
+ * compile-time checks.
+ *
+ * @since 7.1
+ */
+@Immutable
+public final class ParameterizedQuery {
+ private static final DateTimeFormatter TIMESTAMP_FORMATTER =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSZZ");
+ private final String query;
+
+ @SuppressWarnings("Immutable")
+ private final Map parameters;
+
+ private ParameterizedQuery(String query, Map parameters) {
+ this.query = requireNonNull(query);
+ this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(parameters));
+ }
+
+ /**
+ * Convenience method when you need to create the {@link ParameterizedQuery} inline, with
+ * both the query template and the arguments.
+ *
+ * For example:
+ *
+ *
{@code
+ * TableResult result = ParameterizedQuery.of("select * from JOBS where id = {id}", jobId).run();
+ * }
+ */
+ @SuppressWarnings("StringFormatArgsCheck") // Called immediately, runtime error is good enough.
+ public static ParameterizedQuery of(@CompileTimeConstant String query, Object... args) {
+ return template(query).with(args);
+ }
+
+ /**
+ * Returns a template of {@iink QueryJobConfiguration} based on the {@code template} string.
+ *
+ * For example:
+ *
+ *
{@code
+ * private static final StringFormat.To GET_JOB_IDS_BY_QUERY =
+ * ParameterizedQuery.template(
+ * """
+ * SELECT job_id from INFORMATION_SCHEMA.JOBS_BY_PROJECT
+ * WHERE configuration.query LIKE '%{keyword}%'
+ * """);
+ *
+ * TableResult result = GET_JOB_IDS_BY_QUERY.with("sensitive word").run();
+ * }
+ *
+ * Except {@link ParameterizedQuery} itself, which are directly substituted into the query, all
+ * other placeholder arguments are passed into the QueryJobConfiguration as query parameters.
+ *
+ *
Placeholder types supported:
+ *
+ *
+ * - CharSequence
+ *
- Enum
+ *
- java.time.Instant (translated to TIMESTAMP)
+ *
- java.time.LocalDate (translated to DATE)
+ *
- Integer
+ *
- Long
+ *
- BigDecimal
+ *
- Double
+ *
- Float
+ *
+ *
+ * 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 template(@CompileTimeConstant String template) {
+ return StringFormat.template(
+ template,
+ (fragments, placeholders) -> {
+ Iterator it = fragments.iterator();
+ return placeholders
+ .collect(
+ new Builder(),
+ (builder, placeholder, value) -> {
+ builder.append(it.next());
+ if (value == null) {
+ builder.append("NULL");
+ } else if (value instanceof ParameterizedQuery) {
+ builder.addSubQuery((ParameterizedQuery) value);
+ } else {
+ String paramName = placeholder.skip(1, 1).toString().trim();
+ builder.append("@" + paramName);
+ builder.addParameter(paramName, toQueryParameter(value));
+ }
+ })
+ .append(it.next())
+ .build();
+ });
+ }
+
+ /** Returns a joiner that joins ParameterizedQuery elements using {@code delim}. */
+ public static Collector joining(
+ @CompileTimeConstant String delim) {
+ return Collector.of(
+ Builder::new,
+ (b, q) -> b.appendDelimiter(delim).addSubQuery(q),
+ (b1, b2) -> b1.appendDelimiter(delim).addSubQuery(b2.build()),
+ Builder::build);
+ }
+
+ /**
+ * Sends this query to BigQuery using the default options.
+ *
+ * To use alternative options, pass {@link #jobConfiguration} to the {link BigQueryOptions}
+ * of your choice.
+ */
+ public TableResult run() throws JobException, InterruptedException {
+ return BigQueryOptions.getDefaultInstance().getService().query(jobConfiguration());
+ }
+
+ /** Returns the {@link QueryJobConfiguration} that can be sent to BigQuery. */
+ public QueryJobConfiguration jobConfiguration() {
+ return BiStream.from(parameters)
+ .collect(
+ QueryJobConfiguration.newBuilder(query),
+ QueryJobConfiguration.Builder::addNamedParameter)
+ .build();
+ }
+
+ private static final class Builder {
+ private final StringBuilder queryText = new StringBuilder();
+ private final LinkedHashMap parameters = new LinkedHashMap<>();
+
+ @CanIgnoreReturnValue
+ Builder append(String snippet) {
+ queryText.append(snippet);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder appendDelimiter(String delim) {
+ if (queryText.length() > 0) {
+ queryText.append(delim);
+ }
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder addParameter(String name, QueryParameterValue value) {
+ if (parameters.put(name, value) != null) {
+ throw new IllegalArgumentException("Duplicate placeholder name " + name);
+ }
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder addSubQuery(ParameterizedQuery subQuery) {
+ queryText.append(subQuery.query);
+ BiStream.from(subQuery.parameters).forEachOrdered(this::addParameter);
+ return this;
+ }
+
+ ParameterizedQuery build() {
+ return new ParameterizedQuery(queryText.toString(), parameters);
+ }
+ }
+
+ 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());
+ }
+ if (value.getClass().isArray()) {
+ @SuppressWarnings("rawtypes")
+ Class componentType = value.getClass().getComponentType();
+ return QueryParameterValue.array((Object[]) value, componentType);
+ }
+ throw new IllegalArgumentException(
+ "Unsupported parameter type: "
+ + value.getClass().getName()
+ + ". Consider manually converting it to QueryParameterValue.");
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(query, parameters);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ParameterizedQuery) {
+ ParameterizedQuery that = (ParameterizedQuery) obj;
+ return query.equals(that.query) && parameters.equals(that.parameters);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return query;
+ }
+}
diff --git a/mug-bigquery/src/main/java/com/google/mu/bigquery/SafeBigQuery.java b/mug-bigquery/src/main/java/com/google/mu/bigquery/SafeBigQuery.java
deleted file mode 100644
index f05f3b64ae..0000000000
--- a/mug-bigquery/src/main/java/com/google/mu/bigquery/SafeBigQuery.java
+++ /dev/null
@@ -1,143 +0,0 @@
-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.
- *
- * 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.
- *
- *
For example:
{@code
- * private static final StringFormat.To 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");
- * }
- *
- * Except {@link TrustedSql}, which are directly substituted into the query,
- * all other placeholder arguments are passed into the QueryJobConfiguration as query parameters.
- *
- *
Placeholder types supported:
- *
- * - CharSequence
- *
- Enum
- *
- java.time.Instant (translated to TIMESTAMP)
- *
- java.time.LocalDate (translated to DATE)
- *
- Integer
- *
- Long
- *
- BigDecimal
- *
- Double
- *
- Float
- *
- *
- * 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 template(
- @CompileTimeConstant String template) {
- return StringFormat.template(
- template,
- (fragments, placeholders) -> {
- Iterator it = fragments.iterator();
- Set paramNames = new HashSet<>();
- BiStream.Builder 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.");
- }
-}
diff --git a/mug-bigquery/src/main/java/com/google/mu/bigquery/TrustedSql.java b/mug-bigquery/src/main/java/com/google/mu/bigquery/TrustedSql.java
deleted file mode 100644
index 5b16438efb..0000000000
--- a/mug-bigquery/src/main/java/com/google/mu/bigquery/TrustedSql.java
+++ /dev/null
@@ -1,50 +0,0 @@
-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 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;
- }
-}
diff --git a/mug-bigquery/src/test/java/com/google/mu/bigquery/ParameterizedQueryTest.java b/mug-bigquery/src/test/java/com/google/mu/bigquery/ParameterizedQueryTest.java
new file mode 100644
index 0000000000..943917e750
--- /dev/null
+++ b/mug-bigquery/src/test/java/com/google/mu/bigquery/ParameterizedQueryTest.java
@@ -0,0 +1,238 @@
+package com.google.mu.bigquery;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.mu.bigquery.ParameterizedQuery.template;
+import static java.util.Arrays.asList;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.stream.Stream;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.google.cloud.bigquery.QueryJobConfiguration;
+import com.google.cloud.bigquery.QueryParameterValue;
+import com.google.common.testing.EqualsTester;
+import com.google.mu.util.StringFormat;
+
+@RunWith(JUnit4.class)
+public class ParameterizedQueryTest {
+ @Test
+ public void template_noArg() {
+ assertThat(ParameterizedQuery.of("SELECT *").jobConfiguration())
+ .isEqualTo(QueryJobConfiguration.of("SELECT *"));
+ }
+
+ @Test
+ public void template_trustedArg() {
+ assertThat(template("SELECT * FROM {tbl}").with(/* tbl */ ParameterizedQuery.of("Jobs")).jobConfiguration())
+ .isEqualTo(QueryJobConfiguration.of("SELECT * FROM Jobs"));
+ }
+
+ @Test
+ public void template_nullArg() {
+ StringFormat.To query = template("SELECT {expr} where {cond}");
+ assertThat(query.with(/* expr */ null, /* cond */ true).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT NULL where @cond")
+ .addNamedParameter("cond", QueryParameterValue.bool(true))
+ .build());
+ }
+
+ @Test
+ public void template_boolArg() {
+ StringFormat.To query = template("SELECT {expr}");
+ assertThat(query.with(/* expr */ false).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT @expr")
+ .addNamedParameter("expr", QueryParameterValue.bool(false))
+ .build());
+ }
+
+ @Test
+ public void template_stringArg() {
+ StringFormat.To query = template("SELECT * where message like '%{id}%'");
+ assertThat(query.with("1's id").jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT * where message like '%@id%'")
+ .addNamedParameter("id", QueryParameterValue.string("1's id"))
+ .build());
+ }
+
+ @Test
+ public void template_instantArg() {
+ StringFormat.To query = template("SELECT * where timestamp = {now};");
+ ZonedDateTime now =
+ ZonedDateTime.of(2023, 10, 1, 8, 30, 0, 90000, ZoneId.of("America/Los_Angeles"));
+ assertThat(query.with(now.toInstant()).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT * where timestamp = @now;")
+ .addNamedParameter(
+ "now", QueryParameterValue.timestamp("2023-10-01 15:30:00.000090+0000"))
+ .build());
+ }
+
+ @Test
+ public void template_localDateArg() {
+ StringFormat.To query = template("SELECT * where date = {date};");
+ assertThat(query.with(LocalDate.of(2023, 12, 1)).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT * where date = @date;")
+ .addNamedParameter("date", QueryParameterValue.date("2023-12-01"))
+ .build());
+ }
+
+ @Test
+ public void template_queryParameterValueArg() {
+ StringFormat.To query = template("SELECT {param}");
+ QueryParameterValue param = QueryParameterValue.array(new Integer[] {1, 2}, Integer.class);
+ assertThat(query.with(param).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT @param")
+ .addNamedParameter("param", param)
+ .build());
+ }
+
+ @Test
+ public void template_intArg() {
+ StringFormat.To query = template("SELECT {expr}");
+ assertThat(query.with(/* expr */ 1).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT @expr")
+ .addNamedParameter("expr", QueryParameterValue.int64(1))
+ .build());
+ }
+
+ @Test
+ public void template_longArg() {
+ StringFormat.To query = template("SELECT {expr}");
+ assertThat(query.with(/* expr */ 1L).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT @expr")
+ .addNamedParameter("expr", QueryParameterValue.int64(1L))
+ .build());
+ }
+
+ @Test
+ public void template_floatArg() {
+ StringFormat.To query = template("SELECT {expr}");
+ assertThat(query.with(/* expr */ 1.0F).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT @expr")
+ .addNamedParameter("expr", QueryParameterValue.float64(1.0F))
+ .build());
+ }
+
+ @Test
+ public void template_doubleArg() {
+ StringFormat.To query = template("SELECT {expr}");
+ assertThat(query.with(/* expr */ 1.0D).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT @expr")
+ .addNamedParameter("expr", QueryParameterValue.float64(1.0D))
+ .build());
+ }
+
+ @Test
+ public void template_bigDecimalArg() {
+ StringFormat.To query = template("SELECT {expr}");
+ assertThat(query.with(/* expr */ new BigDecimal("1.23456")).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT @expr")
+ .addNamedParameter(
+ "expr", QueryParameterValue.bigNumeric(new BigDecimal("1.23456")))
+ .build());
+ }
+
+ @Test
+ public void template_enumArg() {
+ StringFormat.To query = template("SELECT * WHERE status = {status}");
+ assertThat(query.with(Status.ACTIVE).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT * WHERE status = @status")
+ .addNamedParameter("status", QueryParameterValue.string("ACTIVE"))
+ .build());
+ }
+
+ @Test
+ public void template_stringArrayArg() {
+ StringFormat.To query = template("SELECT {names}");
+ assertThat(query.with(/* names */ (Object) new String[] {"foo", "bar"}).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT @names")
+ .addNamedParameter(
+ "names", QueryParameterValue.array(new String[] {"foo", "bar"}, String.class))
+ .build());
+ }
+
+ @Test
+ public void template_integerArrayArg() {
+ StringFormat.To query = template("SELECT {ids}");
+ assertThat(query.with(/* ids */ (Object) new Integer[] {1, 2}).jobConfiguration())
+ .isEqualTo(
+ QueryJobConfiguration.newBuilder("SELECT @ids")
+ .addNamedParameter(
+ "ids", QueryParameterValue.array(new Integer[] {1, 2}, Integer.class))
+ .build());
+ }
+
+ @Test
+ public void template_duplicatePlaceholderNameThrows() {
+ StringFormat.To query =
+ template("SELECT * WHERE status in ({status}, {status}");
+ IllegalArgumentException thrown =
+ assertThrows(
+ IllegalArgumentException.class, () -> query.with(Status.ACTIVE, Status.INACTIVE));
+ assertThat(thrown).hasMessageThat().contains("status");
+ }
+
+ @Test
+ public void template_iterableArgNotSupported() {
+ StringFormat.To query = template("SELECT * WHERE status = {status}");
+ assertThrows(IllegalArgumentException.class, () -> query.with(asList(Status.ACTIVE)));
+ }
+
+ @Test public void testJoining_noParameters() {
+ assertThat(
+ Stream.of(ParameterizedQuery.of("a"), ParameterizedQuery.of("b"))
+ .collect(ParameterizedQuery.joining(", ")))
+ .isEqualTo(ParameterizedQuery.of("a, b"));
+ }
+
+ @Test public void testJoining_withParameters() {
+ assertThat(
+ Stream.of(ParameterizedQuery.of("{v1}", 1), ParameterizedQuery.of("{v2}", "2"))
+ .collect(ParameterizedQuery.joining(", ")))
+ .isEqualTo(ParameterizedQuery.of("{v1}, {v2}", 1, "2"));
+ }
+
+ @Test public void testJoining_parallel() {
+ assertThat(
+ Stream.of(ParameterizedQuery.of("{v1}", 1), ParameterizedQuery.of("{v2}", "2"), ParameterizedQuery.of("{v3}", 3))
+ .parallel()
+ .collect(ParameterizedQuery.joining(", ")))
+ .isEqualTo(ParameterizedQuery.of("{v1}, {v2}, {v3}", 1, "2", 3));
+ }
+
+ @Test public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(ParameterizedQuery.of("foo"))
+ .addEqualityGroup(ParameterizedQuery.of("bar"))
+ .addEqualityGroup(
+ ParameterizedQuery.of("select {col1}, {col2}", "a", "b"),
+ ParameterizedQuery.of("select {col1}, {col2}", "a", "b"))
+ .addEqualityGroup(
+ ParameterizedQuery.of("select {col1}, {col2}", "b", "a"))
+ .testEquals();
+ }
+
+ private enum Status {
+ ACTIVE,
+ INACTIVE
+ }
+}
diff --git a/mug-bigquery/src/test/java/com/google/mu/bigquery/SafeBigQueryTest.java b/mug-bigquery/src/test/java/com/google/mu/bigquery/SafeBigQueryTest.java
deleted file mode 100644
index 2814d97f01..0000000000
--- a/mug-bigquery/src/test/java/com/google/mu/bigquery/SafeBigQueryTest.java
+++ /dev/null
@@ -1,179 +0,0 @@
-package com.google.mu.bigquery;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.mu.bigquery.SafeBigQuery.template;
-import static java.util.Arrays.asList;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import com.google.cloud.bigquery.QueryJobConfiguration;
-import com.google.cloud.bigquery.QueryParameterValue;
-import com.google.mu.util.StringFormat;
-
-@RunWith(JUnit4.class)
-public class SafeBigQueryTest {
- @Test
- public void template_noArg() {
- assertThat(template("SELECT *").with()).isEqualTo(QueryJobConfiguration.of("SELECT *"));
- }
-
- @Test
- public void template_trustedArg() {
- assertThat(template("SELECT * FROM {tbl}").with(/* tbl */ TrustedSql.of("Jobs")))
- .isEqualTo(QueryJobConfiguration.of("SELECT * FROM Jobs"));
- }
-
- @Test
- public void template_nullArg() {
- StringFormat.To query = template("SELECT {expr} where {cond}");
- assertThat(query.with(/* expr */ null, /* cond */ true))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT NULL where @cond")
- .addNamedParameter("cond", QueryParameterValue.bool(true))
- .build());
- }
-
- @Test
- public void template_boolArg() {
- StringFormat.To query = template("SELECT {expr}");
- assertThat(query.with(/* expr */ false))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT @expr")
- .addNamedParameter("expr", QueryParameterValue.bool(false))
- .build());
- }
-
- @Test
- public void template_stringArg() {
- StringFormat.To query = template("SELECT * where message like '%{id}%'");
- assertThat(query.with("1's id"))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT * where message like '%@id%'")
- .addNamedParameter("id", QueryParameterValue.string("1's id"))
- .build());
- }
-
- @Test
- public void template_instantArg() {
- StringFormat.To query = template("SELECT * where timestamp = {now};");
- ZonedDateTime now =
- ZonedDateTime.of(2023, 10, 1, 8, 30, 0, 90000, ZoneId.of("America/Los_Angeles"));
- assertThat(query.with(now.toInstant()))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT * where timestamp = @now;")
- .addNamedParameter(
- "now", QueryParameterValue.timestamp("2023-10-01 15:30:00.000090+0000"))
- .build());
- }
-
- @Test
- public void template_localDateArg() {
- StringFormat.To query = template("SELECT * where date = {date};");
- assertThat(query.with(LocalDate.of(2023, 12, 1)))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT * where date = @date;")
- .addNamedParameter("date", QueryParameterValue.date("2023-12-01"))
- .build());
- }
-
- @Test
- public void template_queryParameterValueArg() {
- StringFormat.To query = template("SELECT {param}");
- QueryParameterValue param = QueryParameterValue.array(new Integer[] {1, 2}, Integer.class);
- assertThat(query.with(param))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT @param")
- .addNamedParameter("param", param)
- .build());
- }
-
- @Test
- public void template_intArg() {
- StringFormat.To query = template("SELECT {expr}");
- assertThat(query.with(/* expr */ 1))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT @expr")
- .addNamedParameter("expr", QueryParameterValue.int64(1))
- .build());
- }
-
- @Test
- public void template_longArg() {
- StringFormat.To query = template("SELECT {expr}");
- assertThat(query.with(/* expr */ 1L))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT @expr")
- .addNamedParameter("expr", QueryParameterValue.int64(1L))
- .build());
- }
-
- @Test
- public void template_floatArg() {
- StringFormat.To query = template("SELECT {expr}");
- assertThat(query.with(/* expr */ 1.0F))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT @expr")
- .addNamedParameter("expr", QueryParameterValue.float64(1.0F))
- .build());
- }
-
- @Test
- public void template_doubleArg() {
- StringFormat.To query = template("SELECT {expr}");
- assertThat(query.with(/* expr */ 1.0D))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT @expr")
- .addNamedParameter("expr", QueryParameterValue.float64(1.0D))
- .build());
- }
-
- @Test
- public void template_bigDecimalArg() {
- StringFormat.To query = template("SELECT {expr}");
- assertThat(query.with(/* expr */ new BigDecimal("1.23456")))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT @expr")
- .addNamedParameter(
- "expr", QueryParameterValue.bigNumeric(new BigDecimal("1.23456")))
- .build());
- }
-
- @Test
- public void template_enumArg() {
- StringFormat.To query = template("SELECT * WHERE status = {status}");
- assertThat(query.with(Status.ACTIVE))
- .isEqualTo(
- QueryJobConfiguration.newBuilder("SELECT * WHERE status = @status")
- .addNamedParameter("status", QueryParameterValue.string("ACTIVE"))
- .build());
- }
-
- @Test
- public void template_duplicatePlaceholderNameThrows() {
- StringFormat.To query =
- template("SELECT * WHERE status in ({status}, {status}");
- IllegalArgumentException thrown =
- assertThrows(
- IllegalArgumentException.class, () -> query.with(Status.ACTIVE, Status.INACTIVE));
- assertThat(thrown).hasMessageThat().contains("{status}");
- }
-
- @Test
- public void template_iterableArgNotSupported() {
- StringFormat.To query = template("SELECT * WHERE status = {status}");
- assertThrows(IllegalArgumentException.class, () -> query.with(asList(Status.ACTIVE)));
- }
-
- private enum Status {
- ACTIVE,
- INACTIVE
- }
-}
diff --git a/mug-bigquery/src/test/java/com/google/mu/bigquery/TrustedSqlTest.java b/mug-bigquery/src/test/java/com/google/mu/bigquery/TrustedSqlTest.java
deleted file mode 100644
index dea2bab113..0000000000
--- a/mug-bigquery/src/test/java/com/google/mu/bigquery/TrustedSqlTest.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.google.mu.bigquery;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import java.util.stream.Stream;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import com.google.common.testing.EqualsTester;
-
-@RunWith(JUnit4.class)
-public class TrustedSqlTest {
- @Test public void testJoining() {
- assertThat(Stream.of(TrustedSql.of("a"), TrustedSql.of("b")).collect(TrustedSql.joining(", ")))
- .isEqualTo(TrustedSql.of("a, b"));
- }
- @Test public void testEquals() {
- new EqualsTester()
- .addEqualityGroup(TrustedSql.of("foo"), TrustedSql.of("foo"))
- .addEqualityGroup(TrustedSql.of("bar"))
- .testEquals();
- }
-}