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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +173 -1
  3. data/README.md +37 -13
  4. data/lib/spark_connect/arrow.rb +5 -1
  5. data/lib/spark_connect/catalog.rb +157 -12
  6. data/lib/spark_connect/channel_builder.rb +119 -12
  7. data/lib/spark_connect/client.rb +237 -17
  8. data/lib/spark_connect/column.rb +143 -10
  9. data/lib/spark_connect/conf.rb +17 -1
  10. data/lib/spark_connect/data_frame.rb +447 -25
  11. data/lib/spark_connect/functions.rb +1948 -494
  12. data/lib/spark_connect/grouped_data.rb +33 -7
  13. data/lib/spark_connect/merge.rb +146 -0
  14. data/lib/spark_connect/na_functions.rb +43 -9
  15. data/lib/spark_connect/observation.rb +26 -5
  16. data/lib/spark_connect/pipelines.rb +101 -0
  17. data/lib/spark_connect/plan.rb +52 -0
  18. data/lib/spark_connect/proto/spark/connect/base_pb.rb +6 -1
  19. data/lib/spark_connect/proto/spark/connect/base_services_pb.rb +2 -0
  20. data/lib/spark_connect/proto/spark/connect/catalog_pb.rb +11 -1
  21. data/lib/spark_connect/proto/spark/connect/commands_pb.rb +1 -1
  22. data/lib/spark_connect/proto/spark/connect/expressions_pb.rb +3 -1
  23. data/lib/spark_connect/proto/spark/connect/pipelines_pb.rb +5 -1
  24. data/lib/spark_connect/proto/spark/connect/relations_pb.rb +4 -1
  25. data/lib/spark_connect/proto/spark/connect/types_pb.rb +3 -1
  26. data/lib/spark_connect/reader.rb +98 -17
  27. data/lib/spark_connect/session.rb +232 -30
  28. data/lib/spark_connect/stat_functions.rb +19 -9
  29. data/lib/spark_connect/storage_level.rb +79 -0
  30. data/lib/spark_connect/streaming.rb +335 -20
  31. data/lib/spark_connect/table_arg.rb +73 -0
  32. data/lib/spark_connect/tvf.rb +78 -0
  33. data/lib/spark_connect/types.rb +149 -0
  34. data/lib/spark_connect/version.rb +2 -2
  35. data/lib/spark_connect/writer.rb +80 -10
  36. data/lib/spark_connect.rb +4 -0
  37. data/proto/spark/connect/base.proto +93 -1
  38. data/proto/spark/connect/catalog.proto +81 -0
  39. data/proto/spark/connect/commands.proto +7 -0
  40. data/proto/spark/connect/expressions.proto +28 -0
  41. data/proto/spark/connect/pipelines.proto +92 -5
  42. data/proto/spark/connect/relations.proto +71 -1
  43. data/proto/spark/connect/types.proto +19 -0
  44. metadata +10 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad83f60b70501f49722aa4ae51d768d27accbeb056c7f6fe1be695e010116c52
4
- data.tar.gz: 528b67cacc0ba1b516aa93228c242d07efcaa86ae297d75c9c11c9d36905a00f
3
+ metadata.gz: 0e74ad29eb515f38a9735a8d719adc6fb38d8d7cbb7719ca5cf69b43ae4b32de
4
+ data.tar.gz: f9006bc6e7f05631175457fe4d8219f023456d544ff9d1d377ac918dcc4ad9af
5
5
  SHA512:
6
- metadata.gz: ceb4e18bcc75a30a38fe4c2446ae192f07a24446f86b5a76b9992b4f6a70e59bfb3d4709f633988eacd00fa85d174b8904b80eb291855dc943a7c8e10f9c7f4d
7
- data.tar.gz: e06a6c98eee8042f56aabc5689655ad8bb7b04c843d90d2093d24fc6c82247993c0e47b002125ab774b1ce87c0e3b4203a88de6864a0c3dde1b45e7236db29f0
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.2.0...HEAD
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**, **SQL**,
44
- **Structured Streaming**, and **Declarative Pipelines** API -- everything
45
- **except** user-defined functions (UDFs) and the `foreach`/`foreachBatch`
46
- streaming sinks, whose Spark Connect protobuf definitions are not yet finalized.
47
- (The separate, experimental MLlib-over-Connect surface is also out of scope.)
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.1 protocol and supports **Apache Spark 3.5 and above**.
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.1.0 shown here; 3.5+ also works)
80
- curl -fsSL https://archive.apache.org/dist/spark/spark-4.1.0/spark-4.1.0-bin-hadoop3.tgz | tar xz
81
- cd spark-4.1.0-bin-hadoop3
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
- ./sbin/start-connect-server.sh --jars "$(pwd)/jars/spark-connect_2.13-4.1.0.jar"
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`, `user_id`, `user_agent`, `use_ssl`, `session_id`, and any `x-*` custom gRPC headers.
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.1** protocol and supports **Apache Spark 3.5 and above** (the Spark Connect wire protocol is backward compatible across these releases).
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
 
@@ -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
- when Types::TimestampType, Types::TimestampNTZType then { type: :timestamp, unit: :micro }
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
- # @return [Array<Row>] all catalogs.
34
- def list_catalogs
35
- rows(C.new(list_catalogs: Proto::ListCatalogs.new))
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
- # @return [Array<Row>] all databases.
49
- def list_databases
50
- rows(C.new(list_databases: Proto::ListDatabases.new))
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
- # @return [Array<Row>] tables (and views).
55
- def list_tables(db_name = nil)
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. @return [void]
111
- def cache_table(table_name)
112
- run(C.new(cache_table: Proto::CacheTable.new(table_name: table_name.to_s)))
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)