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: + * + *

+ * + * 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(); - } -}