spark-connect 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +173 -1
- data/README.md +37 -13
- data/lib/spark_connect/arrow.rb +5 -1
- data/lib/spark_connect/catalog.rb +157 -12
- data/lib/spark_connect/channel_builder.rb +119 -12
- data/lib/spark_connect/client.rb +237 -17
- data/lib/spark_connect/column.rb +143 -10
- data/lib/spark_connect/conf.rb +17 -1
- data/lib/spark_connect/data_frame.rb +447 -25
- data/lib/spark_connect/functions.rb +1948 -494
- data/lib/spark_connect/grouped_data.rb +33 -7
- data/lib/spark_connect/merge.rb +146 -0
- data/lib/spark_connect/na_functions.rb +43 -9
- data/lib/spark_connect/observation.rb +26 -5
- data/lib/spark_connect/pipelines.rb +101 -0
- data/lib/spark_connect/plan.rb +52 -0
- data/lib/spark_connect/proto/spark/connect/base_pb.rb +6 -1
- data/lib/spark_connect/proto/spark/connect/base_services_pb.rb +2 -0
- data/lib/spark_connect/proto/spark/connect/catalog_pb.rb +11 -1
- data/lib/spark_connect/proto/spark/connect/commands_pb.rb +1 -1
- data/lib/spark_connect/proto/spark/connect/expressions_pb.rb +3 -1
- data/lib/spark_connect/proto/spark/connect/pipelines_pb.rb +5 -1
- data/lib/spark_connect/proto/spark/connect/relations_pb.rb +4 -1
- data/lib/spark_connect/proto/spark/connect/types_pb.rb +3 -1
- data/lib/spark_connect/reader.rb +98 -17
- data/lib/spark_connect/session.rb +232 -30
- data/lib/spark_connect/stat_functions.rb +19 -9
- data/lib/spark_connect/storage_level.rb +79 -0
- data/lib/spark_connect/streaming.rb +335 -20
- data/lib/spark_connect/table_arg.rb +73 -0
- data/lib/spark_connect/tvf.rb +78 -0
- data/lib/spark_connect/types.rb +149 -0
- data/lib/spark_connect/version.rb +2 -2
- data/lib/spark_connect/writer.rb +80 -10
- data/lib/spark_connect.rb +4 -0
- data/proto/spark/connect/base.proto +93 -1
- data/proto/spark/connect/catalog.proto +81 -0
- data/proto/spark/connect/commands.proto +7 -0
- data/proto/spark/connect/expressions.proto +28 -0
- data/proto/spark/connect/pipelines.proto +92 -5
- data/proto/spark/connect/relations.proto +71 -1
- data/proto/spark/connect/types.proto +19 -0
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e74ad29eb515f38a9735a8d719adc6fb38d8d7cbb7719ca5cf69b43ae4b32de
|
|
4
|
+
data.tar.gz: f9006bc6e7f05631175457fe4d8219f023456d544ff9d1d377ac918dcc4ad9af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 975ca916148a4005eddfe9db971746f0128b5190251dde7dcc8e287cae267df8169d3446136c195fc4e2298945e5ad1a6a1c70754c2ca12ae95b00b29eb3ceb2
|
|
7
|
+
data.tar.gz: 9df3247554b07123aedfc0cbc5c790f5cda158fe328ff3d8145f311ab9ac36acf35ad3d0489939b6ea68853fdaf736a66b791652f42ef612222f27596f6b556c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,176 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
Feature parity with the PySpark 4.3.0 Spark Connect client. Items marked with a
|
|
13
|
+
Spark version need a server of at least that version.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Spark Connect 4.3.0 protocol.** Regenerated the vendored protobuf/gRPC stubs
|
|
18
|
+
from Spark v4.3.0 (`SparkConnect::SPARK_VERSION` is now `"4.3.0"`).
|
|
19
|
+
- **Every `pyspark.sql.functions` function** except the UDF family: 280 new
|
|
20
|
+
functions (559 of PySpark's 562), including the HLL/KLL/Theta/Tuple sketch,
|
|
21
|
+
bitmap, variant (with the 4.3 `variant_set`/`variant_insert`/`variant_delete`/
|
|
22
|
+
`variant_array_append` family), vector (4.3), geospatial (`st_*`), TIME
|
|
23
|
+
(`make_time`, `time_*`, `current_time`), nanosecond timestamp (4.3), XML/CSV,
|
|
24
|
+
AES, URL, collation, `try_*`, interval, partition transform, `listagg`/
|
|
25
|
+
`string_agg`, `count_min_sketch`, `hmac`, base32, `jaro_winkler_similarity`,
|
|
26
|
+
`call_function`, `assert_true`/`raise_error`, and `broadcast` functions, plus
|
|
27
|
+
the PySpark 4.3 parameters `instr(..., start, occurrence)`,
|
|
28
|
+
`regexp_replace(..., position)`, and `max_by`/`min_by(..., k)`, an
|
|
29
|
+
`array_sort` comparator, and PySpark's camelCase aliases (`bitwiseNOT`,
|
|
30
|
+
`shiftLeft`, `toDegrees`, ...). `udf`, `udtf`, and `arrow_udtf` raise
|
|
31
|
+
`NotImplementedError`.
|
|
32
|
+
- **DataFrame**: `zip` (4.3), `nearest_by_join` (4.2), `repartition_by_id` (4.1),
|
|
33
|
+
`lateral_join`, `transpose`, `scalar`/`exists` subqueries, `as_table` table
|
|
34
|
+
arguments (`TableArg`), `group_by(..., grouping_sets:)`, `merge_into`
|
|
35
|
+
(`MergeIntoWriter`), `zip_with_index`, `tail`, `with_metadata`,
|
|
36
|
+
`metadata_column`, `random_split`, `cache`/`persist`/`unpersist`/
|
|
37
|
+
`storage_level`/`cached?` with `SparkConnect::StorageLevel`,
|
|
38
|
+
`local_checkpoint(storage_level:)`, `register_temp_table`, `df[...]` by name,
|
|
39
|
+
index, Column (filter), or Array (select), the NA/stat shortcuts (`dropna`,
|
|
40
|
+
`fillna`, `replace`, `approx_quantile`, `corr`, `cov`, `crosstab`,
|
|
41
|
+
`freq_items`, `sample_by`), and PySpark camelCase aliases (`isEmpty`,
|
|
42
|
+
`sameSemantics`, `semanticHash`, `inputFiles`, ...).
|
|
43
|
+
- **Column**: `isin(df)` IN-subqueries (Spark 4.1+), `try_cast`,
|
|
44
|
+
`with_field`, `drop_fields`, `outer`, `transform`, qualified stars
|
|
45
|
+
(`F.col("t.*")`), and the `bitwiseAND`/`bitwiseOR`/`bitwiseXOR`/
|
|
46
|
+
`eqNullSafe`/`isNaN`/`getItem`/`getField` aliases.
|
|
47
|
+
- **Session**: `empty_data_frame`, `clone_session` (4.1), `tvf`
|
|
48
|
+
(table-valued functions), progress handlers
|
|
49
|
+
(`register_progress_handler`/`remove_progress_handler`/
|
|
50
|
+
`clear_progress_handlers`, reporting `StageInfo`s), `stopped?`, and the
|
|
51
|
+
`SparkSession.active`/`get_active_session`/`default_session` lookups. The
|
|
52
|
+
builder gained `channel_builder`, `config(hash)`, a block form that stops the
|
|
53
|
+
session afterwards, `master` (no-op), and `enable_hive_support` (raises, as
|
|
54
|
+
in PySpark).
|
|
55
|
+
- **Client**: `get_operation_statuses` (the 4.2 `GetStatus` RPC), per-RPC
|
|
56
|
+
deadlines (`SparkConnect::RpcDeadlines`), and `close`/`closed?`.
|
|
57
|
+
- **Connection strings**: the `grpc_keepalive_enabled`,
|
|
58
|
+
`grpc_keepalive_time_ms`, `grpc_keepalive_timeout_ms`, and
|
|
59
|
+
`grpc_keepalive_without_calls` parameters (keepalive is on by default, like
|
|
60
|
+
the 4.3 clients), a `SPARK_CONNECT_AUTHENTICATE_TOKEN` default for `token`,
|
|
61
|
+
and 128 MiB gRPC message limits.
|
|
62
|
+
- **Catalog** (4.2): `drop_table`, `drop_view`, `create_database`,
|
|
63
|
+
`drop_database`, `list_partitions`, `list_views`, `get_table_properties`,
|
|
64
|
+
`get_create_table_string`, `truncate_table`, and `analyze_table`; plus
|
|
65
|
+
`get_database`, `get_table`, `get_function`, `refresh_by_path`, name
|
|
66
|
+
patterns for the `list_*` methods, `cache_table(storage_level)`, and
|
|
67
|
+
camelCase aliases.
|
|
68
|
+
- **Reading and writing**: `read.changes` (4.2), `read.xml`, parsing a
|
|
69
|
+
DataFrame of JSON/CSV/XML strings (`read.json(df)`; XML needs 4.2),
|
|
70
|
+
partitioned `read.jdbc`; `write.cluster_by`, `write.xml`, `write.jdbc`,
|
|
71
|
+
`with_schema_evolution` (4.2) on both writers, and `write_to(...).options`/
|
|
72
|
+
`partitioned_by`/`cluster_by`.
|
|
73
|
+
- **Structured Streaming**: `read_stream.name` (named sources, 4.2),
|
|
74
|
+
`read_stream.changes` (4.2), `read_stream.xml`, `write_stream.cluster_by`,
|
|
75
|
+
`trigger(real_time: ...)` (4.2), keyword shortcuts on `start`/`to_table`,
|
|
76
|
+
`StreamingQueryListener` with `streams.add_listener`/`remove_listener`, and
|
|
77
|
+
camelCase aliases on `StreamingQuery`.
|
|
78
|
+
- **Declarative Pipelines**: `create_streaming_table` and
|
|
79
|
+
`create_auto_cdc_flow` (4.2).
|
|
80
|
+
- **Types**: `TimeType` and `GeometryType`/`GeographyType` (4.1),
|
|
81
|
+
`TimestampNTZNanosType`/`TimestampLTZNanosType` (4.3), and decoding of
|
|
82
|
+
user-defined types (`UserDefinedType`); TIME values decode to `Arrow::Time`
|
|
83
|
+
and nanosecond timestamps keep their nanoseconds.
|
|
84
|
+
- `RuntimeConfig#set_all` and the `is_modifiable`/`isModifiable` aliases.
|
|
85
|
+
- `Observation#get` raises the server's error when a metric failed (4.2).
|
|
86
|
+
- Spark 4.3.0 is now the first entry of the CI integration matrix.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
- **Function arguments follow PySpark's conventions exactly** (each function
|
|
91
|
+
was checked against PySpark 4.3.0 expression for expression). Where PySpark
|
|
92
|
+
takes a column, a String is a column name, so `F.date_add("d", "n")`,
|
|
93
|
+
`F.add_months`, `F.date_sub`, `F.substring`, `F.slice`, `F.overlay`,
|
|
94
|
+
`F.repeat`, `F.array_repeat`, `F.array_insert`, the `split` limit, and the
|
|
95
|
+
`regexp_like`/`regexp_count`/`regexp_substr`/`regexp_extract_all` pattern
|
|
96
|
+
now read columns where they used to send String literals; wrap literals in
|
|
97
|
+
`F.lit`.
|
|
98
|
+
- **Client-side random seeds.** `F.rand`, `F.randn`, `F.uuid`, `F.shuffle` (and
|
|
99
|
+
the new `randstr`, `uniform`, `count_min_sketch`), `DataFrame#sample`,
|
|
100
|
+
`random_split`, and `sample_by` pick a seed on the client when none is given,
|
|
101
|
+
so a DataFrame returns the same values on every action. On Spark 3.5
|
|
102
|
+
servers, `F.uuid`, `F.shuffle`, and `F.mode` (which now sends its
|
|
103
|
+
`deterministic` flag) need `F.expr` instead.
|
|
104
|
+
- `F.mean`, `F.pow`, and one-argument `F.log` send `avg`, `power`, and `ln`
|
|
105
|
+
like PySpark, so their result columns are named accordingly; the base of
|
|
106
|
+
`F.log(base, col)` is a literal.
|
|
107
|
+
- `F.lit(BigDecimal(...))` is tagged `decimal(10, 0)` like PySpark; the server
|
|
108
|
+
widens it to fit the value (`1.5` becomes `decimal(10,1)` instead of
|
|
109
|
+
`decimal(2,1)`).
|
|
110
|
+
- `df["x"]`, `df.col("x")`, `df.x`, `col_regex`, and `pivot("x")` refer to that
|
|
111
|
+
DataFrame's column (bound to its plan id, like PySpark), which disambiguates
|
|
112
|
+
self-joins. `group_by`/`rollup`/`cube` accept 1-based ordinals, and `join`
|
|
113
|
+
accepts an Array of Column conditions (combined with AND) and rejects
|
|
114
|
+
DataFrames from another session.
|
|
115
|
+
- `GroupedData`: `sum`/`avg`/`mean`/`max`/`min` without columns aggregate
|
|
116
|
+
every numeric column; `agg` without expressions raises; `pivot` is only
|
|
117
|
+
allowed directly after `group_by` (`NotImplementedError` otherwise).
|
|
118
|
+
- `DataFrameNaFunctions`: `drop`, `fill`, and `replace` validate their
|
|
119
|
+
arguments like PySpark; `replace` needs a `value` unless `to_replace` is a
|
|
120
|
+
Hash, and integer replacement values are sent as doubles.
|
|
121
|
+
- `StatFunctions#corr` raises unless the method is `"pearson"`.
|
|
122
|
+
- Reader/writer/stream `option(key, nil)` is ignored (it used to send `""`),
|
|
123
|
+
and `DataFrameWriter#mode(nil)` is a no-op.
|
|
124
|
+
- Builder options are applied with one request that silently skips options the
|
|
125
|
+
session cannot change (as PySpark does), and `spark.remote`/`spark.master`
|
|
126
|
+
are never sent; `get_or_create` applies them to an existing session too, and
|
|
127
|
+
`create` makes the new session active.
|
|
128
|
+
- `new_session` follows PySpark 4.3: always a fresh, empty server-side session
|
|
129
|
+
against the same endpoint.
|
|
130
|
+
- `SparkSession#stop` also closes the gRPC channel and stops the listener bus,
|
|
131
|
+
and is idempotent.
|
|
132
|
+
- Every connection-string parameter the client does not consume is now sent as
|
|
133
|
+
gRPC request metadata (it used to be only `x-*` ones), and `session_id` must
|
|
134
|
+
be a UUID (`ConnectionError` otherwise).
|
|
135
|
+
- Non-streaming RPCs have deadlines (1 hour for `AnalyzePlan`, 10 minutes for
|
|
136
|
+
the others); `ExecutePlan` has none.
|
|
137
|
+
|
|
138
|
+
### Fixed
|
|
139
|
+
|
|
140
|
+
- `F.bitwise_not`/`F.bitwiseNOT` and `F.negate` sent function names the server
|
|
141
|
+
does not resolve (`bitwise_not`, `negate`); they now send `~` and `negative`.
|
|
142
|
+
- `F.from_json`, `F.to_json`, and `F.schema_of_json` flattened their options
|
|
143
|
+
into extra arguments instead of passing one map, so any options failed.
|
|
144
|
+
- `F.trim`, `F.ltrim`, and `F.rtrim` with a trim string sent their arguments in
|
|
145
|
+
the wrong order.
|
|
146
|
+
- `StreamingQuery#exception` raised `NoMethodError` for a query without an
|
|
147
|
+
exception.
|
|
148
|
+
- `DataFrameReader#schema(StructType)` lost non-nullable fields and metadata;
|
|
149
|
+
the schema is now sent as JSON, like PySpark.
|
|
150
|
+
- `Observation#get` returned raw protobuf messages (decimal, array, map, and
|
|
151
|
+
struct metrics) or epoch integers (date and timestamp metrics).
|
|
152
|
+
- `new_session` reused the `session_id` of the connection string, so the "new"
|
|
153
|
+
session was the same server-side session.
|
|
154
|
+
|
|
155
|
+
## [0.3.0] - 2026-06-15
|
|
156
|
+
|
|
157
|
+
### Fixed
|
|
158
|
+
|
|
159
|
+
- **Correct `TimestampType` instants in `create_data_frame`.** Timestamp columns
|
|
160
|
+
were shipped as zone-less Arrow timestamps, so the server interpreted the epoch
|
|
161
|
+
micros as session-local wall-clock and shifted the value by the session time
|
|
162
|
+
zone. They are now tagged UTC; `TimestampNTZType` remains zone-less.
|
|
163
|
+
- **`RuntimeConfig#get` with a non-String default no longer raises.** A non-String
|
|
164
|
+
default (e.g. `conf.get(key, 8)`) was passed straight into a protobuf string
|
|
165
|
+
field, raising `Google::Protobuf::TypeError`. The default is now coerced to a
|
|
166
|
+
String, matching `#set`.
|
|
167
|
+
- **No duplicate rows when an execute stream is retried.** The result accumulator
|
|
168
|
+
was created outside the retry loop, so a mid-stream gRPC failure replayed
|
|
169
|
+
already-consumed Arrow batches and duplicated rows on retry. The accumulator is
|
|
170
|
+
now reset per attempt.
|
|
171
|
+
- **`DataFrame#drop_duplicates_within_watermark` is now watermark-aware.** It was a
|
|
172
|
+
plain alias of `#drop_duplicates` and never set the `within_watermark` flag, so
|
|
173
|
+
it silently performed an ordinary deduplication. Added a `dropDuplicatesWithinWatermark`
|
|
174
|
+
alias.
|
|
175
|
+
- **`SparkSession::Builder#app_name` is now applied.** `create` explicitly skipped
|
|
176
|
+
`spark.app.name`, making `#app_name` a no-op; all builder options are now
|
|
177
|
+
forwarded to the new session.
|
|
178
|
+
- Corrected misleading doc comments for `Functions#nanvl` and `DataFrame#except_all`.
|
|
179
|
+
|
|
10
180
|
## [0.2.0] - 2026-06-10
|
|
11
181
|
|
|
12
182
|
### Added
|
|
@@ -77,6 +247,8 @@ Initial release.
|
|
|
77
247
|
- Vendored Spark Connect 4.1 protobuf/gRPC definitions and a regeneration script
|
|
78
248
|
(`bin/generate-protos`).
|
|
79
249
|
|
|
80
|
-
[Unreleased]: https://github.com/HyukjinKwon/spark-connect-ruby/compare/v0.
|
|
250
|
+
[Unreleased]: https://github.com/HyukjinKwon/spark-connect-ruby/compare/v0.4.0...HEAD
|
|
251
|
+
[0.4.0]: https://github.com/HyukjinKwon/spark-connect-ruby/compare/v0.3.0...v0.4.0
|
|
252
|
+
[0.3.0]: https://github.com/HyukjinKwon/spark-connect-ruby/compare/v0.2.0...v0.3.0
|
|
81
253
|
[0.2.0]: https://github.com/HyukjinKwon/spark-connect-ruby/compare/v0.1.0...v0.2.0
|
|
82
254
|
[0.1.0]: https://github.com/HyukjinKwon/spark-connect-ruby/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -40,11 +40,29 @@ spark.stop
|
|
|
40
40
|
|
|
41
41
|
## What it supports
|
|
42
42
|
|
|
43
|
-
`spark-connect` implements the Spark Connect **DataFrame**, **
|
|
44
|
-
**
|
|
45
|
-
**
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
`spark-connect` implements the Spark Connect **DataFrame**, **Column** and
|
|
44
|
+
**functions**, **SQL**, **Catalog**, **reader/writer**, **Structured
|
|
45
|
+
Streaming** (including query listeners), and **Declarative Pipelines** API of
|
|
46
|
+
PySpark 4.3: subqueries, lateral and nearest-by joins, `zip`, `merge_into`,
|
|
47
|
+
table-valued functions, `as_table` table arguments, caching with storage
|
|
48
|
+
levels, session cloning, progress handlers, operation status, the TIME,
|
|
49
|
+
spatial, and nanosecond timestamp types, and every built-in SQL function in
|
|
50
|
+
`pyspark.sql.functions`.
|
|
51
|
+
|
|
52
|
+
**Out of scope** are the parts of PySpark that run client-side code on the
|
|
53
|
+
server or depend on Python libraries, because Spark executes such code only in
|
|
54
|
+
Python (or JVM) workers and has no Ruby worker:
|
|
55
|
+
|
|
56
|
+
- user-defined functions and table functions (`udf`, `udtf`, `arrow_udtf`,
|
|
57
|
+
pandas and Arrow UDFs; these raise `NotImplementedError`);
|
|
58
|
+
- the `foreach`/`foreachBatch` streaming sinks (raise `NotImplementedError`);
|
|
59
|
+
- `applyInPandas`/`applyInArrow`, `transformWithState(InPandas)`, cogrouped
|
|
60
|
+
maps, `mapInPandas`/`mapInArrow`, and Python data sources;
|
|
61
|
+
- pandas interop (`toPandas`, `pandas_api`, `plot`) -- use `to_arrow` for a
|
|
62
|
+
columnar `Arrow::Table`;
|
|
63
|
+
- the UDF profiler (`spark.profile`) and artifact upload (`addArtifacts`,
|
|
64
|
+
`copyFromLocalToFs`), which exist to serve UDF code;
|
|
65
|
+
- the separate, experimental MLlib-over-Connect surface.
|
|
48
66
|
|
|
49
67
|
Results decode through Apache Arrow into ordered, name-addressable `Row`s.
|
|
50
68
|
Method names are snake_case (idiomatic Ruby) with camelCase aliases for the
|
|
@@ -57,33 +75,39 @@ common PySpark names (`groupBy`, `withColumn`, `orderBy`, `createDataFrame`,
|
|
|
57
75
|
- **Apache Arrow C++/GLib system libraries** (required by the `red-arrow` dependency):
|
|
58
76
|
- macOS: `brew install apache-arrow apache-arrow-glib`
|
|
59
77
|
- Ubuntu/Debian: install `libarrow-glib-dev` from the [Apache Arrow APT repository](https://arrow.apache.org/install/)
|
|
60
|
-
- A reachable **Spark Connect server**. This client is generated against the Spark Connect 4.
|
|
78
|
+
- A reachable **Spark Connect server**. This client is generated against the Spark Connect 4.3 protocol and supports **Apache Spark 3.5 and above**.
|
|
61
79
|
|
|
62
80
|
See the [installation guide](https://hyukjinkwon.github.io/spark-connect-ruby/installation.html) for details.
|
|
63
81
|
|
|
64
82
|
## Installation
|
|
65
83
|
|
|
66
84
|
```bash
|
|
85
|
+
gem install rubygems-requirements-system
|
|
67
86
|
gem install spark-connect
|
|
68
87
|
```
|
|
69
88
|
|
|
70
89
|
Or in a `Gemfile`:
|
|
71
90
|
|
|
72
91
|
```ruby
|
|
92
|
+
plugin "rubygems-requirements-system"
|
|
73
93
|
gem "spark-connect"
|
|
74
94
|
```
|
|
75
95
|
|
|
76
96
|
## Running a local Spark Connect server
|
|
77
97
|
|
|
78
98
|
```bash
|
|
79
|
-
# Download a Spark distribution (4.
|
|
80
|
-
curl -fsSL https://archive.apache.org/dist/spark/spark-4.
|
|
81
|
-
cd spark-4.
|
|
99
|
+
# Download a Spark distribution (4.3.0 shown here; 3.5+ also works)
|
|
100
|
+
curl -fsSL https://archive.apache.org/dist/spark/spark-4.3.0/spark-4.3.0-bin-hadoop3.tgz | tar xz
|
|
101
|
+
cd spark-4.3.0-bin-hadoop3
|
|
82
102
|
|
|
83
|
-
# Start the Connect server (requires Java 17+)
|
|
84
|
-
|
|
103
|
+
# Start the Connect server (requires Java 17+).
|
|
104
|
+
# Spark 4.0.0+ bundles the Connect server, so no extra packages are needed.
|
|
105
|
+
./sbin/start-connect-server.sh
|
|
85
106
|
```
|
|
86
107
|
|
|
108
|
+
On **Spark 3.5.x** the Connect server is not bundled; pull it in with
|
|
109
|
+
`--packages "org.apache.spark:spark-connect_2.13:3.5.5"` (use a Scala 2.13 distribution).
|
|
110
|
+
|
|
87
111
|
The server listens on `sc://localhost:15002` by default.
|
|
88
112
|
|
|
89
113
|
## Connecting
|
|
@@ -100,7 +124,7 @@ SparkConnect::SparkSession.builder
|
|
|
100
124
|
.get_or_create
|
|
101
125
|
```
|
|
102
126
|
|
|
103
|
-
Supported parameters: `token
|
|
127
|
+
Supported parameters: `token` (defaults to `$SPARK_CONNECT_AUTHENTICATE_TOKEN`), `user_id`, `user_agent`, `use_ssl`, `session_id`, and the gRPC keepalive settings `grpc_keepalive_enabled`, `grpc_keepalive_time_ms`, `grpc_keepalive_timeout_ms`, `grpc_keepalive_without_calls`. Any other parameter (e.g. `x-api-key=...`) is sent as a gRPC request header.
|
|
104
128
|
|
|
105
129
|
## A quick tour
|
|
106
130
|
|
|
@@ -143,7 +167,7 @@ Runnable [`examples/`](examples/) cover quickstart, transformations, aggregation
|
|
|
143
167
|
|
|
144
168
|
## Compatibility
|
|
145
169
|
|
|
146
|
-
The client is generated against the **Spark Connect 4.
|
|
170
|
+
The client is generated against the **Spark Connect 4.3** protocol and supports **Apache Spark 3.5 and above** (the Spark Connect wire protocol is backward compatible across these releases). APIs added in newer Spark releases (for example `DataFrame#zip` in 4.3 or `nearest_by_join` in 4.2) need a server of at least that version; see [Troubleshooting](https://hyukjinkwon.github.io/spark-connect-ruby/troubleshooting.html#version-compatibility) for the list. CI runs the integration specs against Spark 4.3.0, 4.2.0, 4.1.0, 4.0.0, and 3.5.5.
|
|
147
171
|
|
|
148
172
|
## Development
|
|
149
173
|
|
data/lib/spark_connect/arrow.rb
CHANGED
|
@@ -103,7 +103,11 @@ module SparkConnect
|
|
|
103
103
|
when Types::StringType, Types::CharType, Types::VarcharType then :string
|
|
104
104
|
when Types::BinaryType then :binary
|
|
105
105
|
when Types::DateType then :date32
|
|
106
|
-
|
|
106
|
+
# TimestampType is an instant: tag it UTC so the server reads the epoch
|
|
107
|
+
# micros as a point in time rather than session-local wall-clock. The NTZ
|
|
108
|
+
# variant stays zone-less (wall-clock) to match its semantics.
|
|
109
|
+
when Types::TimestampType then ::Arrow::TimestampDataType.new(:micro, GLib::TimeZone.new("UTC"))
|
|
110
|
+
when Types::TimestampNTZType then { type: :timestamp, unit: :micro }
|
|
107
111
|
when Types::ArrayType
|
|
108
112
|
{ type: :list, field: { name: "element", type: arrow_field_type(data_type.element_type) } }
|
|
109
113
|
when Types::StructType
|
|
@@ -15,6 +15,22 @@ module SparkConnect
|
|
|
15
15
|
Proto = SparkConnect::Proto
|
|
16
16
|
C = Proto::Catalog
|
|
17
17
|
|
|
18
|
+
# PySpark-style camelCase aliases, defined at the end of the class.
|
|
19
|
+
CAMEL_ALIASES = {
|
|
20
|
+
currentCatalog: :current_catalog, setCurrentCatalog: :set_current_catalog, listCatalogs: :list_catalogs,
|
|
21
|
+
currentDatabase: :current_database, setCurrentDatabase: :set_current_database, listDatabases: :list_databases,
|
|
22
|
+
getDatabase: :get_database, listTables: :list_tables, getTable: :get_table, listViews: :list_views,
|
|
23
|
+
listFunctions: :list_functions, getFunction: :get_function, listColumns: :list_columns,
|
|
24
|
+
tableExists: :table_exists, databaseExists: :database_exists, functionExists: :function_exists,
|
|
25
|
+
dropTempView: :drop_temp_view, dropGlobalTempView: :drop_global_temp_view, isCached: :cached?,
|
|
26
|
+
cacheTable: :cache_table, uncacheTable: :uncache_table, clearCache: :clear_cache, refreshTable: :refresh_table,
|
|
27
|
+
refreshByPath: :refresh_by_path, recoverPartitions: :recover_partitions, createTable: :create_table,
|
|
28
|
+
createExternalTable: :create_external_table, dropTable: :drop_table, dropView: :drop_view,
|
|
29
|
+
createDatabase: :create_database, dropDatabase: :drop_database, listPartitions: :list_partitions,
|
|
30
|
+
getTableProperties: :get_table_properties, getCreateTableString: :get_create_table_string,
|
|
31
|
+
truncateTable: :truncate_table, analyzeTable: :analyze_table,
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
18
34
|
# @param session [SparkSession]
|
|
19
35
|
def initialize(session)
|
|
20
36
|
@session = session
|
|
@@ -30,9 +46,12 @@ module SparkConnect
|
|
|
30
46
|
run(C.new(set_current_catalog: Proto::SetCurrentCatalog.new(catalog_name: name.to_s)))
|
|
31
47
|
end
|
|
32
48
|
|
|
33
|
-
# @
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
# @param pattern [String, nil] a SQL LIKE pattern the names must match.
|
|
50
|
+
# @return [Array<Row>] the catalogs (`name`, `description`).
|
|
51
|
+
def list_catalogs(pattern = nil)
|
|
52
|
+
lc = Proto::ListCatalogs.new
|
|
53
|
+
lc.pattern = pattern if pattern
|
|
54
|
+
rows(C.new(list_catalogs: lc))
|
|
36
55
|
end
|
|
37
56
|
|
|
38
57
|
# @return [String] the current default database.
|
|
@@ -45,26 +64,70 @@ module SparkConnect
|
|
|
45
64
|
run(C.new(set_current_database: Proto::SetCurrentDatabase.new(db_name: name.to_s)))
|
|
46
65
|
end
|
|
47
66
|
|
|
48
|
-
# @
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
# @param pattern [String, nil] a SQL LIKE pattern the names must match.
|
|
68
|
+
# @return [Array<Row>] the databases (`name`, `catalog`, `description`, `locationUri`).
|
|
69
|
+
def list_databases(pattern = nil)
|
|
70
|
+
ld = Proto::ListDatabases.new
|
|
71
|
+
ld.pattern = pattern if pattern
|
|
72
|
+
rows(C.new(list_databases: ld))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param db_name [String] the database (may be catalog-qualified).
|
|
76
|
+
# @return [Row] the database (`name`, `catalog`, `description`, `locationUri`).
|
|
77
|
+
def get_database(db_name)
|
|
78
|
+
rows(C.new(get_database: Proto::GetDatabase.new(db_name: db_name.to_s))).first
|
|
51
79
|
end
|
|
52
80
|
|
|
53
81
|
# @param db_name [String, nil] restrict to a database.
|
|
54
|
-
# @
|
|
55
|
-
|
|
82
|
+
# @param pattern [String, nil] a SQL LIKE pattern the names must match.
|
|
83
|
+
# @return [Array<Row>] tables and views (`name`, `catalog`, `namespace`,
|
|
84
|
+
# `description`, `tableType`, `isTemporary`).
|
|
85
|
+
def list_tables(db_name = nil, pattern = nil)
|
|
56
86
|
lt = Proto::ListTables.new
|
|
57
87
|
lt.db_name = db_name if db_name
|
|
88
|
+
lt.pattern = pattern if pattern
|
|
58
89
|
rows(C.new(list_tables: lt))
|
|
59
90
|
end
|
|
60
91
|
|
|
92
|
+
# @param table_name [String] a (possibly qualified) table or view name.
|
|
93
|
+
# @return [Row] the table (`name`, `catalog`, `namespace`, `description`,
|
|
94
|
+
# `tableType`, `isTemporary`).
|
|
95
|
+
def get_table(table_name, db_name = nil)
|
|
96
|
+
gt = Proto::GetTable.new(table_name: table_name.to_s)
|
|
97
|
+
gt.db_name = db_name if db_name
|
|
98
|
+
rows(C.new(get_table: gt)).first
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Views (not tables) in a database (Spark 4.2+).
|
|
102
|
+
#
|
|
103
|
+
# @param db_name [String, nil] the database (current database when nil).
|
|
104
|
+
# @param pattern [String, nil] a SQL LIKE pattern the names must match.
|
|
105
|
+
# @return [Array<Row>] like {#list_tables}.
|
|
106
|
+
def list_views(db_name = nil, pattern = nil)
|
|
107
|
+
db_name = current_database if pattern && db_name.nil?
|
|
108
|
+
lv = Proto::ListViews.new
|
|
109
|
+
lv.db_name = db_name if db_name
|
|
110
|
+
lv.pattern = pattern if pattern
|
|
111
|
+
rows(C.new(list_views: lv))
|
|
112
|
+
end
|
|
113
|
+
|
|
61
114
|
# @return [Array<Row>] functions registered in the catalog.
|
|
62
|
-
def list_functions(db_name = nil)
|
|
115
|
+
def list_functions(db_name = nil, pattern = nil)
|
|
63
116
|
lf = Proto::ListFunctions.new
|
|
64
117
|
lf.db_name = db_name if db_name
|
|
118
|
+
lf.pattern = pattern if pattern
|
|
65
119
|
rows(C.new(list_functions: lf))
|
|
66
120
|
end
|
|
67
121
|
|
|
122
|
+
# @param function_name [String] a (possibly qualified) function name.
|
|
123
|
+
# @return [Row] the function (`name`, `catalog`, `namespace`, `description`,
|
|
124
|
+
# `className`, `isTemporary`).
|
|
125
|
+
def get_function(function_name, db_name = nil)
|
|
126
|
+
gf = Proto::GetFunction.new(function_name: function_name.to_s)
|
|
127
|
+
gf.db_name = db_name if db_name
|
|
128
|
+
rows(C.new(get_function: gf)).first
|
|
129
|
+
end
|
|
130
|
+
|
|
68
131
|
# @param table_name [String]
|
|
69
132
|
# @return [Array<Row>] the columns of a table.
|
|
70
133
|
def list_columns(table_name, db_name = nil)
|
|
@@ -106,10 +169,15 @@ module SparkConnect
|
|
|
106
169
|
def cached?(table_name)
|
|
107
170
|
scalar(C.new(is_cached: Proto::IsCached.new(table_name: table_name.to_s))) == true
|
|
108
171
|
end
|
|
172
|
+
alias is_cached cached?
|
|
109
173
|
|
|
110
|
-
# Cache a table in memory
|
|
111
|
-
|
|
112
|
-
|
|
174
|
+
# Cache a table (in memory and on disk by default).
|
|
175
|
+
# @param storage_level [StorageLevel, nil]
|
|
176
|
+
# @return [void]
|
|
177
|
+
def cache_table(table_name, storage_level = nil)
|
|
178
|
+
ct = Proto::CacheTable.new(table_name: table_name.to_s)
|
|
179
|
+
ct.storage_level = storage_level.to_proto if storage_level
|
|
180
|
+
run(C.new(cache_table: ct))
|
|
113
181
|
end
|
|
114
182
|
|
|
115
183
|
# Remove a table from the cache. @return [void]
|
|
@@ -127,11 +195,86 @@ module SparkConnect
|
|
|
127
195
|
run(C.new(refresh_table: Proto::RefreshTable.new(table_name: table_name.to_s)))
|
|
128
196
|
end
|
|
129
197
|
|
|
198
|
+
# Invalidate and refresh cached data/metadata of everything under `path`.
|
|
199
|
+
# @return [void]
|
|
200
|
+
def refresh_by_path(path)
|
|
201
|
+
run(C.new(refresh_by_path: Proto::RefreshByPath.new(path: path.to_s)))
|
|
202
|
+
end
|
|
203
|
+
|
|
130
204
|
# Recover all partitions of a table. @return [void]
|
|
131
205
|
def recover_partitions(table_name)
|
|
132
206
|
run(C.new(recover_partitions: Proto::RecoverPartitions.new(table_name: table_name.to_s)))
|
|
133
207
|
end
|
|
134
208
|
|
|
209
|
+
# ---- DDL and table maintenance (Spark 4.2+) -------------------------------
|
|
210
|
+
|
|
211
|
+
# Drop a persistent table (not a temporary view; see {#drop_temp_view}).
|
|
212
|
+
#
|
|
213
|
+
# @param if_exists [Boolean] do not fail when the table does not exist.
|
|
214
|
+
# @param purge [Boolean] skip the trash (where supported).
|
|
215
|
+
# @return [void]
|
|
216
|
+
def drop_table(table_name, if_exists: false, purge: false)
|
|
217
|
+
run(C.new(drop_table: Proto::DropTable.new(table_name: table_name.to_s, if_exists: if_exists, purge: purge)))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Drop a persistent view.
|
|
221
|
+
# @return [void]
|
|
222
|
+
def drop_view(view_name, if_exists: false)
|
|
223
|
+
run(C.new(drop_view: Proto::DropView.new(view_name: view_name.to_s, if_exists: if_exists)))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Create a database (namespace).
|
|
227
|
+
#
|
|
228
|
+
# @param db_name [String] may be catalog-qualified / multi-part.
|
|
229
|
+
# @param if_not_exists [Boolean]
|
|
230
|
+
# @param properties [Hash{String=>String}] e.g. `comment`, `location`.
|
|
231
|
+
# @return [void]
|
|
232
|
+
def create_database(db_name, if_not_exists: false, properties: {})
|
|
233
|
+
cd = Proto::CreateDatabase.new(db_name: db_name.to_s, if_not_exists: if_not_exists, properties: stringify(properties))
|
|
234
|
+
run(C.new(create_database: cd))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Drop a database (namespace).
|
|
238
|
+
#
|
|
239
|
+
# @param cascade [Boolean] also drop its tables, views, and functions.
|
|
240
|
+
# @return [void]
|
|
241
|
+
def drop_database(db_name, if_exists: false, cascade: false)
|
|
242
|
+
run(C.new(drop_database: Proto::DropDatabase.new(db_name: db_name.to_s, if_exists: if_exists, cascade: cascade)))
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# The partitions of a table, like `SHOW PARTITIONS`.
|
|
246
|
+
# @return [Array<Row>] one Row with a `partition` String (e.g. `"p=1"`) per partition.
|
|
247
|
+
def list_partitions(table_name)
|
|
248
|
+
rows(C.new(list_partitions: Proto::ListPartitions.new(table_name: table_name.to_s)))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# The properties of a table or view, like `SHOW TBLPROPERTIES`.
|
|
252
|
+
# @return [Hash{String=>String}]
|
|
253
|
+
def get_table_properties(table_name)
|
|
254
|
+
rows(C.new(get_table_properties: Proto::GetTableProperties.new(table_name: table_name.to_s))).to_h { |r| [r[0], r[1]] }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# The `SHOW CREATE TABLE` DDL of a table or view.
|
|
258
|
+
# @param as_serde [Boolean] request Hive SerDe DDL where applicable.
|
|
259
|
+
# @return [String]
|
|
260
|
+
def get_create_table_string(table_name, as_serde: false)
|
|
261
|
+
gcts = Proto::GetCreateTableString.new(table_name: table_name.to_s, as_serde: as_serde)
|
|
262
|
+
scalar(C.new(get_create_table_string: gcts)).to_s
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Remove all rows from a table (not supported for views).
|
|
266
|
+
# @return [void]
|
|
267
|
+
def truncate_table(table_name)
|
|
268
|
+
run(C.new(truncate_table: Proto::TruncateTable.new(table_name: table_name.to_s)))
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Compute table statistics, like `ANALYZE TABLE ... COMPUTE STATISTICS`.
|
|
272
|
+
# @param no_scan [Boolean] only compute size statistics (`NOSCAN`).
|
|
273
|
+
# @return [void]
|
|
274
|
+
def analyze_table(table_name, no_scan: false)
|
|
275
|
+
run(C.new(analyze_table: Proto::AnalyzeTable.new(table_name: table_name.to_s, no_scan: no_scan)))
|
|
276
|
+
end
|
|
277
|
+
|
|
135
278
|
# Create a managed table and return a {DataFrame} over it.
|
|
136
279
|
#
|
|
137
280
|
# @param table_name [String]
|
|
@@ -163,6 +306,8 @@ module SparkConnect
|
|
|
163
306
|
@session.table(table_name.to_s)
|
|
164
307
|
end
|
|
165
308
|
|
|
309
|
+
CAMEL_ALIASES.each { |camel, snake| alias_method camel, snake }
|
|
310
|
+
|
|
166
311
|
private
|
|
167
312
|
|
|
168
313
|
def stringify(options)
|