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
|
@@ -43,6 +43,17 @@ module SparkConnect
|
|
|
43
43
|
left_anti: :JOIN_TYPE_LEFT_ANTI,
|
|
44
44
|
}.freeze
|
|
45
45
|
|
|
46
|
+
# Join types accepted by {#lateral_join}.
|
|
47
|
+
LATERAL_JOIN_TYPES = {
|
|
48
|
+
inner: :JOIN_TYPE_INNER, left: :JOIN_TYPE_LEFT_OUTER, leftouter: :JOIN_TYPE_LEFT_OUTER, cross: :JOIN_TYPE_CROSS,
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# {#nearest_by_join} argument domains (mirroring Spark's `NearestByJoinValidation`).
|
|
52
|
+
NEAREST_BY_JOIN_MAX_NUM_RESULTS = 100_000
|
|
53
|
+
NEAREST_BY_JOIN_JOIN_TYPES = %w[inner leftouter left].freeze
|
|
54
|
+
NEAREST_BY_JOIN_MODES = %w[approx exact].freeze
|
|
55
|
+
NEAREST_BY_JOIN_DIRECTIONS = %w[distance similarity].freeze
|
|
56
|
+
|
|
46
57
|
# @return [SparkSession]
|
|
47
58
|
attr_reader :session
|
|
48
59
|
# @return [Spark::Connect::Relation] the logical plan this DataFrame builds.
|
|
@@ -182,7 +193,24 @@ module SparkConnect
|
|
|
182
193
|
build(deduplicate: dedup)
|
|
183
194
|
end
|
|
184
195
|
alias dropDuplicates drop_duplicates
|
|
185
|
-
|
|
196
|
+
|
|
197
|
+
# Drop duplicate rows within the event-time watermark, optionally restricted
|
|
198
|
+
# to a subset of columns. Unlike {#drop_duplicates}, this is watermark-aware
|
|
199
|
+
# and is intended for streaming DataFrames (mirrors PySpark's
|
|
200
|
+
# `dropDuplicatesWithinWatermark`).
|
|
201
|
+
#
|
|
202
|
+
# @param subset [Array<String>, nil]
|
|
203
|
+
# @return [DataFrame]
|
|
204
|
+
def drop_duplicates_within_watermark(subset = nil)
|
|
205
|
+
dedup =
|
|
206
|
+
if subset.nil? || subset.empty?
|
|
207
|
+
Proto::Deduplicate.new(input: @relation, all_columns_as_keys: true, within_watermark: true)
|
|
208
|
+
else
|
|
209
|
+
Proto::Deduplicate.new(input: @relation, column_names: Array(subset).map(&:to_s), within_watermark: true)
|
|
210
|
+
end
|
|
211
|
+
build(deduplicate: dedup)
|
|
212
|
+
end
|
|
213
|
+
alias dropDuplicatesWithinWatermark drop_duplicates_within_watermark
|
|
186
214
|
|
|
187
215
|
# ---- Ordering ----------------------------------------------------------
|
|
188
216
|
|
|
@@ -224,7 +252,7 @@ module SparkConnect
|
|
|
224
252
|
# @param cols [Array<Column, String>]
|
|
225
253
|
# @return [GroupedData]
|
|
226
254
|
def group_by(*cols)
|
|
227
|
-
GroupedData.new(self,
|
|
255
|
+
GroupedData.new(self, grouping_columns(cols), :GROUP_TYPE_GROUPBY)
|
|
228
256
|
end
|
|
229
257
|
alias groupBy group_by
|
|
230
258
|
alias groupby group_by
|
|
@@ -232,15 +260,28 @@ module SparkConnect
|
|
|
232
260
|
# Multi-dimensional rollup.
|
|
233
261
|
# @return [GroupedData]
|
|
234
262
|
def rollup(*cols)
|
|
235
|
-
GroupedData.new(self,
|
|
263
|
+
GroupedData.new(self, grouping_columns(cols), :GROUP_TYPE_ROLLUP)
|
|
236
264
|
end
|
|
237
265
|
|
|
238
266
|
# Multi-dimensional cube.
|
|
239
267
|
# @return [GroupedData]
|
|
240
268
|
def cube(*cols)
|
|
241
|
-
GroupedData.new(self,
|
|
269
|
+
GroupedData.new(self, grouping_columns(cols), :GROUP_TYPE_CUBE)
|
|
242
270
|
end
|
|
243
271
|
|
|
272
|
+
# Aggregate over explicit grouping sets (`GROUP BY ... GROUPING SETS (...)`).
|
|
273
|
+
#
|
|
274
|
+
# @example
|
|
275
|
+
# df.grouping_sets([["dept", "year"], ["dept"], []], "dept", "year").agg(F.sum("x"))
|
|
276
|
+
# @param grouping_sets [Array<Array<Column, String, Integer>>]
|
|
277
|
+
# @param cols [Array<Column, String, Integer>] the grouping columns.
|
|
278
|
+
# @return [GroupedData]
|
|
279
|
+
def grouping_sets(grouping_sets, *cols)
|
|
280
|
+
sets = grouping_sets.map { |set| grouping_columns(Array(set)) }
|
|
281
|
+
GroupedData.new(self, grouping_columns(cols), :GROUP_TYPE_GROUPING_SETS, grouping_sets: sets)
|
|
282
|
+
end
|
|
283
|
+
alias groupingSets grouping_sets
|
|
284
|
+
|
|
244
285
|
# Aggregate over the whole DataFrame (a group-by with no grouping columns).
|
|
245
286
|
#
|
|
246
287
|
# @param exprs [Array<Column>, Hash]
|
|
@@ -254,23 +295,82 @@ module SparkConnect
|
|
|
254
295
|
# Join with another DataFrame.
|
|
255
296
|
#
|
|
256
297
|
# @param other [DataFrame]
|
|
257
|
-
# @param on [String, Array<String>, Column, nil] join key
|
|
258
|
-
# boolean join condition
|
|
259
|
-
#
|
|
298
|
+
# @param on [String, Array<String>, Column, Array<Column>, nil] join key
|
|
299
|
+
# column name(s), a boolean join condition, or several conditions that
|
|
300
|
+
# are combined with AND.
|
|
301
|
+
# @param how [Symbol, String, nil] join type (see {JOIN_TYPES}); `inner` by
|
|
302
|
+
# default. Case and underscores are ignored (`"LEFT_OUTER"` == `"leftouter"`).
|
|
260
303
|
# @return [DataFrame]
|
|
261
304
|
def join(other, on: nil, how: :inner)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
j = Proto::Join.new(left: @relation, right: other.relation, join_type: jt)
|
|
305
|
+
check_same_session(other)
|
|
306
|
+
j = Proto::Join.new(left: @relation, right: other.relation, join_type: join_type(how))
|
|
265
307
|
case on
|
|
266
308
|
when nil then nil
|
|
267
309
|
when Column then j.join_condition = on.to_expr
|
|
268
|
-
when Array
|
|
310
|
+
when Array
|
|
311
|
+
if on.all?(Column)
|
|
312
|
+
j.join_condition = on.reduce(:&).to_expr unless on.empty?
|
|
313
|
+
else
|
|
314
|
+
j.using_columns += on.map(&:to_s)
|
|
315
|
+
end
|
|
269
316
|
else j.using_columns << on.to_s
|
|
270
317
|
end
|
|
271
318
|
build(join: j)
|
|
272
319
|
end
|
|
273
320
|
|
|
321
|
+
# Lateral join with a DataFrame that may reference this one's columns via
|
|
322
|
+
# {Column#outer} (e.g. a table-valued function over each row).
|
|
323
|
+
#
|
|
324
|
+
# @param other [DataFrame]
|
|
325
|
+
# @param on [Column, nil] the join condition.
|
|
326
|
+
# @param how [Symbol, String, nil] `inner` (default), `left`/`leftouter`, or `cross`.
|
|
327
|
+
# @return [DataFrame]
|
|
328
|
+
def lateral_join(other, on: nil, how: nil)
|
|
329
|
+
check_same_session(other)
|
|
330
|
+
jt = how.nil? ? :JOIN_TYPE_INNER : LATERAL_JOIN_TYPES[normalize_how(how)]
|
|
331
|
+
raise IllegalArgumentError, "Unsupported lateral join type: #{how} (supported: inner, left, leftouter, cross)" unless jt
|
|
332
|
+
|
|
333
|
+
lj = Proto::LateralJoin.new(left: @relation, right: other.relation, join_type: jt)
|
|
334
|
+
lj.join_condition = on.to_expr if on
|
|
335
|
+
build(lateral_join: lj)
|
|
336
|
+
end
|
|
337
|
+
alias lateralJoin lateral_join
|
|
338
|
+
|
|
339
|
+
# Combine the columns of this DataFrame with those of `other` side by side
|
|
340
|
+
# (Spark 4.3+). Both must derive from the same source through
|
|
341
|
+
# projection-only operations (`select`, `with_column`, ...) so that their
|
|
342
|
+
# rows stay aligned; the server rejects anything else.
|
|
343
|
+
#
|
|
344
|
+
# @param other [DataFrame]
|
|
345
|
+
# @return [DataFrame] the columns of `self` followed by those of `other`.
|
|
346
|
+
def zip(other)
|
|
347
|
+
check_same_session(other)
|
|
348
|
+
build(zip: Proto::Zip.new(left: @relation, right: other.relation))
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# For each row of this DataFrame (the query side), join up to
|
|
352
|
+
# `num_results` rows of `other` (the base side) ranked by
|
|
353
|
+
# `ranking_expression` (Spark 4.2+).
|
|
354
|
+
#
|
|
355
|
+
# @param other [DataFrame]
|
|
356
|
+
# @param ranking_expression [Column] a scalar ranking expression.
|
|
357
|
+
# @param num_results [Integer] matches per row, 1..100000.
|
|
358
|
+
# @param mode [String] `"approx"` or `"exact"`.
|
|
359
|
+
# @param direction [String] `"distance"` (smallest first) or `"similarity"` (largest first).
|
|
360
|
+
# @param join_type [String] `"inner"` (default) or `"leftouter"`.
|
|
361
|
+
# @raise [AnalysisError] for invalid arguments (`NEAREST_BY_JOIN.*` error classes).
|
|
362
|
+
# @return [DataFrame]
|
|
363
|
+
def nearest_by_join(other, ranking_expression, num_results, mode, direction, join_type: "inner")
|
|
364
|
+
check_same_session(other)
|
|
365
|
+
validate_nearest_by_join(num_results, join_type.to_s, mode.to_s, direction.to_s)
|
|
366
|
+
nbj = Proto::NearestByJoin.new(
|
|
367
|
+
left: @relation, right: other.relation, ranking_expression: Column.to_col(ranking_expression).to_expr,
|
|
368
|
+
num_results: Integer(num_results), join_type: join_type.to_s, mode: mode.to_s, direction: direction.to_s
|
|
369
|
+
)
|
|
370
|
+
build(nearest_by_join: nbj)
|
|
371
|
+
end
|
|
372
|
+
alias nearestByJoin nearest_by_join
|
|
373
|
+
|
|
274
374
|
# Cartesian product with another DataFrame.
|
|
275
375
|
# @return [DataFrame]
|
|
276
376
|
def cross_join(other)
|
|
@@ -311,7 +411,7 @@ module SparkConnect
|
|
|
311
411
|
end
|
|
312
412
|
alias intersectAll intersect_all
|
|
313
413
|
|
|
314
|
-
# Rows in this DataFrame not in `other`
|
|
414
|
+
# Rows in this DataFrame not in `other`, keeping duplicates - Spark's `EXCEPT ALL`.
|
|
315
415
|
# @return [DataFrame]
|
|
316
416
|
def except_all(other)
|
|
317
417
|
set_op(other, :SET_OP_TYPE_EXCEPT, is_all: true)
|
|
@@ -342,6 +442,25 @@ module SparkConnect
|
|
|
342
442
|
end
|
|
343
443
|
end
|
|
344
444
|
|
|
445
|
+
# Repartition into `num_partitions` sending each row directly to the
|
|
446
|
+
# partition given by the integer column `partition_id_col` (Spark 4.1+).
|
|
447
|
+
#
|
|
448
|
+
# @param num_partitions [Integer]
|
|
449
|
+
# @param partition_id_col [Column, String]
|
|
450
|
+
# @return [DataFrame]
|
|
451
|
+
def repartition_by_id(num_partitions, partition_id_col)
|
|
452
|
+
unless num_partitions.is_a?(Integer) && num_partitions.positive?
|
|
453
|
+
raise IllegalArgumentError, "num_partitions must be a positive Integer, got #{num_partitions.inspect}"
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
pid = Functions._col(partition_id_col).to_expr
|
|
457
|
+
expr = Proto::Expression.new(direct_shuffle_partition_id: Proto::Expression::DirectShufflePartitionID.new(child: pid))
|
|
458
|
+
build(repartition_by_expression: Proto::RepartitionByExpression.new(
|
|
459
|
+
input: @relation, partition_exprs: [expr], num_partitions: num_partitions
|
|
460
|
+
))
|
|
461
|
+
end
|
|
462
|
+
alias repartitionById repartition_by_id
|
|
463
|
+
|
|
345
464
|
# Reduce to `num_partitions` without a full shuffle.
|
|
346
465
|
# @return [DataFrame]
|
|
347
466
|
def coalesce(num_partitions)
|
|
@@ -381,13 +500,34 @@ module SparkConnect
|
|
|
381
500
|
# @param seed [Integer, nil]
|
|
382
501
|
# @return [DataFrame]
|
|
383
502
|
def sample(fraction, with_replacement: false, seed: nil)
|
|
503
|
+
# Like PySpark, pick the seed on the client so the sample is stable across actions.
|
|
384
504
|
s = Proto::Sample.new(
|
|
385
|
-
input: @relation, lower_bound: 0.0, upper_bound: fraction, with_replacement: with_replacement
|
|
505
|
+
input: @relation, lower_bound: 0.0, upper_bound: fraction.to_f, with_replacement: with_replacement,
|
|
506
|
+
seed: seed.nil? ? random_seed : Integer(seed)
|
|
386
507
|
)
|
|
387
|
-
s.seed = seed if seed
|
|
388
508
|
build(sample: s)
|
|
389
509
|
end
|
|
390
510
|
|
|
511
|
+
# Randomly split this DataFrame by `weights` (normalized to sum to 1).
|
|
512
|
+
#
|
|
513
|
+
# @param weights [Array<Numeric>] non-negative weights.
|
|
514
|
+
# @param seed [Integer, nil] random seed (generated when omitted).
|
|
515
|
+
# @return [Array<DataFrame>]
|
|
516
|
+
def random_split(weights, seed = nil)
|
|
517
|
+
raise IllegalArgumentError, "weights must be non-negative, got #{weights.inspect}" if weights.any?(&:negative?)
|
|
518
|
+
|
|
519
|
+
total = weights.sum.to_f
|
|
520
|
+
raise IllegalArgumentError, "the sum of weights must be positive, got #{total}" unless total.positive?
|
|
521
|
+
|
|
522
|
+
seed = seed.nil? ? random_seed : Integer(seed)
|
|
523
|
+
bounds = weights.each_with_object([0.0]) { |w, acc| acc << (acc.last + (w / total)) }
|
|
524
|
+
bounds.each_cons(2).map do |lower, upper|
|
|
525
|
+
build(sample: Proto::Sample.new(input: @relation, lower_bound: lower, upper_bound: upper,
|
|
526
|
+
with_replacement: false, seed: seed, deterministic_order: true))
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
alias randomSplit random_split
|
|
530
|
+
|
|
391
531
|
# ---- Misc transforms ---------------------------------------------------
|
|
392
532
|
|
|
393
533
|
# Alias this DataFrame (a subquery alias usable in join conditions).
|
|
@@ -403,8 +543,9 @@ module SparkConnect
|
|
|
403
543
|
# @param params [Array]
|
|
404
544
|
# @return [DataFrame]
|
|
405
545
|
def hint(name, *params)
|
|
546
|
+
params = params.first if params.size == 1 && params.first.is_a?(Array)
|
|
406
547
|
h = Proto::Hint.new(input: @relation, name: name.to_s,
|
|
407
|
-
parameters: params.map { |p| Column.
|
|
548
|
+
parameters: params.map { |p| Column.lit(p).to_expr })
|
|
408
549
|
build(hint: h)
|
|
409
550
|
end
|
|
410
551
|
|
|
@@ -439,6 +580,45 @@ module SparkConnect
|
|
|
439
580
|
DataFrameStatFunctions.new(self)
|
|
440
581
|
end
|
|
441
582
|
|
|
583
|
+
# Shortcut for {DataFrameNaFunctions#drop} (PySpark's `dropna`).
|
|
584
|
+
# @return [DataFrame]
|
|
585
|
+
def dropna(how: :any, thresh: nil, subset: nil) = na.drop(how: how, thresh: thresh, subset: subset)
|
|
586
|
+
|
|
587
|
+
# Shortcut for {DataFrameNaFunctions#fill} (PySpark's `fillna`).
|
|
588
|
+
# @return [DataFrame]
|
|
589
|
+
def fillna(value, subset: nil) = na.fill(value, subset: subset)
|
|
590
|
+
|
|
591
|
+
# Shortcut for {DataFrameNaFunctions#replace}.
|
|
592
|
+
# @return [DataFrame]
|
|
593
|
+
def replace(to_replace, value = DataFrameNaFunctions::NO_VALUE, subset: nil) = na.replace(to_replace, value, subset: subset)
|
|
594
|
+
|
|
595
|
+
# Shortcut for {DataFrameStatFunctions#approx_quantile}.
|
|
596
|
+
# @return [Array<Float>, Array<Array<Float>>]
|
|
597
|
+
def approx_quantile(cols, probabilities, relative_error) = stat.approx_quantile(cols, probabilities, relative_error)
|
|
598
|
+
alias approxQuantile approx_quantile
|
|
599
|
+
|
|
600
|
+
# Shortcut for {DataFrameStatFunctions#corr}.
|
|
601
|
+
# @return [Float]
|
|
602
|
+
def corr(col1, col2, method = nil) = stat.corr(col1, col2, method)
|
|
603
|
+
|
|
604
|
+
# Shortcut for {DataFrameStatFunctions#cov}.
|
|
605
|
+
# @return [Float]
|
|
606
|
+
def cov(col1, col2) = stat.cov(col1, col2)
|
|
607
|
+
|
|
608
|
+
# Shortcut for {DataFrameStatFunctions#crosstab}.
|
|
609
|
+
# @return [DataFrame]
|
|
610
|
+
def crosstab(col1, col2) = stat.crosstab(col1, col2)
|
|
611
|
+
|
|
612
|
+
# Shortcut for {DataFrameStatFunctions#freq_items}.
|
|
613
|
+
# @return [DataFrame]
|
|
614
|
+
def freq_items(cols, support = nil) = stat.freq_items(cols, support)
|
|
615
|
+
alias freqItems freq_items
|
|
616
|
+
|
|
617
|
+
# Shortcut for {DataFrameStatFunctions#sample_by}.
|
|
618
|
+
# @return [DataFrame]
|
|
619
|
+
def sample_by(col, fractions, seed = nil) = stat.sample_by(col, fractions, seed)
|
|
620
|
+
alias sampleBy sample_by
|
|
621
|
+
|
|
442
622
|
# @return [DataFrameWriter] interface for saving this DataFrame.
|
|
443
623
|
def write
|
|
444
624
|
DataFrameWriter.new(self)
|
|
@@ -457,6 +637,21 @@ module SparkConnect
|
|
|
457
637
|
end
|
|
458
638
|
alias writeStream write_stream
|
|
459
639
|
|
|
640
|
+
# Start a `MERGE INTO table` from this (source) DataFrame.
|
|
641
|
+
#
|
|
642
|
+
# @example
|
|
643
|
+
# source.merge_into("target", F.col("t.id") == F.col("s.id"))
|
|
644
|
+
# .when_matched.update_all
|
|
645
|
+
# .when_not_matched.insert_all
|
|
646
|
+
# .merge
|
|
647
|
+
# @param table [String] the target table.
|
|
648
|
+
# @param condition [Column] the merge condition.
|
|
649
|
+
# @return [MergeIntoWriter]
|
|
650
|
+
def merge_into(table, condition)
|
|
651
|
+
MergeIntoWriter.new(self, table, condition)
|
|
652
|
+
end
|
|
653
|
+
alias mergeInto merge_into
|
|
654
|
+
|
|
460
655
|
# ---- Temporary views ---------------------------------------------------
|
|
461
656
|
|
|
462
657
|
# Register this DataFrame as a session-scoped temporary view, failing if a
|
|
@@ -488,15 +683,83 @@ module SparkConnect
|
|
|
488
683
|
end
|
|
489
684
|
alias createOrReplaceGlobalTempView create_or_replace_global_temp_view
|
|
490
685
|
|
|
686
|
+
# Deprecated alias of {#create_or_replace_temp_view} (kept for PySpark parity).
|
|
687
|
+
# @return [void]
|
|
688
|
+
def register_temp_table(name)
|
|
689
|
+
create_or_replace_temp_view(name)
|
|
690
|
+
end
|
|
691
|
+
alias registerTempTable register_temp_table
|
|
692
|
+
|
|
491
693
|
# Select columns by a regular expression matched against their names.
|
|
492
694
|
#
|
|
493
695
|
# @param regex [String]
|
|
494
696
|
# @return [Column]
|
|
495
697
|
def col_regex(regex)
|
|
496
|
-
|
|
698
|
+
raise IllegalArgumentError, "col_regex expects a String, got #{regex.class}" unless regex.is_a?(String)
|
|
699
|
+
|
|
700
|
+
Column.new(Proto::Expression.new(
|
|
701
|
+
unresolved_regex: Proto::Expression::UnresolvedRegex.new(col_name: regex, plan_id: plan_id)
|
|
702
|
+
))
|
|
497
703
|
end
|
|
498
704
|
alias colRegex col_regex
|
|
499
705
|
|
|
706
|
+
# A column of this DataFrame bound to it by plan id, so it stays
|
|
707
|
+
# unambiguous in self-joins (like `df.col` / `df["col"]` in PySpark).
|
|
708
|
+
#
|
|
709
|
+
# @param name [String]
|
|
710
|
+
# @return [Column]
|
|
711
|
+
def col(name)
|
|
712
|
+
Column.from_name(name.to_s, plan_id: plan_id)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# A metadata column (e.g. `_metadata` of file sources) of this DataFrame.
|
|
716
|
+
#
|
|
717
|
+
# @param name [String]
|
|
718
|
+
# @return [Column]
|
|
719
|
+
def metadata_column(name)
|
|
720
|
+
raise IllegalArgumentError, "metadata_column expects a String, got #{name.class}" unless name.is_a?(String)
|
|
721
|
+
|
|
722
|
+
Column.from_name(name, plan_id: plan_id, is_metadata_column: true)
|
|
723
|
+
end
|
|
724
|
+
alias metadataColumn metadata_column
|
|
725
|
+
|
|
726
|
+
# Attach JSON-serializable `metadata` to the column `column_name`.
|
|
727
|
+
#
|
|
728
|
+
# @param column_name [String]
|
|
729
|
+
# @param metadata [Hash]
|
|
730
|
+
# @return [DataFrame]
|
|
731
|
+
def with_metadata(column_name, metadata)
|
|
732
|
+
raise IllegalArgumentError, "metadata must be a Hash, got #{metadata.class}" unless metadata.is_a?(Hash)
|
|
733
|
+
|
|
734
|
+
a = Proto::Expression::Alias.new(expr: col(column_name.to_s).to_expr, name: [column_name.to_s],
|
|
735
|
+
metadata: JSON.generate(metadata))
|
|
736
|
+
build(with_columns: Proto::WithColumns.new(input: @relation, aliases: [a]))
|
|
737
|
+
end
|
|
738
|
+
alias withMetadata with_metadata
|
|
739
|
+
|
|
740
|
+
# Append a column of consecutive 0-based Long row indices (Spark 4.2+),
|
|
741
|
+
# like `RDD#zipWithIndex`.
|
|
742
|
+
#
|
|
743
|
+
# @param index_col_name [String]
|
|
744
|
+
# @return [DataFrame]
|
|
745
|
+
def zip_with_index(index_col_name = "index")
|
|
746
|
+
seq = Proto::Expression.new(
|
|
747
|
+
unresolved_function: Proto::Expression::UnresolvedFunction.new(function_name: "distributed_sequence_id")
|
|
748
|
+
)
|
|
749
|
+
select(Functions.col("*"), Column.new(seq).alias(index_col_name.to_s))
|
|
750
|
+
end
|
|
751
|
+
alias zipWithIndex zip_with_index
|
|
752
|
+
|
|
753
|
+
# Transpose rows and columns. The `index_column` (the first column when
|
|
754
|
+
# omitted) supplies the new column names.
|
|
755
|
+
#
|
|
756
|
+
# @param index_column [Column, String, nil]
|
|
757
|
+
# @return [DataFrame]
|
|
758
|
+
def transpose(index_column = nil)
|
|
759
|
+
cols = index_column.nil? ? [] : [Functions._col(index_column).to_expr]
|
|
760
|
+
build(transpose: Proto::Transpose.new(input: @relation, index_columns: cols))
|
|
761
|
+
end
|
|
762
|
+
|
|
500
763
|
# @return [DataFrame] a single-column (`value`) DataFrame of each row encoded
|
|
501
764
|
# as a JSON string.
|
|
502
765
|
def to_json(*_args)
|
|
@@ -539,9 +802,10 @@ module SparkConnect
|
|
|
539
802
|
# storage), which is faster but not fault-tolerant.
|
|
540
803
|
#
|
|
541
804
|
# @param eager [Boolean]
|
|
805
|
+
# @param storage_level [StorageLevel, nil] where to keep the checkpointed data.
|
|
542
806
|
# @return [DataFrame]
|
|
543
|
-
def local_checkpoint(eager: true)
|
|
544
|
-
checkpoint_command(local: true, eager: eager)
|
|
807
|
+
def local_checkpoint(eager: true, storage_level: nil)
|
|
808
|
+
checkpoint_command(local: true, eager: eager, storage_level: storage_level)
|
|
545
809
|
end
|
|
546
810
|
alias localCheckpoint local_checkpoint
|
|
547
811
|
|
|
@@ -551,6 +815,9 @@ module SparkConnect
|
|
|
551
815
|
# @param exprs [Array<Column>]
|
|
552
816
|
# @return [DataFrame]
|
|
553
817
|
def observe(name, *exprs)
|
|
818
|
+
raise IllegalArgumentError, "observe requires at least one expression" if exprs.empty?
|
|
819
|
+
raise IllegalArgumentError, "observe expressions must be Columns" unless exprs.all?(Column)
|
|
820
|
+
|
|
554
821
|
obs_name = name.is_a?(Observation) ? name.name : name.to_s
|
|
555
822
|
cm = Proto::CollectMetrics.new(
|
|
556
823
|
input: @relation, name: obs_name, metrics: exprs.map { |e| Column.to_col(e).to_expr }
|
|
@@ -560,6 +827,67 @@ module SparkConnect
|
|
|
560
827
|
df
|
|
561
828
|
end
|
|
562
829
|
|
|
830
|
+
# ---- Subqueries ----------------------------------------------------------
|
|
831
|
+
|
|
832
|
+
# This (single-row, single-column) DataFrame as a scalar subquery
|
|
833
|
+
# {Column}, e.g. `df.filter(F.col("x") > other.select(F.max("x")).scalar)`.
|
|
834
|
+
# Correlated references to the outer query use {Column#outer}.
|
|
835
|
+
# @return [Column]
|
|
836
|
+
def scalar
|
|
837
|
+
subquery_expression(:SUBQUERY_TYPE_SCALAR)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
# An `EXISTS (subquery)` {Column} over this DataFrame.
|
|
841
|
+
# @return [Column]
|
|
842
|
+
def exists
|
|
843
|
+
subquery_expression(:SUBQUERY_TYPE_EXISTS)
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# This DataFrame as a table argument for a table-valued function, with
|
|
847
|
+
# optional partitioning/ordering (see {TableArg}).
|
|
848
|
+
# @return [TableArg]
|
|
849
|
+
def as_table
|
|
850
|
+
TableArg.new(self)
|
|
851
|
+
end
|
|
852
|
+
alias asTable as_table
|
|
853
|
+
|
|
854
|
+
# ---- Caching -------------------------------------------------------------
|
|
855
|
+
|
|
856
|
+
# Persist with the default storage level (`MEMORY_AND_DISK_DESER`).
|
|
857
|
+
# @return [self]
|
|
858
|
+
def cache
|
|
859
|
+
persist
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# Persist this DataFrame with the given storage level.
|
|
863
|
+
# @param storage_level [StorageLevel]
|
|
864
|
+
# @return [self]
|
|
865
|
+
def persist(storage_level = StorageLevel::MEMORY_AND_DISK_DESER)
|
|
866
|
+
analyze(persist: Proto::AnalyzePlanRequest::Persist.new(relation: @relation, storage_level: storage_level.to_proto))
|
|
867
|
+
self
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# Remove this DataFrame from the cache.
|
|
871
|
+
# @param blocking [Boolean] wait until all blocks are deleted.
|
|
872
|
+
# @return [self]
|
|
873
|
+
def unpersist(blocking = false)
|
|
874
|
+
analyze(unpersist: Proto::AnalyzePlanRequest::Unpersist.new(relation: @relation, blocking: blocking))
|
|
875
|
+
self
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
# @return [StorageLevel] the current storage level (`StorageLevel::NONE` when not cached).
|
|
879
|
+
def storage_level
|
|
880
|
+
resp = analyze(get_storage_level: Proto::AnalyzePlanRequest::GetStorageLevel.new(relation: @relation))
|
|
881
|
+
StorageLevel.from_proto(resp.get_storage_level.storage_level)
|
|
882
|
+
end
|
|
883
|
+
alias storageLevel storage_level
|
|
884
|
+
|
|
885
|
+
# @return [Boolean] whether this DataFrame is cached.
|
|
886
|
+
def cached?
|
|
887
|
+
storage_level != StorageLevel::NONE
|
|
888
|
+
end
|
|
889
|
+
alias is_cached cached?
|
|
890
|
+
|
|
563
891
|
# ---- Schema introspection ---------------------------------------------
|
|
564
892
|
|
|
565
893
|
# @return [Types::StructType] the DataFrame's schema.
|
|
@@ -589,21 +917,25 @@ module SparkConnect
|
|
|
589
917
|
end
|
|
590
918
|
alias printSchema print_schema
|
|
591
919
|
|
|
592
|
-
# Index into a column by name (`df["id"]
|
|
920
|
+
# Index into a column by name (`df["id"]`, bound to this DataFrame like
|
|
921
|
+
# {#col}) or position (`df[0]`), filter by a boolean {Column}
|
|
922
|
+
# (`df[df["x"] > 1]`), or select several columns (`df[["a", "b"]]`).
|
|
593
923
|
#
|
|
594
|
-
# @param key [String, Symbol, Integer]
|
|
595
|
-
# @return [Column]
|
|
924
|
+
# @param key [String, Symbol, Integer, Column, Array]
|
|
925
|
+
# @return [Column, DataFrame]
|
|
596
926
|
def [](key)
|
|
597
927
|
case key
|
|
598
928
|
when Integer then Functions.col(columns[key])
|
|
599
|
-
|
|
929
|
+
when Column then filter(key)
|
|
930
|
+
when Array then select(*key)
|
|
931
|
+
else col(key.to_s)
|
|
600
932
|
end
|
|
601
933
|
end
|
|
602
934
|
|
|
603
935
|
# Allows `df.column_name` for valid identifier column names.
|
|
604
936
|
def method_missing(name, *args)
|
|
605
937
|
if args.empty? && columns.include?(name.to_s)
|
|
606
|
-
|
|
938
|
+
col(name.to_s)
|
|
607
939
|
else
|
|
608
940
|
super
|
|
609
941
|
end
|
|
@@ -649,6 +981,11 @@ module SparkConnect
|
|
|
649
981
|
limit(n).collect
|
|
650
982
|
end
|
|
651
983
|
|
|
984
|
+
# @return [Array<Row>] the last `n` rows.
|
|
985
|
+
def tail(n)
|
|
986
|
+
build(tail: Proto::Tail.new(input: @relation, limit: n)).collect
|
|
987
|
+
end
|
|
988
|
+
|
|
652
989
|
# @return [Array<Row>, Row] `n` rows (array) or the single first row when called with no arg.
|
|
653
990
|
def head(n = nil)
|
|
654
991
|
return first if n.nil?
|
|
@@ -678,6 +1015,7 @@ module SparkConnect
|
|
|
678
1015
|
limit(1).collect.empty?
|
|
679
1016
|
end
|
|
680
1017
|
alias is_empty empty?
|
|
1018
|
+
alias isEmpty empty?
|
|
681
1019
|
|
|
682
1020
|
# Render the first `n` rows as a formatted table.
|
|
683
1021
|
#
|
|
@@ -735,16 +1073,21 @@ module SparkConnect
|
|
|
735
1073
|
def input_files
|
|
736
1074
|
analyze(input_files: Proto::AnalyzePlanRequest::InputFiles.new(plan: plan)).input_files.files.to_a
|
|
737
1075
|
end
|
|
1076
|
+
alias inputFiles input_files
|
|
738
1077
|
|
|
739
1078
|
# @return [Boolean] whether the data is small enough to be local.
|
|
740
1079
|
def local?
|
|
741
1080
|
analyze(is_local: Proto::AnalyzePlanRequest::IsLocal.new(plan: plan)).is_local.is_local
|
|
742
1081
|
end
|
|
1082
|
+
alias is_local local?
|
|
1083
|
+
alias isLocal local?
|
|
743
1084
|
|
|
744
1085
|
# @return [Boolean] whether this is a streaming DataFrame.
|
|
745
1086
|
def streaming?
|
|
746
1087
|
analyze(is_streaming: Proto::AnalyzePlanRequest::IsStreaming.new(plan: plan)).is_streaming.is_streaming
|
|
747
1088
|
end
|
|
1089
|
+
alias is_streaming streaming?
|
|
1090
|
+
alias isStreaming streaming?
|
|
748
1091
|
|
|
749
1092
|
# @return [Boolean] whether `other` has the same logical plan.
|
|
750
1093
|
def same_semantics?(other)
|
|
@@ -752,11 +1095,18 @@ module SparkConnect
|
|
|
752
1095
|
target_plan: plan, other_plan: other.plan
|
|
753
1096
|
)).same_semantics.result
|
|
754
1097
|
end
|
|
1098
|
+
alias same_semantics same_semantics?
|
|
1099
|
+
alias sameSemantics same_semantics?
|
|
755
1100
|
|
|
756
1101
|
# @return [Integer] a hash of the logical plan.
|
|
757
1102
|
def semantic_hash
|
|
758
1103
|
analyze(semantic_hash: Proto::AnalyzePlanRequest::SemanticHash.new(plan: plan)).semantic_hash.result
|
|
759
1104
|
end
|
|
1105
|
+
alias semanticHash semantic_hash
|
|
1106
|
+
|
|
1107
|
+
# @return [SparkSession] the session this DataFrame belongs to.
|
|
1108
|
+
def spark_session = @session
|
|
1109
|
+
alias sparkSession spark_session
|
|
760
1110
|
|
|
761
1111
|
# @api private - the executable plan rooted at this relation.
|
|
762
1112
|
# @return [Spark::Connect::Plan]
|
|
@@ -772,7 +1122,23 @@ module SparkConnect
|
|
|
772
1122
|
# @api private - build a derived DataFrame from a relation built by W-owned
|
|
773
1123
|
# facades (GroupedData, NaFunctions, ...).
|
|
774
1124
|
def build(**rel)
|
|
775
|
-
|
|
1125
|
+
relation = PlanBuilder.relation(@session, **rel)
|
|
1126
|
+
DataFrame.new(@session, PlanBuilder.with_subquery_references(@session, relation))
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
# @api private - the plan id of this DataFrame's relation.
|
|
1130
|
+
# @return [Integer]
|
|
1131
|
+
def plan_id
|
|
1132
|
+
@relation.common.plan_id
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
# @api private - a subquery expression referring to this DataFrame.
|
|
1136
|
+
# @return [Column]
|
|
1137
|
+
def subquery_expression(type, in_subquery_values: [], table_arg_options: nil)
|
|
1138
|
+
@session.register_subquery_relation(@relation)
|
|
1139
|
+
sq = Proto::SubqueryExpression.new(plan_id: plan_id, subquery_type: type, in_subquery_values: in_subquery_values)
|
|
1140
|
+
sq.table_arg_options = table_arg_options if table_arg_options
|
|
1141
|
+
Column.new(Proto::Expression.new(subquery_expression: sq))
|
|
776
1142
|
end
|
|
777
1143
|
|
|
778
1144
|
private
|
|
@@ -789,8 +1155,9 @@ module SparkConnect
|
|
|
789
1155
|
nil
|
|
790
1156
|
end
|
|
791
1157
|
|
|
792
|
-
def checkpoint_command(local:, eager:)
|
|
1158
|
+
def checkpoint_command(local:, eager:, storage_level: nil)
|
|
793
1159
|
cmd = Proto::CheckpointCommand.new(relation: @relation, local: local, eager: eager)
|
|
1160
|
+
cmd.storage_level = storage_level.to_proto if storage_level
|
|
794
1161
|
result = @session.client.execute_command(Proto::Command.new(checkpoint_command: cmd))
|
|
795
1162
|
cached = result.checkpoint_relation
|
|
796
1163
|
raise SparkConnectError, "Server did not return a checkpointed relation" unless cached
|
|
@@ -806,6 +1173,61 @@ module SparkConnect
|
|
|
806
1173
|
cols.flatten.map { |c| c.is_a?(Column) ? c : Functions.col(c.to_s) }
|
|
807
1174
|
end
|
|
808
1175
|
|
|
1176
|
+
# Like {#normalize_columns} but an Integer is a 1-based column ordinal
|
|
1177
|
+
# (`group_by(1)`), like PySpark.
|
|
1178
|
+
def grouping_columns(cols)
|
|
1179
|
+
cols.flatten.map do |c|
|
|
1180
|
+
case c
|
|
1181
|
+
when Column then c
|
|
1182
|
+
when Integer
|
|
1183
|
+
raise IllegalArgumentError, "column ordinal must be positive, got #{c}" if c < 1
|
|
1184
|
+
|
|
1185
|
+
self[c - 1]
|
|
1186
|
+
else Functions.col(c.to_s)
|
|
1187
|
+
end
|
|
1188
|
+
end
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
def check_same_session(other)
|
|
1192
|
+
raise IllegalArgumentError, "expected a DataFrame, got #{other.class}" unless other.is_a?(DataFrame)
|
|
1193
|
+
return if other.session.session_id == @session.session_id
|
|
1194
|
+
|
|
1195
|
+
raise IllegalArgumentError, "the DataFrames belong to different SparkSessions"
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
def normalize_how(how)
|
|
1199
|
+
how.to_s.downcase.delete("_").to_sym
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
def join_type(how)
|
|
1203
|
+
return :JOIN_TYPE_INNER if how.nil?
|
|
1204
|
+
|
|
1205
|
+
JOIN_TYPES[normalize_how(how)] or raise IllegalArgumentError, "Unsupported join type: #{how}"
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
def random_seed = Random.rand(2**63)
|
|
1209
|
+
|
|
1210
|
+
def validate_nearest_by_join(num_results, join_type, mode, direction)
|
|
1211
|
+
unless num_results.is_a?(Integer) && num_results.between?(1, NEAREST_BY_JOIN_MAX_NUM_RESULTS)
|
|
1212
|
+
raise AnalysisError.new(
|
|
1213
|
+
"numResults must be between 1 and #{NEAREST_BY_JOIN_MAX_NUM_RESULTS}, got #{num_results}",
|
|
1214
|
+
error_class: "NEAREST_BY_JOIN.NUM_RESULTS_OUT_OF_RANGE"
|
|
1215
|
+
)
|
|
1216
|
+
end
|
|
1217
|
+
unless NEAREST_BY_JOIN_JOIN_TYPES.include?(join_type.downcase.delete("_"))
|
|
1218
|
+
raise AnalysisError.new("Unsupported join type #{join_type}; supported: 'INNER', 'LEFT OUTER'",
|
|
1219
|
+
error_class: "NEAREST_BY_JOIN.UNSUPPORTED_JOIN_TYPE")
|
|
1220
|
+
end
|
|
1221
|
+
unless NEAREST_BY_JOIN_MODES.include?(mode.downcase)
|
|
1222
|
+
raise AnalysisError.new("Unsupported mode #{mode}; supported: 'approx', 'exact'",
|
|
1223
|
+
error_class: "NEAREST_BY_JOIN.UNSUPPORTED_MODE")
|
|
1224
|
+
end
|
|
1225
|
+
return if NEAREST_BY_JOIN_DIRECTIONS.include?(direction.downcase)
|
|
1226
|
+
|
|
1227
|
+
raise AnalysisError.new("Unsupported direction #{direction}; supported: 'distance', 'similarity'",
|
|
1228
|
+
error_class: "NEAREST_BY_JOIN.UNSUPPORTED_DIRECTION")
|
|
1229
|
+
end
|
|
1230
|
+
|
|
809
1231
|
def to_sort_order(col)
|
|
810
1232
|
expr = col.to_expr
|
|
811
1233
|
if expr.expr_type == :sort_order
|