Repository: googleapis/google-cloud-go
Pull request #5048 (2021-10-29 18:14:45)

feat(bigquery): allow construction of jobs from other projects
Adds a `JobfromProject()` method to allow users to get metadata from a
job that was created within another project.

Fixes: https://github.com/googleapis/google-cloud-go/issues/4228

shollyman: > Design LGTM. Are there other places that use c.projectID that need to be updated? Might be good to see a unit test or two that verifies when the job project != client project sets the expect HTTP path.

Other callsites that reference the client's projectID directly is job insertion, either the standard jobs.insert or the optimized jobs.query path.  There's other things like dataset iterator Datasets(), but under the hood it calls DatasetsInProject() which allows remote enumeration.
shollyman: Getting this to a unit test is surprisingly annoying given the lack of intermediate interfaces, so I wrote a small integration test for now.

Patch:
diff --git a/bigquery/job.go b/bigquery/job.go
index 6a2809b5d7..f1978eccd7 100644
--- a/bigquery/job.go
+++ b/bigquery/job.go
@@ -46,17 +46,24 @@ type Job struct {
 // For jobs whose location is other than "US" or "EU", set Client.Location or use
 // JobFromIDLocation.
 func (c *Client) JobFromID(ctx context.Context, id string) (*Job, error) {
-	return c.JobFromIDLocation(ctx, id, c.Location)
+	return c.JobFromProject(ctx, c.projectID, id, c.Location)
 }
 
 // JobFromIDLocation creates a Job which refers to an existing BigQuery job. The job
 // need not have been created by this package (for example, it may have
 // been created in the BigQuery console), but it must exist in the specified location.
 func (c *Client) JobFromIDLocation(ctx context.Context, id, location string) (j *Job, err error) {
-	ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.JobFromIDLocation")
+	return c.JobFromProject(ctx, c.projectID, id, location)
+}
+
+// JobFromProject creates a Job which refers to an existing BigQuery job.  The job
+// need not have been created by this package, nor does it need to reside within the same
+// project or location as the instantiated client.
+func (c *Client) JobFromProject(ctx context.Context, projectID, jobID, location string) (j *Job, err error) {
+	ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.JobFromProject")
 	defer func() { trace.EndSpan(ctx, err) }()
 
-	bqjob, err := c.getJobInternal(ctx, id, location, "configuration", "jobReference", "status", "statistics")
+	bqjob, err := c.getJobInternal(ctx, jobID, location, projectID, "configuration", "jobReference", "status", "statistics")
 	if err != nil {
 		return nil, err
 	}
@@ -799,9 +806,13 @@ func convertListedJob(j *bq.JobListJobs, c *Client) (*Job, error) {
 	return bqToJob2(j.JobReference, j.Configuration, j.Status, j.Statistics, j.UserEmail, c)
 }
 
-func (c *Client) getJobInternal(ctx context.Context, jobID, location string, fields ...googleapi.Field) (*bq.Job, error) {
+func (c *Client) getJobInternal(ctx context.Context, jobID, location, projectID string, fields ...googleapi.Field) (*bq.Job, error) {
 	var job *bq.Job
-	call := c.bqs.Jobs.Get(c.projectID, jobID).Context(ctx)
+	proj := projectID
+	if proj == "" {
+		proj = c.projectID
+	}
+	call := c.bqs.Jobs.Get(proj, jobID).Context(ctx)
 	if location != "" {
 		call = call.Location(location)
 	}


Test patch:
diff --git a/bigquery/integration_test.go b/bigquery/integration_test.go
index cc72392f7b..6ab8da7e63 100644
--- a/bigquery/integration_test.go
+++ b/bigquery/integration_test.go
@@ -250,6 +250,70 @@ func TestIntegration_DetectProjectID(t *testing.T) {
 	}
 }
 
+func TestIntegration_JobFrom(t *testing.T) {
+	if client == nil {
+		t.Skip("Integration tests skipped")
+	}
+	ctx := context.Background()
+
+	// Create a job we can use for referencing.
+	q := client.Query("SELECT 123 as foo")
+	it, err := q.Read(ctx)
+	if err != nil {
+		t.Fatalf("failed to run test query: %v", err)
+	}
+	want := it.SourceJob()
+
+	// establish a new client that's pointed at an invalid project/location.
+	otherClient, err := NewClient(ctx, "bad-project-id")
+	if err != nil {
+		t.Fatalf("failed to create other client: %v", err)
+	}
+	otherClient.Location = "badloc"
+
+	for _, tc := range []struct {
+		description string
+		f           func(*Client) (*Job, error)
+		wantErr     bool
+	}{
+		{
+			description: "JobFromID",
+			f:           func(c *Client) (*Job, error) { return c.JobFromID(ctx, want.jobID) },
+			wantErr:     true,
+		},
+		{
+			description: "JobFromIDLocation",
+			f:           func(c *Client) (*Job, error) { return c.JobFromIDLocation(ctx, want.jobID, want.location) },
+			wantErr:     true,
+		},
+		{
+			description: "JobFromProject",
+			f:           func(c *Client) (*Job, error) { return c.JobFromProject(ctx, want.projectID, want.jobID, want.location) },
+		},
+	} {
+		got, err := tc.f(otherClient)
+		if err != nil {
+			if !tc.wantErr {
+				t.Errorf("case %q errored: %v", tc.description, err)
+			}
+			continue
+		}
+		if tc.wantErr {
+			t.Errorf("case %q got success, expected error", tc.description)
+		}
+		if got.projectID != want.projectID {
+			t.Errorf("case %q projectID mismatch, got %s want %s", tc.description, got.projectID, want.projectID)
+		}
+		if got.location != want.location {
+			t.Errorf("case %q location mismatch, got %s want %s", tc.description, got.location, want.location)
+		}
+		if got.jobID != want.jobID {
+			t.Errorf("case %q jobID mismatch, got %s want %s", tc.description, got.jobID, want.jobID)
+		}
+	}
+
+}
+
 func TestIntegration_TableCreate(t *testing.T) {
 	// Check that creating a record field with an empty schema is an error.
 	if client == nil {