From b024444e67cfe49b743c43c0d47a334b4cf734c1 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Thu, 12 Jul 2018 17:14:56 -0700 Subject: [PATCH 1/6] Avoid listing table data for destination of CREATE VIEW DDL queries. CREATE VIEW DDL queries set a "destination table" field, but that table is actually a view. If you attempt to list rows on the view table, it results in an error. The fix is to avoid listing rows altogether if getQueryResults() says that a query has completed but the number of rows in the result set is undefined. --- .../cloud/bigquery/EmptyTableResult.java | 49 ++++++++++++++ .../java/com/google/cloud/bigquery/Job.java | 23 ++++++- .../com/google/cloud/bigquery/JobStatus.java | 2 + .../bigquery/snippets/ITCloudSnippets.java | 64 +++++++++++++++++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java diff --git a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java new file mode 100644 index 000000000000..78d75c6a2fbb --- /dev/null +++ b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.bigquery; + +import com.google.api.core.InternalApi; +import com.google.api.gax.paging.Page; +import com.google.cloud.PageImpl; +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; + +import javax.annotation.Nullable; +import java.io.Serializable; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class EmptyTableResult extends TableResult { + + private static final long serialVersionUID = -4831062717210349819L; + + /** + * If {@code schema} is non-null, {@code TableResult} adds the schema to {@code FieldValueList}s + * when iterating through them. {@code pageNoSchema} must not be null. + */ + @InternalApi("Exposed for testing") + public EmptyTableResult() { + super(null, 0, new PageImpl( + null, + "", + null)); + } +} diff --git a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java index 9f85ffc73449..00bdd0aac384 100644 --- a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java +++ b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java @@ -35,6 +35,8 @@ import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; + +import com.google.common.collect.ImmutableList; import org.threeten.bp.Duration; /** @@ -285,10 +287,25 @@ public TableResult getQueryResults(QueryResultsOption... options) QueryResponse response = waitForQueryResults( DEFAULT_JOB_WAIT_SETTINGS, waitOptions.toArray(new QueryResultsOption[0])); - if (response.getSchema() == null) { - throw new JobException(getJobId(), response.getErrors()); + + // Get the job resource to determine if it has errored. + Job job = this; + if (!this.isDone()) { + job = reload(); + } + if (job.getStatus().getError() != null) { + throw new JobException( + getJobId(), + ImmutableList.copyOf(job.getStatus().getExecutionErrors())); } - + + // If there are no rows in the result, this may have been a DDL query. + // Listing table data might fail, such as with CREATE VIEW queries. + // Avoid a tabledata.list API request by returning an empty TableResult. + if (response.getTotalRows() == 0) { + return new EmptyTableResult(); + } + TableId table = ((QueryJobConfiguration) getConfiguration()).getDestinationTable(); return bigquery.listTableData( table, response.getSchema(), listOptions.toArray(new TableDataListOption[0])); diff --git a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobStatus.java b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobStatus.java index 24017499f190..12d6b3961574 100644 --- a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobStatus.java +++ b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobStatus.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +import javax.annotation.Nullable; import java.io.Serializable; import java.util.List; import java.util.Objects; @@ -130,6 +131,7 @@ public State getState() { * @see * Troubleshooting Errors */ + @Nullable public BigQueryError getError() { return error; } diff --git a/google-cloud-examples/src/test/java/com/google/cloud/examples/bigquery/snippets/ITCloudSnippets.java b/google-cloud-examples/src/test/java/com/google/cloud/examples/bigquery/snippets/ITCloudSnippets.java index 08dd9289ef43..79d4f9e6df37 100644 --- a/google-cloud-examples/src/test/java/com/google/cloud/examples/bigquery/snippets/ITCloudSnippets.java +++ b/google-cloud-examples/src/test/java/com/google/cloud/examples/bigquery/snippets/ITCloudSnippets.java @@ -16,16 +16,26 @@ package com.google.cloud.examples.bigquery.snippets; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import com.google.cloud.bigquery.BigQuery; import com.google.cloud.bigquery.BigQuery.DatasetDeleteOption; +import com.google.cloud.bigquery.BigQueryOptions; import com.google.cloud.bigquery.DatasetInfo; +import com.google.cloud.bigquery.FieldValueList; +import com.google.cloud.bigquery.Job; +import com.google.cloud.bigquery.JobInfo; +import com.google.cloud.bigquery.QueryJobConfiguration; +import com.google.cloud.bigquery.TableResult; import com.google.cloud.bigquery.testing.RemoteBigQueryHelper; import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; + +import com.google.common.collect.Lists; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -134,4 +144,58 @@ public void testUndeleteTable() throws InterruptedException { String got = bout.toString(); assertTrue(got.contains("DONE")); } + + @Test + public void testQueryDdlCreateView() throws InterruptedException { + String projectId = bigquery.getOptions().getProjectId(); + String datasetId = DATASET; + String tableId = "query_ddl_create_view"; + + // [START bigquery_ddl_create_view] + // import com.google.cloud.bigquery.*; + // String projectId = "my-project"; + // String datasetId = "my_dataset"; + // String tableId = "new_view"; + // BigQuery bigquery = BigQueryOptions.getDefaultInstance().toBuilder() + // .setProjectId(projectId) + // .build().getService(); + + String sql = String.format( + "CREATE VIEW `%s.%s.%s`\n" + + "OPTIONS(\n" + + " expiration_timestamp=TIMESTAMP_ADD(\n" + + " CURRENT_TIMESTAMP(), INTERVAL 48 HOUR),\n" + + " friendly_name=\"new_view\",\n" + + " description=\"a view that expires in 2 days\",\n" + + " labels=[(\"org_unit\", \"development\")]\n" + + ")\n" + + "AS SELECT name, state, year, number\n" + + " FROM `bigquery-public-data.usa_names.usa_1910_current`\n" + + " WHERE state LIKE 'W%%';\n", + projectId, datasetId, tableId); + + // Make an API request to run the query job. + Job job = bigquery.create( + JobInfo.of(QueryJobConfiguration.newBuilder(sql).build())); + + // Wait for the query to finish. + job = job.waitFor(); + + QueryJobConfiguration jobConfig = (QueryJobConfiguration) job.getConfiguration(); + System.out.printf( + "Created new view \"%s.%s.%s\".\n", + jobConfig.getDestinationTable().getProject(), + jobConfig.getDestinationTable().getDataset(), + jobConfig.getDestinationTable().getTable()); + // [END bigquery_ddl_create_view] + + String got = bout.toString(); + assertTrue(got.contains("Created new view ")); + + // Test that listing query result rows succeeds so that generic query + // processing tools work with DDL statements. + TableResult results = job.getQueryResults(); + List rows = Lists.newArrayList(results.iterateAll()); + assertEquals(rows.size(), 0); + } } From 3c6270becbf240601370297617c52015d8e0ee15 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 13 Jul 2018 15:22:23 -0700 Subject: [PATCH 2/6] Applied google-java-format --- .../cloud/bigquery/EmptyTableResult.java | 17 +----- .../java/com/google/cloud/bigquery/Job.java | 27 +++++---- .../com/google/cloud/bigquery/JobStatus.java | 56 +++++++------------ .../bigquery/snippets/ITCloudSnippets.java | 38 ++++++------- 4 files changed, 55 insertions(+), 83 deletions(-) diff --git a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java index 78d75c6a2fbb..827c11b7a1c3 100644 --- a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java +++ b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java @@ -17,19 +17,7 @@ package com.google.cloud.bigquery; import com.google.api.core.InternalApi; -import com.google.api.gax.paging.Page; import com.google.cloud.PageImpl; -import com.google.common.base.Function; -import com.google.common.base.MoreObjects; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; - -import javax.annotation.Nullable; -import java.io.Serializable; -import java.util.Objects; - -import static com.google.common.base.Preconditions.checkNotNull; public class EmptyTableResult extends TableResult { @@ -41,9 +29,6 @@ public class EmptyTableResult extends TableResult { */ @InternalApi("Exposed for testing") public EmptyTableResult() { - super(null, 0, new PageImpl( - null, - "", - null)); + super(null, 0, new PageImpl(null, "", null)); } } diff --git a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java index 00bdd0aac384..2145399ddde3 100644 --- a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java +++ b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java @@ -27,6 +27,7 @@ import com.google.cloud.bigquery.BigQuery.QueryResultsOption; import com.google.cloud.bigquery.BigQuery.TableDataListOption; import com.google.cloud.bigquery.JobConfiguration.Type; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.io.ObjectInputStream; import java.util.ArrayList; @@ -35,8 +36,6 @@ import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; - -import com.google.common.collect.ImmutableList; import org.threeten.bp.Duration; /** @@ -156,7 +155,8 @@ public Job build() { * Checks if this job exists. * *

Example of checking that a job exists. - *

 {@code
+   *
+   * 
{@code
    * if (!job.exists()) {
    *   // job doesn't exist
    * }
@@ -175,7 +175,8 @@ public boolean exists() {
    * not exist this method returns {@code true}.
    *
    * 

Example of waiting for a job until it reports that it is done. - *

 {@code
+   *
+   * 
{@code
    * while (!job.isDone()) {
    *   Thread.sleep(1000L);
    * }
@@ -198,7 +199,8 @@ public boolean isDone() {
    * 12 hours as a total timeout and unlimited number of attempts.
    *
    * 

Example usage of {@code waitFor()}. - *

 {@code
+   *
+   * 
{@code
    * Job completedJob = job.waitFor();
    * if (completedJob == null) {
    *   // job no longer exists
@@ -210,7 +212,8 @@ public boolean isDone() {
    * }
* *

Example usage of {@code waitFor()} with checking period and timeout. - *

 {@code
+   *
+   * 
{@code
    * Job completedJob =
    *     job.waitFor(
    *         RetryOption.initialRetryDelay(Duration.ofSeconds(1)),
@@ -295,8 +298,7 @@ public TableResult getQueryResults(QueryResultsOption... options)
     }
     if (job.getStatus().getError() != null) {
       throw new JobException(
-          getJobId(),
-          ImmutableList.copyOf(job.getStatus().getExecutionErrors()));
+          getJobId(), ImmutableList.copyOf(job.getStatus().getExecutionErrors()));
     }
 
     // If there are no rows in the result, this may have been a DDL query.
@@ -373,7 +375,8 @@ public boolean shouldRetry(Throwable prevThrowable, Job prevResponse) {
    * Fetches current job's latest information. Returns {@code null} if the job does not exist.
    *
    * 

Example of reloading all fields until job status is DONE. - *

 {@code
+   *
+   * 
{@code
    * while (job.getStatus().getState() != JobStatus.State.DONE) {
    *   Thread.sleep(1000L);
    *   job = job.reload();
@@ -381,7 +384,8 @@ public boolean shouldRetry(Throwable prevThrowable, Job prevResponse) {
    * }
* *

Example of reloading status field until job status is DONE. - *

 {@code
+   *
+   * 
{@code
    * while (job.getStatus().getState() != JobStatus.State.DONE) {
    *   Thread.sleep(1000L);
    *   job = job.reload(BigQuery.JobOption.fields(BigQuery.JobField.STATUS));
@@ -401,7 +405,8 @@ public Job reload(JobOption... options) {
    * Sends a job cancel request.
    *
    * 

Example of cancelling a job. - *

 {@code
+   *
+   * 
{@code
    * if (job.cancel()) {
    *   return true; // job successfully cancelled
    * } else {
diff --git a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobStatus.java b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobStatus.java
index 12d6b3961574..b09d00e852eb 100644
--- a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobStatus.java
+++ b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobStatus.java
@@ -22,11 +22,10 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-
-import javax.annotation.Nullable;
 import java.io.Serializable;
 import java.util.List;
 import java.util.Objects;
+import javax.annotation.Nullable;
 
 /**
  * A Google BigQuery Job status. Objects of this class can be examined when polling an asynchronous
@@ -36,9 +35,7 @@ public class JobStatus implements Serializable {
 
   private static final long serialVersionUID = -714976456815445365L;
 
-  /**
-   * Possible states that a BigQuery Job can assume.
-   */
+  /** Possible states that a BigQuery Job can assume. */
   public static final class State extends StringEnumValue {
     private static final long serialVersionUID = 818920627219751204L;
 
@@ -50,18 +47,12 @@ public State apply(String constant) {
           }
         };
 
-    private static final StringEnumType type = new StringEnumType(
-        State.class,
-        CONSTRUCTOR);
+    private static final StringEnumType type = new StringEnumType(State.class, CONSTRUCTOR);
 
-    /**
-     * The BigQuery Job is waiting to be executed.
-     */
+    /** The BigQuery Job is waiting to be executed. */
     public static final State PENDING = type.createAndRegister("PENDING");
 
-    /**
-     * The BigQuery Job is being executed.
-     */
+    /** The BigQuery Job is being executed. */
     public static final State RUNNING = type.createAndRegister("RUNNING");
 
     /**
@@ -75,23 +66,19 @@ private State(String constant) {
     }
 
     /**
-     * Get the State for the given String constant, and throw an exception if the constant is
-     * not recognized.
+     * Get the State for the given String constant, and throw an exception if the constant is not
+     * recognized.
      */
     public static State valueOfStrict(String constant) {
       return type.valueOfStrict(constant);
     }
 
-    /**
-     * Get the State for the given String constant, and allow unrecognized values.
-     */
+    /** Get the State for the given String constant, and allow unrecognized values. */
     public static State valueOf(String constant) {
       return type.valueOf(constant);
     }
 
-    /**
-     * Return the known values for State.
-     */
+    /** Return the known values for State. */
     public static State[] values() {
       return type.values();
     }
@@ -113,36 +100,33 @@ public static State[] values() {
     this.executionErrors = executionErrors != null ? ImmutableList.copyOf(executionErrors) : null;
   }
 
-
   /**
-   * Returns the state of the job. A {@link State#PENDING} job is waiting to be executed. A
-   * {@link State#RUNNING} is being executed. A {@link State#DONE} job has completed either
-   * succeeding or failing. If failed {@link #getError()} will be non-null.
+   * Returns the state of the job. A {@link State#PENDING} job is waiting to be executed. A {@link
+   * State#RUNNING} is being executed. A {@link State#DONE} job has completed either succeeding or
+   * failing. If failed {@link #getError()} will be non-null.
    */
   public State getState() {
     return state;
   }
 
-
   /**
-   * Returns the final error result of the job. If present, indicates that the job has completed
-   * and was unsuccessful.
+   * Returns the final error result of the job. If present, indicates that the job has completed and
+   * was unsuccessful.
    *
-   * @see 
-   *     Troubleshooting Errors
+   * @see Troubleshooting
+   *     Errors
    */
   @Nullable
   public BigQueryError getError() {
     return error;
   }
 
-
   /**
    * Returns all errors encountered during the running of the job. Errors here do not necessarily
    * mean that the job has completed or was unsuccessful.
    *
-   * @see 
-   *     Troubleshooting Errors
+   * @see Troubleshooting
+   *     Errors
    */
   public List getExecutionErrors() {
     return executionErrors;
@@ -166,8 +150,8 @@ public final int hashCode() {
   public final boolean equals(Object obj) {
     return obj == this
         || obj != null
-        && obj.getClass().equals(JobStatus.class)
-        && Objects.equals(toPb(), ((JobStatus) obj).toPb());
+            && obj.getClass().equals(JobStatus.class)
+            && Objects.equals(toPb(), ((JobStatus) obj).toPb());
   }
 
   com.google.api.services.bigquery.model.JobStatus toPb() {
diff --git a/google-cloud-examples/src/test/java/com/google/cloud/examples/bigquery/snippets/ITCloudSnippets.java b/google-cloud-examples/src/test/java/com/google/cloud/examples/bigquery/snippets/ITCloudSnippets.java
index 79d4f9e6df37..cc66677a7dba 100644
--- a/google-cloud-examples/src/test/java/com/google/cloud/examples/bigquery/snippets/ITCloudSnippets.java
+++ b/google-cloud-examples/src/test/java/com/google/cloud/examples/bigquery/snippets/ITCloudSnippets.java
@@ -21,7 +21,6 @@
 
 import com.google.cloud.bigquery.BigQuery;
 import com.google.cloud.bigquery.BigQuery.DatasetDeleteOption;
-import com.google.cloud.bigquery.BigQueryOptions;
 import com.google.cloud.bigquery.DatasetInfo;
 import com.google.cloud.bigquery.FieldValueList;
 import com.google.cloud.bigquery.Job;
@@ -29,13 +28,12 @@
 import com.google.cloud.bigquery.QueryJobConfiguration;
 import com.google.cloud.bigquery.TableResult;
 import com.google.cloud.bigquery.testing.RemoteBigQueryHelper;
+import com.google.common.collect.Lists;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
-
-import com.google.common.collect.Lists;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -160,33 +158,33 @@ public void testQueryDdlCreateView() throws InterruptedException {
     //     .setProjectId(projectId)
     //     .build().getService();
 
-    String sql = String.format(
+    String sql =
+        String.format(
             "CREATE VIEW `%s.%s.%s`\n"
-            + "OPTIONS(\n"
-            + "  expiration_timestamp=TIMESTAMP_ADD(\n"
-            + "    CURRENT_TIMESTAMP(), INTERVAL 48 HOUR),\n"
-            + "  friendly_name=\"new_view\",\n"
-            + "  description=\"a view that expires in 2 days\",\n"
-            + "  labels=[(\"org_unit\", \"development\")]\n"
-            + ")\n"
-            + "AS SELECT name, state, year, number\n"
-            + "  FROM `bigquery-public-data.usa_names.usa_1910_current`\n"
-            + "  WHERE state LIKE 'W%%';\n",
+                + "OPTIONS(\n"
+                + "  expiration_timestamp=TIMESTAMP_ADD(\n"
+                + "    CURRENT_TIMESTAMP(), INTERVAL 48 HOUR),\n"
+                + "  friendly_name=\"new_view\",\n"
+                + "  description=\"a view that expires in 2 days\",\n"
+                + "  labels=[(\"org_unit\", \"development\")]\n"
+                + ")\n"
+                + "AS SELECT name, state, year, number\n"
+                + "  FROM `bigquery-public-data.usa_names.usa_1910_current`\n"
+                + "  WHERE state LIKE 'W%%';\n",
             projectId, datasetId, tableId);
 
     // Make an API request to run the query job.
-    Job job = bigquery.create(
-            JobInfo.of(QueryJobConfiguration.newBuilder(sql).build()));
+    Job job = bigquery.create(JobInfo.of(QueryJobConfiguration.newBuilder(sql).build()));
 
     // Wait for the query to finish.
     job = job.waitFor();
 
     QueryJobConfiguration jobConfig = (QueryJobConfiguration) job.getConfiguration();
     System.out.printf(
-            "Created new view \"%s.%s.%s\".\n",
-            jobConfig.getDestinationTable().getProject(),
-            jobConfig.getDestinationTable().getDataset(),
-            jobConfig.getDestinationTable().getTable());
+        "Created new view \"%s.%s.%s\".\n",
+        jobConfig.getDestinationTable().getProject(),
+        jobConfig.getDestinationTable().getDataset(),
+        jobConfig.getDestinationTable().getTable());
     // [END bigquery_ddl_create_view]
 
     String got = bout.toString();

From c4da2116348d10158fbe5f96c4bc668552a47a90 Mon Sep 17 00:00:00 2001
From: Tim Swast 
Date: Fri, 13 Jul 2018 15:24:16 -0700
Subject: [PATCH 3/6] Correct EmptyTableResult javadoc

---
 .../main/java/com/google/cloud/bigquery/EmptyTableResult.java  | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java
index 827c11b7a1c3..1bda79caca04 100644
--- a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java
+++ b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java
@@ -24,8 +24,7 @@ public class EmptyTableResult extends TableResult {
   private static final long serialVersionUID = -4831062717210349819L;
 
   /**
-   * If {@code schema} is non-null, {@code TableResult} adds the schema to {@code FieldValueList}s
-   * when iterating through them. {@code pageNoSchema} must not be null.
+   * An empty {@code TableResult} to avoid making API requests to unlistable tables.
    */
   @InternalApi("Exposed for testing")
   public EmptyTableResult() {

From efb47478ed4fd8ffc93c5e2d3c6208e70a71004c Mon Sep 17 00:00:00 2001
From: Tim Swast 
Date: Fri, 13 Jul 2018 17:24:33 -0700
Subject: [PATCH 4/6] Fix BigQueryImpl unit tests.

---
 .../src/main/java/com/google/cloud/bigquery/Job.java       | 4 ++--
 .../java/com/google/cloud/bigquery/BigQueryImplTest.java   | 7 ++++++-
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java
index 2145399ddde3..0643c268b7e0 100644
--- a/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java
+++ b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Job.java
@@ -293,10 +293,10 @@ public TableResult getQueryResults(QueryResultsOption... options)
 
     // Get the job resource to determine if it has errored.
     Job job = this;
-    if (!this.isDone()) {
+    if (job.getStatus() == null || job.getStatus().getState() != JobStatus.State.DONE) {
       job = reload();
     }
-    if (job.getStatus().getError() != null) {
+    if (job.getStatus() != null && job.getStatus().getError() != null) {
       throw new JobException(
           getJobId(), ImmutableList.copyOf(job.getStatus().getExecutionErrors()));
     }
diff --git a/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java b/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java
index 58f0eb67e674..7fd91024c8eb 100644
--- a/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java
+++ b/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java
@@ -17,6 +17,8 @@
 package com.google.cloud.bigquery;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.anyString;
 import static org.easymock.EasyMock.capture;
 import static org.easymock.EasyMock.eq;
 import static org.junit.Assert.assertArrayEquals;
@@ -962,7 +964,7 @@ public JobId get() {
         .andThrow(new BigQueryException(409, "already exists, for some reason"));
     EasyMock.expect(
             bigqueryRpcMock.getJob(
-                EasyMock.anyString(),
+                anyString(),
                 EasyMock.eq(id),
                 EasyMock.eq((String) null),
                 EasyMock.eq(EMPTY_RPC_OPTIONS)))
@@ -1270,6 +1272,9 @@ public void testQueryRequestCompletedOnSecondAttempt() throws InterruptedExcepti
             bigqueryRpcMock.create(
                 JOB_INFO.toPb(), Collections.emptyMap()))
         .andReturn(jobResponsePb1);
+    EasyMock.expect(
+            bigqueryRpcMock.getJob(eq(PROJECT), eq(JOB), anyString(), anyObject(Map.class)))
+            .andReturn(jobResponsePb1);
 
     EasyMock.expect(
             bigqueryRpcMock.getQueryResults(

From d02231b96ad0b476348b36521d4094abd317a44c Mon Sep 17 00:00:00 2001
From: Tim Swast 
Date: Fri, 13 Jul 2018 17:42:42 -0700
Subject: [PATCH 5/6] Fix JobTest unit tests.

---
 .../com/google/cloud/bigquery/JobTest.java    | 94 +++++++++++++++++--
 1 file changed, 85 insertions(+), 9 deletions(-)

diff --git a/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/JobTest.java b/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/JobTest.java
index 5d8b2b13c34f..da68537118e4 100644
--- a/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/JobTest.java
+++ b/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/JobTest.java
@@ -18,8 +18,10 @@
 
 import static com.google.common.collect.ObjectArrays.concat;
 import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.anyObject;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
@@ -224,6 +226,82 @@ public void testWaitFor() throws InterruptedException {
     verify(status, mockOptions);
   }
 
+  @Test
+  public void testWaitForAndGetQueryResultsEmpty() throws InterruptedException {
+    QueryJobConfiguration jobConfig =
+            QueryJobConfiguration.newBuilder("CREATE VIEW").setDestinationTable(TABLE_ID1).build();
+    QueryStatistics jobStatistics =
+            QueryStatistics.newBuilder()
+                    .setCreationTimestamp(1L)
+                    .setEndTime(3L)
+                    .setStartTime(2L)
+                    .build();
+    JobInfo jobInfo =
+            JobInfo.newBuilder(jobConfig)
+                    .setJobId(JOB_ID)
+                    .setStatistics(jobStatistics)
+                    .setJobId(JOB_ID)
+                    .setEtag(ETAG)
+                    .setGeneratedId(GENERATED_ID)
+                    .setSelfLink(SELF_LINK)
+                    .setUserEmail(EMAIL)
+                    .setStatus(JOB_STATUS)
+                    .build();
+
+    initializeExpectedJob(2, jobInfo);
+    JobStatus status = createStrictMock(JobStatus.class);
+    expect(bigquery.getOptions()).andReturn(mockOptions);
+    expect(mockOptions.getClock()).andReturn(CurrentMillisClock.getDefaultClock()).times(2);
+    Job completedJob = expectedJob.toBuilder().setStatus(status).build();
+    // TODO(pongad): remove when we bump gax to 1.15.
+    Page emptyPage =
+            new Page() {
+              @Override
+              public boolean hasNextPage() {
+                return false;
+              }
+
+              @Override
+              public String getNextPageToken() {
+                return "";
+              }
+
+              @Override
+              public Page getNextPage() {
+                return null;
+              }
+
+              @Override
+              public Iterable iterateAll() {
+                return Collections.emptyList();
+              }
+
+              @Override
+              public Iterable getValues() {
+                return Collections.emptyList();
+              }
+            };
+    TableResult result = new TableResult(Schema.of(), 0, emptyPage);
+    QueryResponse completedQuery =
+            QueryResponse.newBuilder()
+                    .setCompleted(true)
+                    .setTotalRows(0)
+                    .setSchema(Schema.of())
+                    .setErrors(ImmutableList.of())
+                    .build();
+
+    expect(bigquery.getQueryResults(jobInfo.getJobId(), Job.DEFAULT_QUERY_WAIT_OPTIONS)).andReturn(completedQuery);
+    expect(bigquery.getJob(JOB_INFO.getJobId())).andReturn(completedJob);
+    expect(bigquery.getQueryResults(jobInfo.getJobId(), Job.DEFAULT_QUERY_WAIT_OPTIONS))
+            .andReturn(completedQuery);
+
+    replay(status, bigquery, mockOptions);
+    initializeJob(jobInfo);
+    assertThat(job.waitFor(TEST_RETRY_OPTIONS)).isSameAs(completedJob);
+    assertThat(job.getQueryResults().iterateAll()).isEmpty();
+    verify(status, mockOptions);
+  }
+
   @Test
   public void testWaitForAndGetQueryResults() throws InterruptedException {
     QueryJobConfiguration jobConfig =
@@ -252,7 +330,7 @@ public void testWaitForAndGetQueryResults() throws InterruptedException {
     expect(mockOptions.getClock()).andReturn(CurrentMillisClock.getDefaultClock()).times(2);
     Job completedJob = expectedJob.toBuilder().setStatus(status).build();
     // TODO(pongad): remove when we bump gax to 1.15.
-    Page emptyPage =
+    Page singlePage =
         new Page() {
           @Override
           public boolean hasNextPage() {
@@ -270,21 +348,19 @@ public Page getNextPage() {
           }
 
           @Override
-          public Iterable iterateAll() {
-            return Collections.emptyList();
-          }
+          public Iterable iterateAll() { return Collections.emptyList(); }
 
           @Override
           public Iterable getValues() {
             return Collections.emptyList();
           }
         };
-    TableResult result = new TableResult(Schema.of(), 0, emptyPage);
+    TableResult result = new TableResult(Schema.of(), 1, singlePage);
     QueryResponse completedQuery =
         QueryResponse.newBuilder()
             .setCompleted(true)
-            .setTotalRows(0)
-            .setSchema(Schema.of())
+            .setTotalRows(1)  // Lies to force call of listTableData().
+            .setSchema(Schema.of(Field.of("_f0", LegacySQLTypeName.INTEGER)))
             .setErrors(ImmutableList.of())
             .build();
 
@@ -292,12 +368,12 @@ public Iterable getValues() {
     expect(bigquery.getJob(JOB_INFO.getJobId())).andReturn(completedJob);
     expect(bigquery.getQueryResults(jobInfo.getJobId(), Job.DEFAULT_QUERY_WAIT_OPTIONS))
         .andReturn(completedQuery);
-    expect(bigquery.listTableData(TABLE_ID1, Schema.of())).andReturn(result);
+    expect(bigquery.listTableData(eq(TABLE_ID1), anyObject(Schema.class))).andReturn(result);
 
     replay(status, bigquery, mockOptions);
     initializeJob(jobInfo);
     assertThat(job.waitFor(TEST_RETRY_OPTIONS)).isSameAs(completedJob);
-    assertThat(job.getQueryResults().iterateAll()).isEmpty();
+    assertThat(job.getQueryResults().iterateAll()).hasSize(0);
     verify(status, mockOptions);
   }
 

From bffb0eb227863f94b9942f3618e8c33608113689 Mon Sep 17 00:00:00 2001
From: Tim Swast 
Date: Mon, 16 Jul 2018 08:44:08 -0700
Subject: [PATCH 6/6] Remove unused TableResult.

---
 .../src/test/java/com/google/cloud/bigquery/JobTest.java         | 1 -
 1 file changed, 1 deletion(-)

diff --git a/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/JobTest.java b/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/JobTest.java
index da68537118e4..5a3d465e9db3 100644
--- a/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/JobTest.java
+++ b/google-cloud-clients/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/JobTest.java
@@ -281,7 +281,6 @@ public Iterable getValues() {
                 return Collections.emptyList();
               }
             };
-    TableResult result = new TableResult(Schema.of(), 0, emptyPage);
     QueryResponse completedQuery =
             QueryResponse.newBuilder()
                     .setCompleted(true)