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
data/lib/spark_connect/column.rb
CHANGED
|
@@ -40,17 +40,25 @@ module SparkConnect
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# An unresolved attribute reference by (possibly dotted) name. The special
|
|
43
|
-
# name `"*"` expands to all columns
|
|
43
|
+
# name `"*"` expands to all columns and `"t.*"` to all columns of `t`.
|
|
44
44
|
#
|
|
45
45
|
# @param name [String]
|
|
46
|
+
# @param plan_id [Integer, nil] resolve against the relation with this plan
|
|
47
|
+
# id (set by {DataFrame#[]} so self-joins stay unambiguous).
|
|
48
|
+
# @param is_metadata_column [Boolean, nil] reference a metadata column.
|
|
46
49
|
# @return [Column]
|
|
47
|
-
def from_name(name)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
def from_name(name, plan_id: nil, is_metadata_column: nil)
|
|
51
|
+
name = name.to_s
|
|
52
|
+
if name == "*" || name.end_with?(".*")
|
|
53
|
+
star = Proto::Expression::UnresolvedStar.new
|
|
54
|
+
star.unparsed_target = name unless name == "*"
|
|
55
|
+
star.plan_id = plan_id if plan_id
|
|
56
|
+
new(Proto::Expression.new(unresolved_star: star))
|
|
50
57
|
else
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
attr = Proto::Expression::UnresolvedAttribute.new(unparsed_identifier: name)
|
|
59
|
+
attr.plan_id = plan_id if plan_id
|
|
60
|
+
attr.is_metadata_column = is_metadata_column unless is_metadata_column.nil?
|
|
61
|
+
new(Proto::Expression.new(unresolved_attribute: attr))
|
|
54
62
|
end
|
|
55
63
|
end
|
|
56
64
|
|
|
@@ -107,7 +115,10 @@ module SparkConnect
|
|
|
107
115
|
when Float
|
|
108
116
|
l.new(double: value)
|
|
109
117
|
when BigDecimal
|
|
110
|
-
|
|
118
|
+
# Like PySpark, tag decimal literals as DecimalType(10, 0); the server
|
|
119
|
+
# widens that to fit the value (max of the declared and the value's own
|
|
120
|
+
# precision/scale), so nothing is rounded.
|
|
121
|
+
l.new(decimal: l::Decimal.new(value: decimal_string(value), precision: 10, scale: 0))
|
|
111
122
|
when Rational
|
|
112
123
|
l.new(double: value.to_f)
|
|
113
124
|
when String
|
|
@@ -144,6 +155,31 @@ module SparkConnect
|
|
|
144
155
|
end
|
|
145
156
|
end
|
|
146
157
|
|
|
158
|
+
# Convert a protobuf `Expression.Literal` into the corresponding Ruby
|
|
159
|
+
# value (the inverse of {to_literal}; used for observed metrics).
|
|
160
|
+
#
|
|
161
|
+
# @param literal [Spark::Connect::Expression::Literal]
|
|
162
|
+
# @return [Object]
|
|
163
|
+
def literal_value(literal)
|
|
164
|
+
kind = literal.literal_type
|
|
165
|
+
case kind
|
|
166
|
+
when nil, :null then nil
|
|
167
|
+
when :decimal then BigDecimal(literal.decimal.value)
|
|
168
|
+
when :date then Date.new(1970, 1, 1) + literal.date
|
|
169
|
+
when :timestamp then Time.at(Rational(literal.timestamp, 1_000_000)).utc
|
|
170
|
+
when :timestamp_ntz then Time.at(Rational(literal.timestamp_ntz, 1_000_000)).utc
|
|
171
|
+
when :array then literal.array.elements.map { |e| literal_value(e) }
|
|
172
|
+
when :map then literal.map.keys.map { |k| literal_value(k) }.zip(literal.map.values.map { |v| literal_value(v) }).to_h
|
|
173
|
+
when :struct then literal.struct.elements.map { |e| literal_value(e) }
|
|
174
|
+
when :specialized_array
|
|
175
|
+
arr = literal.specialized_array
|
|
176
|
+
arr.public_send(arr.value_type).values.to_a
|
|
177
|
+
else
|
|
178
|
+
value = literal.public_send(kind)
|
|
179
|
+
value.is_a?(Google::Protobuf::MessageExts) ? value.to_h : value
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
147
183
|
# Infer the Spark {Types::DataType} for a Ruby value (used when building
|
|
148
184
|
# array/map literals). Mirrors PySpark's literal type inference.
|
|
149
185
|
#
|
|
@@ -172,6 +208,13 @@ module SparkConnect
|
|
|
172
208
|
|
|
173
209
|
private
|
|
174
210
|
|
|
211
|
+
# Render a BigDecimal like Python's `str(Decimal)` for the common cases:
|
|
212
|
+
# integral values without a fractional part ("12"), others in plain
|
|
213
|
+
# notation ("1.5").
|
|
214
|
+
def decimal_string(value)
|
|
215
|
+
value.frac.zero? ? value.to_i.to_s : value.to_s("F")
|
|
216
|
+
end
|
|
217
|
+
|
|
175
218
|
def infer_array_element_type(array)
|
|
176
219
|
sample = array.find { |v| !v.nil? }
|
|
177
220
|
sample.nil? ? Types.null : infer_type(sample)
|
|
@@ -216,6 +259,13 @@ module SparkConnect
|
|
|
216
259
|
def bitwise_and(other) = bin_op("&", other)
|
|
217
260
|
def bitwise_or(other) = bin_op("|", other)
|
|
218
261
|
def bitwise_xor(other) = bin_op("^", other)
|
|
262
|
+
alias bitwiseAND bitwise_and
|
|
263
|
+
alias bitwiseOR bitwise_or
|
|
264
|
+
alias bitwiseXOR bitwise_xor
|
|
265
|
+
|
|
266
|
+
# Null-safe equality alias matching PySpark's `eqNullSafe`.
|
|
267
|
+
# @return [Column]
|
|
268
|
+
def eqNullSafe(other) = eq_null_safe(other)
|
|
219
269
|
|
|
220
270
|
# ---- Null / membership predicates -------------------------------------
|
|
221
271
|
def is_null = Column.invoke("isNull", self)
|
|
@@ -223,10 +273,18 @@ module SparkConnect
|
|
|
223
273
|
def is_nan = Column.invoke("isNaN", self)
|
|
224
274
|
alias isNull is_null
|
|
225
275
|
alias isNotNull is_not_null
|
|
276
|
+
alias isNaN is_nan
|
|
226
277
|
|
|
227
|
-
# True if the column's value is in `values
|
|
278
|
+
# True if the column's value is in `values`, or - given a single-column
|
|
279
|
+
# {DataFrame} - in the rows of that subquery (`IN (SELECT ...)`). A struct
|
|
280
|
+
# column (`F.struct("a", "b")`) compares several columns against a
|
|
281
|
+
# multi-column subquery. IN subqueries need a Spark 4.1+ server.
|
|
282
|
+
#
|
|
283
|
+
# @param values [Array<Object>, DataFrame]
|
|
228
284
|
# @return [Column]
|
|
229
285
|
def isin(*values)
|
|
286
|
+
return in_subquery(values.first) if values.size == 1 && values.first.is_a?(DataFrame)
|
|
287
|
+
|
|
230
288
|
values = values.first if values.size == 1 && values.first.is_a?(Array)
|
|
231
289
|
Column.invoke("in", self, *Array(values))
|
|
232
290
|
end
|
|
@@ -270,8 +328,41 @@ module SparkConnect
|
|
|
270
328
|
# Extract a struct field by name.
|
|
271
329
|
# @return [Column]
|
|
272
330
|
def get_field(name)
|
|
273
|
-
get_item(name.to_s)
|
|
331
|
+
get_item(name.is_a?(Column) ? name : name.to_s)
|
|
274
332
|
end
|
|
333
|
+
alias getItem get_item
|
|
334
|
+
alias getField get_field
|
|
335
|
+
|
|
336
|
+
# Add or replace the struct field `field_name` with `col`.
|
|
337
|
+
#
|
|
338
|
+
# @param field_name [String]
|
|
339
|
+
# @param col [Column]
|
|
340
|
+
# @return [Column]
|
|
341
|
+
def with_field(field_name, col)
|
|
342
|
+
raise IllegalArgumentError, "field_name must be a String, got #{field_name.class}" unless field_name.is_a?(String)
|
|
343
|
+
raise IllegalArgumentError, "col must be a Column, got #{col.class}" unless col.is_a?(Column)
|
|
344
|
+
|
|
345
|
+
Column.new(Proto::Expression.new(update_fields: Proto::Expression::UpdateFields.new(
|
|
346
|
+
struct_expression: @expr, field_name: field_name, value_expression: col.to_expr
|
|
347
|
+
)))
|
|
348
|
+
end
|
|
349
|
+
alias withField with_field
|
|
350
|
+
|
|
351
|
+
# Drop one or more (possibly nested, dotted) fields from a struct column.
|
|
352
|
+
#
|
|
353
|
+
# @param field_names [Array<String>]
|
|
354
|
+
# @return [Column]
|
|
355
|
+
def drop_fields(*field_names)
|
|
356
|
+
raise IllegalArgumentError, "drop_fields requires at least one field name" if field_names.empty?
|
|
357
|
+
|
|
358
|
+
expr = field_names.reduce(@expr) do |acc, name|
|
|
359
|
+
raise IllegalArgumentError, "field name must be a String, got #{name.class}" unless name.is_a?(String)
|
|
360
|
+
|
|
361
|
+
Proto::Expression.new(update_fields: Proto::Expression::UpdateFields.new(struct_expression: acc, field_name: name))
|
|
362
|
+
end
|
|
363
|
+
Column.new(expr)
|
|
364
|
+
end
|
|
365
|
+
alias dropFields drop_fields
|
|
275
366
|
|
|
276
367
|
# ---- Aliasing / naming -------------------------------------------------
|
|
277
368
|
# Assign one or more output names. With multiple names the expression must
|
|
@@ -306,6 +397,38 @@ module SparkConnect
|
|
|
306
397
|
alias as_type cast
|
|
307
398
|
alias astype cast
|
|
308
399
|
|
|
400
|
+
# Like {#cast}, but returns null instead of failing when the value cannot
|
|
401
|
+
# be cast (TRY mode).
|
|
402
|
+
#
|
|
403
|
+
# @param data_type [Types::DataType, String]
|
|
404
|
+
# @return [Column]
|
|
405
|
+
def try_cast(data_type)
|
|
406
|
+
c = cast(data_type).to_expr.cast
|
|
407
|
+
c.eval_mode = :EVAL_MODE_TRY
|
|
408
|
+
Column.new(Proto::Expression.new(cast: c))
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Mark this column as a reference to the outer query inside a subquery
|
|
412
|
+
# (e.g. a correlated {DataFrame#scalar}). Over Spark Connect the server
|
|
413
|
+
# resolves outer references itself, so this returns an equivalent column,
|
|
414
|
+
# like PySpark.
|
|
415
|
+
# @return [Column]
|
|
416
|
+
def outer
|
|
417
|
+
Column.new(@expr)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Apply a function (a block or callable) to this column and return its
|
|
421
|
+
# result, enabling fluent custom transformations.
|
|
422
|
+
#
|
|
423
|
+
# @yieldparam col [Column] self
|
|
424
|
+
# @return [Column]
|
|
425
|
+
def transform(fn = nil, &block)
|
|
426
|
+
fn ||= block
|
|
427
|
+
raise IllegalArgumentError, "transform requires a block or callable" unless fn
|
|
428
|
+
|
|
429
|
+
fn.call(self)
|
|
430
|
+
end
|
|
431
|
+
|
|
309
432
|
# ---- Sort ordering -----------------------------------------------------
|
|
310
433
|
def asc = sort_order(:SORT_DIRECTION_ASCENDING, :SORT_NULLS_FIRST)
|
|
311
434
|
def desc = sort_order(:SORT_DIRECTION_DESCENDING, :SORT_NULLS_LAST)
|
|
@@ -364,6 +487,16 @@ module SparkConnect
|
|
|
364
487
|
|
|
365
488
|
private
|
|
366
489
|
|
|
490
|
+
def in_subquery(df)
|
|
491
|
+
values =
|
|
492
|
+
if @expr.expr_type == :unresolved_function && @expr.unresolved_function.function_name == "struct"
|
|
493
|
+
@expr.unresolved_function.arguments.to_a
|
|
494
|
+
else
|
|
495
|
+
[@expr]
|
|
496
|
+
end
|
|
497
|
+
df.subquery_expression(:SUBQUERY_TYPE_IN, in_subquery_values: values)
|
|
498
|
+
end
|
|
499
|
+
|
|
367
500
|
def bin_op(name, other)
|
|
368
501
|
Column.invoke(name, self, Column.to_col(other))
|
|
369
502
|
end
|
data/lib/spark_connect/conf.rb
CHANGED
|
@@ -28,6 +28,20 @@ module SparkConnect
|
|
|
28
28
|
nil
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Set several configuration properties in one request.
|
|
32
|
+
#
|
|
33
|
+
# @param configs [Hash{String=>Object}]
|
|
34
|
+
# @param silent [Boolean] ignore failures (e.g. static configurations that
|
|
35
|
+
# cannot be changed at runtime) on Spark 4.0+ servers.
|
|
36
|
+
# @return [void]
|
|
37
|
+
def set_all(configs, silent: false)
|
|
38
|
+
pairs = configs.map { |k, v| Proto::KeyValue.new(key: k.to_s, value: v.to_s) }
|
|
39
|
+
set = CR::Set.new(pairs: pairs)
|
|
40
|
+
set.silent = true if silent
|
|
41
|
+
@client.config(Op.new(set: set))
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
31
45
|
# Get the value of a configuration property.
|
|
32
46
|
#
|
|
33
47
|
# @param key [String]
|
|
@@ -39,7 +53,7 @@ module SparkConnect
|
|
|
39
53
|
Op.new(get: CR::Get.new(keys: [key.to_s]))
|
|
40
54
|
else
|
|
41
55
|
Op.new(get_with_default: CR::GetWithDefault.new(
|
|
42
|
-
pairs: [Proto::KeyValue.new(key: key.to_s, value: default)]
|
|
56
|
+
pairs: [Proto::KeyValue.new(key: key.to_s, value: default.to_s)]
|
|
43
57
|
))
|
|
44
58
|
end
|
|
45
59
|
resp = @client.config(op)
|
|
@@ -75,5 +89,7 @@ module SparkConnect
|
|
|
75
89
|
resp = @client.config(Op.new(is_modifiable: CR::IsModifiable.new(keys: [key.to_s])))
|
|
76
90
|
resp.pairs.first&.value == "true"
|
|
77
91
|
end
|
|
92
|
+
alias is_modifiable modifiable?
|
|
93
|
+
alias isModifiable modifiable?
|
|
78
94
|
end
|
|
79
95
|
end
|