Google Cloud Storage (Parquet)¶
Reads and writes Parquet objects on Google Cloud Storage through Arrow's
GcsFileSystem, providing both a source and a sink for the int64 and string channels.
Overview¶
This connector integrates Google Cloud Storage as a Parquet store. It uses Arrow's arrow::fs::GcsFileSystem as the transport, so there is no separate GCS client dependency: the google-cloud-cpp storage client is statically bundled into libarrow when Arrow is built with ARROW_GCS=ON. Records are encoded as Parquet via the same ArrowBatcher<T> seam used by the operator wire and the local Parquet connector. Two record channels are supported, std::int64_t and std::string, each with a sink and a source. The sink writes with ZSTD compression by default.
Dependency and version¶
| Component | Provenance | Version |
|---|---|---|
| Apache Arrow + Parquet | Compiled from source (scripts/build-arrow.sh), built with ARROW_GCS=ON |
24.0.0 |
| google-cloud-cpp storage client | Statically bundled inside libarrow by the Arrow GCS build; not a separate clink dependency |
bundled with Arrow 24.0.0 |
There is no apt or brew client library and no separate link dependency. The GCS support rides the Arrow that clink::core already links (impls/gcs/CMakeLists.txt).
Enabling it¶
The module is gated by the CLINK_WITH_GCS CMake option (AUTO / ON / OFF), which defaults to AUTO (CMakeLists.txt). The real gate is whether the pinned Arrow was compiled with GCS support: find_package(Arrow) sets ARROW_GCS, and the module only builds when that is true.
AUTO: builds the connector ifARROW_GCSis set, otherwise skips it silently.ON: fails configuration with a fatal error if Arrow was built without GCS support.OFF: the target is not defined.
To guarantee GCS support, build Arrow with -DARROW_GCS=ON (already set in scripts/build-arrow.sh) and re-bootstrap the dependency prefix.
cmake -S . -B build -DCLINK_WITH_GCS=ON
cmake --build build -j
Factories¶
| Factory name | Direction | Record type |
|---|---|---|
gcs_parquet_int64_sink |
Sink | int64 |
gcs_parquet_string_sink |
Sink | string |
gcs_parquet_2pc_int64_sink |
Sink | int64 (two-phase commit, one staged file per checkpoint) |
gcs_parquet_2pc_string_sink |
Sink | string (two-phase commit, one staged file per checkpoint) |
gcs_parquet_int64_source |
Source | int64 |
gcs_parquet_string_source |
Source | string |
These are the exact names passed to register_sink / register_source in impls/gcs/src/register_factories.cpp.
Configuration¶
The sink and source share the same auth and endpoint parameters: the sources parse them with apply_gcs_params, and the sink factories with gcs_sink_fs_factory (both in impls/gcs/src/register_factories.cpp). A source needs key or prefix; a sink needs prefix (or key, taken as the prefix).
| Option | Required | Default | Description |
|---|---|---|---|
bucket |
Yes | (none) | Target GCS bucket. The factory throws if it is empty. |
prefix |
Sink: one of prefix/key |
(none) | Sink: the prefix its part objects are written under (bucket/prefix/sub<N>-...parquet). Source: read every matching object under it. |
key |
Source: one of key/prefix |
(none) | Source: the single object bucket + "/" + key. Sink: accepted as the prefix, for configurations written before the sink wrote a directory. |
anonymous |
No | false |
When true, uses anonymous access (GcsOptions::Anonymous()), suitable for the fake-gcs-server emulator or a public bucket. |
access_token |
No | (unset) | An explicit OAuth2 access token. Static, with a 24-hour far-future expiry and no refresh; the caller is responsible for token validity. Prefer credentials_file/credentials_json or Application Default Credentials for a long-running job. |
credentials_file / credentials_json |
No | (unset) | A service-account key, as a file path or inline JSON, mapped to GcsOptions::FromServiceAccountCredentials. Auto-refreshing: google-cloud-cpp mints and renews the OAuth token over the filesystem's lifetime. Takes precedence over access_token. |
endpoint_override |
No | (unset) | Overrides the GCS API endpoint, for targeting an emulator such as fake-gcs-server. When set, scheme defaults to http. |
scheme |
No | https (or http when endpoint_override is set) |
Connection scheme. |
project_id |
No | (unset) | GCP project id, applied to the GcsOptions. |
retry_limit_seconds |
No | Arrow default (15 minutes) | Caps the GCS retry window. Parsed from retry_limit_seconds as an integer; only applied when greater than 0. The streaming sink uses this to fail fast on a dead endpoint. |
Authentication and refresh: the credential precedence is anonymous > a service-account key (credentials_file/credentials_json) > a static access_token > Application Default Credentials (the default, GOOGLE_APPLICATION_CREDENTIALS or the ambient GCE/GKE/Cloud Run identity). Both the service-account key and Application Default Credentials auto-refresh, that is google-cloud-cpp mints and renews the OAuth token over the filesystem's lifetime, so they suit a long-running job; only the explicit access_token is static and expires on its own (make_gcs_options in parquet_gcs_sink.hpp).
The sink Options struct also carries compression (defaults to parquet::Compression::ZSTD) and an optional bucket_assigner for per-record Hive-style object keys. Neither is wired through the factory parameter parsing; they are only reachable via the programmatic API.
Each subtask names its part objects sub<N>-<run>-<seq>.parquet, so parallel subtasks and successive runs never write the same key (register_factories.cpp).
SQL usage¶
Mapped in src/sql/physical_plan.cpp under connector='gcs_parquet'. The SQL frontend resolves this to the string-channel factories (gcs_parquet_string_source / gcs_parquet_string_sink).
CREATE TABLE gcs_out (
line STRING
) WITH (
connector = 'gcs_parquet',
bucket = 'my-bucket',
prefix = 'output/data', -- parts land at output/data/sub<N>-...parquet
project_id = 'my-gcp-project'
);
Read it back with a table on the same prefix.
Other option keys from the table above (anonymous, access_token, endpoint_override, scheme, retry_limit_seconds) are passed through the same WITH (...) properties.
A format='json' sink table's rows are serialised by the row_to_json_string bridge, so a NaN or infinite double in the stored JSON text is null, or "NaN", "Infinity" or "-Infinity" with json_non_finite='string'. See NaN and infinity in JSON output.
Example¶
Programmatic use of the single-object classes with their Options, based on impls/gcs/tests/test_parquet_gcs_live.cpp. Write one Parquet object, then read it back on the int64 channel. (The registered sink factories build ParquetRollingSink<T> instead; see Delivery semantics.)
#include "clink/connectors/parquet_gcs_sink.hpp"
#include "clink/connectors/parquet_gcs_source.hpp"
#include "clink/core/arrow_batcher.hpp"
using clink::Batch;
using clink::int64_arrow_batcher;
using clink::ParquetGcsSink;
using clink::ParquetGcsSource;
// Write
ParquetGcsSink<std::int64_t>::Options so;
so.bucket = "my-bucket";
so.key = "data.parquet";
so.project_id = "my-gcp-project"; // Application Default Credentials by default
ParquetGcsSink<std::int64_t> sink(so, int64_arrow_batcher());
sink.open();
Batch<std::int64_t> b;
b.emplace(10);
b.emplace(20);
b.emplace(30);
sink.on_data(b);
sink.close(); // finalises the Parquet object
// Read back
ParquetGcsSource<std::int64_t>::Options ro;
ro.bucket = "my-bucket";
ro.key = "data.parquet";
ro.project_id = "my-gcp-project";
ParquetGcsSource<std::int64_t> src(ro, int64_arrow_batcher());
src.open();
clink::Emitter<std::int64_t> em(/* ... */);
while (src.produce(em)) {
}
src.close();
Via the registry, look up the factory by name and channel, for example gcs_parquet_string_sink / string, as exercised in impls/gcs/tests/test_factory_registration.cpp.
Delivery semantics¶
The factory sink is at-least-once and writes a directory of part objects under bucket/prefix through the shared ParquetRollingSink<T> (include/clink/connectors/parquet_rolling_sink.hpp). Each subtask closes a part at every checkpoint barrier and at the end of input. A part is written as <name>.inprogress and copied to its final .parquet key when complete, then the in-progress object is deleted: a GCS output stream cannot abort (Arrow's Abort() completes the upload), so writing to the final key would publish a partial part when a sink is torn down mid-interval. So the prefix never shows a partial .parquet object, everything up to the last barrier is already published when a kill lands, and a leftover .inprogress object is skipped by readers and removed by the next run. A run restored from a checkpoint keeps every part, including those past the restore point, whose rows the sources then replay; a run that starts from empty state deletes the previous run's parts under the prefix (only keys named exactly as the sink names its parts). The sink therefore needs list and delete permission as well as write. Read the directory back with the source on the same prefix. The programmatic ParquetGcsSink<T> writes a single object and finalises it only in close().
For exactly-once, use the 2PC sink: gcs_parquet_2pc_{int64,string}_sink programmatically, or delivery_guarantee='exactly_once' in SQL with a prefix instead of a key. It stages one Parquet file per checkpoint interval under <bucket>/<prefix>/staging and promotes it to <bucket>/<prefix>/committed only when the checkpoint completes globally; a crash between pre-commit and commit is recovered on open. Each object is named sub<N>-<run>-<ckpt>.parquet, where <run> is unique to each run of the sink, so a later run under the same prefix adds its objects beside an earlier run's, even where it reuses a checkpoint id, instead of replacing them. Read the result with the source pointed at <prefix>/committed.
The source reads a single Parquet object to its last row group and reports is_bounded() == true (parquet_gcs_source.hpp). It does not track or replay from an offset. On open() it validates that the file schema equals the ArrowBatcher schema (ignoring metadata) and throws on a mismatch.
Limitations¶
- The
gcs_parquet_2pcsink must be the only sink on its chain: at every checkpoint barrier it prepares the interval's transaction and stages its handle in the checkpoint, which holds only while it owns its chain's checkpoint, soDag::add_sinkrefuses another sink beside it, whichever is added first. SQL and job graphs plan every sink as a subtask of its own; with the Dag API, put the other sink in a separate Dag (see checkpointing). - The source reads one object with
key, or every object underprefix(aprefixinstead of akeyroutes to the sharedMultiObjectParquetSource, which lists the prefix, sorts, and shards objects round-robin across subtasks: objectiis read by subtaski % parallelism). Optional multi-object source params:recursive(defaulttrue),suffix(default.parquet). There is no Hive-partition pruning or column projection on either path. - Channels are limited to
int64andstring; there are no other typed factories. - The factory sink rolls parts at checkpoint barriers only (no size-based rollover) and is at-least-once. For exactly-once use the 2PC sink (
prefix+delivery_guarantee='exactly_once'), which stages one file per checkpoint and promotes it with a server-side copy when the checkpoint completes, so the committed object appears only whole. Up to and including v0.11.0 the promotion streamed the object to its committed name, and a GCS stream torn down mid-write completes its upload with what it holds, so a commit cut short could publish a partial committed object until recovery committed again. compressionand the per-recordbucket_assignerexist on the sinkOptionsbut are not configurable through the factory parameters or SQL; they require the programmatic API. The compression default is ZSTD.- The programmatic
ParquetGcsSink<T>keeps no checkpoint state and finishes each object only at close. An object being written when the process fails is left missing or incomplete; a cancelled task closes its objects with the rows they held, so they read as complete; and a run restored from a checkpoint writes only the rows after the restore point, so those before it are lost. Use it where a failure re-reads the input from the start; the factory sinks keep their output across a restore. access_tokenis static (fixed 24-hour expiry, no refresh); usecredentials_file/credentials_json(service account) or Application Default Credentials, both of which auto-refresh, for a long-running job.- The source requires the on-disk Parquet schema to match the
ArrowBatcherschema exactly (metadata aside), otherwiseopen()throws.
Testing¶
A live integration test exists at impls/gcs/tests/test_parquet_gcs_live.cpp (GcsParquetLive.WriteThenReadRoundTrip). It is skipped unless the environment variable CLINK_GCS_TEST_ENDPOINT is set to a GCS-API endpoint host:port (for example localhost:4443 from a fake-gcs-server emulator). The test creates a bucket up front via GcsFileSystem (writes do not auto-create buckets), writes int64 records through the sink, then reads them back through the source and asserts the round trip.
A non-gated unit test, impls/gcs/tests/test_factory_registration.cpp, verifies that all four factories are registered on the runner registry without needing any GCS endpoint.