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..1bda79caca04 --- /dev/null +++ b/google-cloud-clients/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/EmptyTableResult.java @@ -0,0 +1,33 @@ +/* + * 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.cloud.PageImpl; + +public class EmptyTableResult extends TableResult { + + private static final long serialVersionUID = -4831062717210349819L; + + /** + * An empty {@code TableResult} to avoid making API requests to unlistable tables. + */ + @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..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 @@ -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; @@ -154,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
    * }
@@ -173,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);
    * }
@@ -196,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
@@ -208,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)),
@@ -285,10 +290,24 @@ 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 (job.getStatus() == null || job.getStatus().getState() != JobStatus.State.DONE) {
+      job = reload();
+    }
+    if (job.getStatus() != null && 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]));
@@ -356,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();
@@ -364,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));
@@ -384,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 24017499f190..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,10 +22,10 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-
 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
@@ -35,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;
 
@@ -49,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");
 
     /**
@@ -74,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();
     }
@@ -112,35 +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;
@@ -164,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-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(
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..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
@@ -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,81 @@ 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();
+              }
+            };
+    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 +329,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 +347,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 +367,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);
   }
 
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..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
@@ -16,14 +16,22 @@
 
 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.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 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 org.junit.AfterClass;
@@ -134,4 +142,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);
+  }
 }