Skip to content

[Bug]: BigQueryIO.read() requires GCS tempLocation when using BigQuery Emulator #35777

Open

Description

@hichmo

What happened?

Description

When using the BigQuery emulator in combination with BigQueryIO.read() in Apache Beam (Java), the pipeline fails with the following error when running locally with DirectRunner:

java.lang.IllegalArgumentException: BigQueryIO.Read needs a GCS temp location to store temp files. This can be set with option --tempLocation.

This makes it impossible to write integration tests against the emulator using BigQueryIO.read() unless a (fake) GCS-compatible path is provided—even though the emulator does not support or require real GCS interaction.

To Reproduce

Steps to reproduce the behavior:

  1. Start BigQuery emulator (e.g., [goccy/bigquery-emulator](https://github.com/goccy/bigquery-emulator))
  2. Create a Beam pipeline that reads using BigQueryIO.readTableRows().from(...)
  3. Set bigQueryEndpoint to point to the emulator
  4. Omit tempLocation (or use a local one)
  5. Run with DirectRunner

Expected behavior

The pipeline should be able to run against the emulator.

Environment:

  • Beam version: 2.63.0 (also affects earlier)
  • Runner: DirectRunner
  • Java SDK
  • Emulator: ghcr.io/goccy/bigquery-emulator

Issue Priority

Priority: 2 (default / most bugs should be filed as P2)

Issue Components

  • Component: Python SDK
  • Component: Java SDK
  • Component: Go SDK
  • Component: Typescript SDK
  • Component: IO connector
  • Component: Beam YAML
  • Component: Beam examples
  • Component: Beam playground
  • Component: Beam katas
  • Component: Website
  • Component: Infrastructure
  • Component: Spark Runner
  • Component: Flink Runner
  • Component: Samza Runner
  • Component: Twister2 Runner
  • Component: Hazelcast Jet Runner
  • Component: Google Cloud Dataflow Runner

Activity

  1. Abacn commented on Aug 8, 2025

    @Abacn
    Contributor

    This is WAI. BigQueryIO.read() default to EXPORT, where Beam does not read from BigQuery directly. It invokes BigQuery export API to export tables to gcs, then Beam just read GCS files.

    Have you tried BigQueryIO.read with DIRECT_READ method (which does not require tempLocation)?

  2. hichmo commented on Aug 8, 2025

    @hichmo
    Author

    I have also try with DIRECT_READ and face another issue.

    The BigQuery job completes successfully, but the Apache Beam pipeline remains stuck, beam is likely waiting for the results to be available in the temporary table

  3. github-actions commented on Jan 6, 2026

    @github-actions
    Contributor

    This issue has been marked as stale due to 150 days of inactivity. It will be closed in 30 days if no further activity occurs. If you think that’s incorrect or this issue still needs to be addressed, please simply write any comment. If closed, you can reopen the issue at any time. Thank you for your contributions.

  4. github-actions commented on Feb 5, 2026

    @github-actions
    Contributor

    This issue has been closed due to lack of activity. If you think that is incorrect, you can reopen the issue at any time.

  5. reopened this on Feb 6, 2026
  6. mxtymoshyk commented on Aug 31, 2026

    @mxtymoshyk
    Contributor

    @Abacn thanks for reopening this. I reproduced it against ghcr.io/goccy/bigquery-emulator and the DIRECT_READ half that @hichmo raised in the follow-up looks like a real gap, distinct from the tempLocation part.

    The tempLocation half is WAI, as you said. BigQueryIO.java:1415 already skips that check when the method is DIRECT_READ, so nothing needs to change there:

    if (getMethod() != TypedRead.Method.DIRECT_READ) {
      String tempLocation = bqOptions.getTempLocation();
      checkArgument(!Strings.isNullOrEmpty(tempLocation), "BigQueryIO.Read needs a GCS temp location ...");

    Why DIRECT_READ hangs

    StorageClientImpl honors --bigQueryEndpoint but keeps the default gRPC transport, which negotiates TLS (BigQueryServicesImpl.java:1902-1913):

    BigQueryReadSettings.Builder settingsBuilder =
        BigQueryReadSettings.newBuilder()
            .setCredentialsProvider(FixedCredentialsProvider.create(options.getGcpCredential()))
            .setTransportChannelProvider(
                BigQueryReadSettings.defaultGrpcTransportProviderBuilder()
                    .setHeaderProvider(USER_AGENT_HEADER_PROVIDER)
                    .build())
            .setReadRowsRetryAttemptListener(listener);
    @Nullable String endpoint = options.getBigQueryEndpoint();
    if (!Strings.isNullOrEmpty(endpoint)) {
      settingsBuilder.setEndpoint(trimSchemaIfNecessary(endpoint));
    }

    The emulator's gRPC port is plaintext, so the handshake never completes. A/B with the same client, same endpoint, only the transport differing:

    Transport Result
    InstantiatingGrpcChannelProvider + setChannelConfigurator(c -> c.usePlaintext()) success in 0.4s, read session created, streams=1
    BigQueryReadSettings.defaultGrpcTransportProviderBuilder() (what Beam uses) hangs; killed at 25s and again at 60s

    Parked here:

    com.google.cloud.bigquery.storage.v1.BigQueryReadClient.createReadSession(BigQueryReadClient.java:259)
    com.google.api.gax.rpc.ApiExceptions.callAndTranslateApiException(ApiExceptions.java:53)
    

    The emulator logged nothing at all for the TLS attempt, i.e. the request never got past the handshake. The plaintext row is the control: the emulator's Storage Read API itself works.

    In a real pipeline this is worse than the 25s above suggests. BigQueryServicesImpl.java:1918-1925 sets createReadSession's initial, max and total RPC timeout to Duration.ofHours(2), so the user-visible symptom is a silent two-hour stall rather than an error. That matches @hichmo's report that the BigQuery job completes but the pipeline stays stuck.

    newBigQueryWriteClient (BigQueryServicesImpl.java:1786-1789) has the same gap, so STORAGE_WRITE_API against an emulator is blocked in the same way.

    A second, independent blocker: one option, two ports

    Running an actual DIRECT_READ pipeline never even reaches the storage client. With --bigQueryEndpoint=http://localhost:9060/ (the value the gRPC client needs), the REST client is pointed at the gRPC port and hangs there first:

    BigQueryServicesImpl$DatasetServiceImpl.getTable(BigQueryServicesImpl.java:703)
    BigQueryServicesImpl.executeWithRetries(BigQueryServicesImpl.java:1720)
    BigQueryStorageTableSource.getTargetTable(BigQueryStorageTableSource.java:220)
    

    bigQueryEndpoint feeds three clients with incompatible requirements: the REST client takes it verbatim as setRootUrl and needs http://host:9050/, while both gRPC clients run it through trimSchemaIfNecessary (BigQueryServicesImpl.java:1803) and need host:9060. The emulator serves REST on 9050 and gRPC on 9060, so no single value works. Fixing plaintext alone would not be enough.

    Precedent already in this module

    FirestoreStatefulComponentFactory.java:86-97 solves exactly this, in the same package tree:

    if (emulatorHostPort != null) {
      builder
          .setCredentialsProvider(FixedCredentialsProvider.create(new EmulatorCredentials()))
          .setEndpoint(emulatorHostPort)
          .setTransportChannelProvider(
              InstantiatingGrpcChannelProvider.newBuilder()
                  .setEndpoint(emulatorHostPort)
                  .setChannelConfigurator(c -> c.usePlaintext())
                  .build());
    }

    SpannerAccessor.java:275-277 does the same behind a usePlaintext flag.

    One question before I write anything

    Any fix here has to add public surface to BigQueryOptions, and that is the part I did not want to guess at. What reads most naturally to me is a new getBigQueryEmulatorHost(): it would point the two gRPC clients at that host with plaintext and a no-op credential, and leave bigQueryEndpoint alone for the REST client. That follows the Firestore precedent above, and since it is purely additive nothing changes for anyone already setting bigQueryEndpoint. The other way to do it would be a separate gRPC endpoint option alongside an explicit plaintext flag, which is closer to the Spanner shape.

    Do you have a preference? I have the reproduction set up and am happy to send a PR either way. Not claiming the issue in the meantime.

    Setup for all of the above: Beam 2.75.0 on the DirectRunner, emulator seeded with a single three-row table.

    One note if you try to reproduce this on 2.75.0. Before reaching either of the blockers above you will hit an NPE in BigQueryStorageTableSource.getEstimatedSizeBytes, because the emulator returns no numBytes for the table. That one is unrelated to this issue and is already fixed on master by 4e5ae91.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions