spark-connect 0.3.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 +144 -27
- data/README.md +29 -11
- data/lib/spark_connect/catalog.rb +157 -12
- data/lib/spark_connect/channel_builder.rb +119 -12
- data/lib/spark_connect/client.rb +233 -17
- data/lib/spark_connect/column.rb +143 -10
- data/lib/spark_connect/conf.rb +16 -0
- data/lib/spark_connect/data_frame.rb +428 -23
- data/lib/spark_connect/functions.rb +1947 -493
- 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 +6 -2
|
@@ -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.
|
|
@@ -241,7 +252,7 @@ module SparkConnect
|
|
|
241
252
|
# @param cols [Array<Column, String>]
|
|
242
253
|
# @return [GroupedData]
|
|
243
254
|
def group_by(*cols)
|
|
244
|
-
GroupedData.new(self,
|
|
255
|
+
GroupedData.new(self, grouping_columns(cols), :GROUP_TYPE_GROUPBY)
|
|
245
256
|
end
|
|
246
257
|
alias groupBy group_by
|
|
247
258
|
alias groupby group_by
|
|
@@ -249,15 +260,28 @@ module SparkConnect
|
|
|
249
260
|
# Multi-dimensional rollup.
|
|
250
261
|
# @return [GroupedData]
|
|
251
262
|
def rollup(*cols)
|
|
252
|
-
GroupedData.new(self,
|
|
263
|
+
GroupedData.new(self, grouping_columns(cols), :GROUP_TYPE_ROLLUP)
|
|
253
264
|
end
|
|
254
265
|
|
|
255
266
|
# Multi-dimensional cube.
|
|
256
267
|
# @return [GroupedData]
|
|
257
268
|
def cube(*cols)
|
|
258
|
-
GroupedData.new(self,
|
|
269
|
+
GroupedData.new(self, grouping_columns(cols), :GROUP_TYPE_CUBE)
|
|
259
270
|
end
|
|
260
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
|
+
|
|
261
285
|
# Aggregate over the whole DataFrame (a group-by with no grouping columns).
|
|
262
286
|
#
|
|
263
287
|
# @param exprs [Array<Column>, Hash]
|
|
@@ -271,23 +295,82 @@ module SparkConnect
|
|
|
271
295
|
# Join with another DataFrame.
|
|
272
296
|
#
|
|
273
297
|
# @param other [DataFrame]
|
|
274
|
-
# @param on [String, Array<String>, Column, nil] join key
|
|
275
|
-
# boolean join condition
|
|
276
|
-
#
|
|
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"`).
|
|
277
303
|
# @return [DataFrame]
|
|
278
304
|
def join(other, on: nil, how: :inner)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
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))
|
|
282
307
|
case on
|
|
283
308
|
when nil then nil
|
|
284
309
|
when Column then j.join_condition = on.to_expr
|
|
285
|
-
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
|
|
286
316
|
else j.using_columns << on.to_s
|
|
287
317
|
end
|
|
288
318
|
build(join: j)
|
|
289
319
|
end
|
|
290
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
|
+
|
|
291
374
|
# Cartesian product with another DataFrame.
|
|
292
375
|
# @return [DataFrame]
|
|
293
376
|
def cross_join(other)
|
|
@@ -359,6 +442,25 @@ module SparkConnect
|
|
|
359
442
|
end
|
|
360
443
|
end
|
|
361
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
|
+
|
|
362
464
|
# Reduce to `num_partitions` without a full shuffle.
|
|
363
465
|
# @return [DataFrame]
|
|
364
466
|
def coalesce(num_partitions)
|
|
@@ -398,13 +500,34 @@ module SparkConnect
|
|
|
398
500
|
# @param seed [Integer, nil]
|
|
399
501
|
# @return [DataFrame]
|
|
400
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.
|
|
401
504
|
s = Proto::Sample.new(
|
|
402
|
-
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)
|
|
403
507
|
)
|
|
404
|
-
s.seed = seed if seed
|
|
405
508
|
build(sample: s)
|
|
406
509
|
end
|
|
407
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
|
+
|
|
408
531
|
# ---- Misc transforms ---------------------------------------------------
|
|
409
532
|
|
|
410
533
|
# Alias this DataFrame (a subquery alias usable in join conditions).
|
|
@@ -420,8 +543,9 @@ module SparkConnect
|
|
|
420
543
|
# @param params [Array]
|
|
421
544
|
# @return [DataFrame]
|
|
422
545
|
def hint(name, *params)
|
|
546
|
+
params = params.first if params.size == 1 && params.first.is_a?(Array)
|
|
423
547
|
h = Proto::Hint.new(input: @relation, name: name.to_s,
|
|
424
|
-
parameters: params.map { |p| Column.
|
|
548
|
+
parameters: params.map { |p| Column.lit(p).to_expr })
|
|
425
549
|
build(hint: h)
|
|
426
550
|
end
|
|
427
551
|
|
|
@@ -456,6 +580,45 @@ module SparkConnect
|
|
|
456
580
|
DataFrameStatFunctions.new(self)
|
|
457
581
|
end
|
|
458
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
|
+
|
|
459
622
|
# @return [DataFrameWriter] interface for saving this DataFrame.
|
|
460
623
|
def write
|
|
461
624
|
DataFrameWriter.new(self)
|
|
@@ -474,6 +637,21 @@ module SparkConnect
|
|
|
474
637
|
end
|
|
475
638
|
alias writeStream write_stream
|
|
476
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
|
+
|
|
477
655
|
# ---- Temporary views ---------------------------------------------------
|
|
478
656
|
|
|
479
657
|
# Register this DataFrame as a session-scoped temporary view, failing if a
|
|
@@ -505,15 +683,83 @@ module SparkConnect
|
|
|
505
683
|
end
|
|
506
684
|
alias createOrReplaceGlobalTempView create_or_replace_global_temp_view
|
|
507
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
|
+
|
|
508
693
|
# Select columns by a regular expression matched against their names.
|
|
509
694
|
#
|
|
510
695
|
# @param regex [String]
|
|
511
696
|
# @return [Column]
|
|
512
697
|
def col_regex(regex)
|
|
513
|
-
|
|
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
|
+
))
|
|
514
703
|
end
|
|
515
704
|
alias colRegex col_regex
|
|
516
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
|
+
|
|
517
763
|
# @return [DataFrame] a single-column (`value`) DataFrame of each row encoded
|
|
518
764
|
# as a JSON string.
|
|
519
765
|
def to_json(*_args)
|
|
@@ -556,9 +802,10 @@ module SparkConnect
|
|
|
556
802
|
# storage), which is faster but not fault-tolerant.
|
|
557
803
|
#
|
|
558
804
|
# @param eager [Boolean]
|
|
805
|
+
# @param storage_level [StorageLevel, nil] where to keep the checkpointed data.
|
|
559
806
|
# @return [DataFrame]
|
|
560
|
-
def local_checkpoint(eager: true)
|
|
561
|
-
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)
|
|
562
809
|
end
|
|
563
810
|
alias localCheckpoint local_checkpoint
|
|
564
811
|
|
|
@@ -568,6 +815,9 @@ module SparkConnect
|
|
|
568
815
|
# @param exprs [Array<Column>]
|
|
569
816
|
# @return [DataFrame]
|
|
570
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
|
+
|
|
571
821
|
obs_name = name.is_a?(Observation) ? name.name : name.to_s
|
|
572
822
|
cm = Proto::CollectMetrics.new(
|
|
573
823
|
input: @relation, name: obs_name, metrics: exprs.map { |e| Column.to_col(e).to_expr }
|
|
@@ -577,6 +827,67 @@ module SparkConnect
|
|
|
577
827
|
df
|
|
578
828
|
end
|
|
579
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
|
+
|
|
580
891
|
# ---- Schema introspection ---------------------------------------------
|
|
581
892
|
|
|
582
893
|
# @return [Types::StructType] the DataFrame's schema.
|
|
@@ -606,21 +917,25 @@ module SparkConnect
|
|
|
606
917
|
end
|
|
607
918
|
alias printSchema print_schema
|
|
608
919
|
|
|
609
|
-
# 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"]]`).
|
|
610
923
|
#
|
|
611
|
-
# @param key [String, Symbol, Integer]
|
|
612
|
-
# @return [Column]
|
|
924
|
+
# @param key [String, Symbol, Integer, Column, Array]
|
|
925
|
+
# @return [Column, DataFrame]
|
|
613
926
|
def [](key)
|
|
614
927
|
case key
|
|
615
928
|
when Integer then Functions.col(columns[key])
|
|
616
|
-
|
|
929
|
+
when Column then filter(key)
|
|
930
|
+
when Array then select(*key)
|
|
931
|
+
else col(key.to_s)
|
|
617
932
|
end
|
|
618
933
|
end
|
|
619
934
|
|
|
620
935
|
# Allows `df.column_name` for valid identifier column names.
|
|
621
936
|
def method_missing(name, *args)
|
|
622
937
|
if args.empty? && columns.include?(name.to_s)
|
|
623
|
-
|
|
938
|
+
col(name.to_s)
|
|
624
939
|
else
|
|
625
940
|
super
|
|
626
941
|
end
|
|
@@ -666,6 +981,11 @@ module SparkConnect
|
|
|
666
981
|
limit(n).collect
|
|
667
982
|
end
|
|
668
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
|
+
|
|
669
989
|
# @return [Array<Row>, Row] `n` rows (array) or the single first row when called with no arg.
|
|
670
990
|
def head(n = nil)
|
|
671
991
|
return first if n.nil?
|
|
@@ -695,6 +1015,7 @@ module SparkConnect
|
|
|
695
1015
|
limit(1).collect.empty?
|
|
696
1016
|
end
|
|
697
1017
|
alias is_empty empty?
|
|
1018
|
+
alias isEmpty empty?
|
|
698
1019
|
|
|
699
1020
|
# Render the first `n` rows as a formatted table.
|
|
700
1021
|
#
|
|
@@ -752,16 +1073,21 @@ module SparkConnect
|
|
|
752
1073
|
def input_files
|
|
753
1074
|
analyze(input_files: Proto::AnalyzePlanRequest::InputFiles.new(plan: plan)).input_files.files.to_a
|
|
754
1075
|
end
|
|
1076
|
+
alias inputFiles input_files
|
|
755
1077
|
|
|
756
1078
|
# @return [Boolean] whether the data is small enough to be local.
|
|
757
1079
|
def local?
|
|
758
1080
|
analyze(is_local: Proto::AnalyzePlanRequest::IsLocal.new(plan: plan)).is_local.is_local
|
|
759
1081
|
end
|
|
1082
|
+
alias is_local local?
|
|
1083
|
+
alias isLocal local?
|
|
760
1084
|
|
|
761
1085
|
# @return [Boolean] whether this is a streaming DataFrame.
|
|
762
1086
|
def streaming?
|
|
763
1087
|
analyze(is_streaming: Proto::AnalyzePlanRequest::IsStreaming.new(plan: plan)).is_streaming.is_streaming
|
|
764
1088
|
end
|
|
1089
|
+
alias is_streaming streaming?
|
|
1090
|
+
alias isStreaming streaming?
|
|
765
1091
|
|
|
766
1092
|
# @return [Boolean] whether `other` has the same logical plan.
|
|
767
1093
|
def same_semantics?(other)
|
|
@@ -769,11 +1095,18 @@ module SparkConnect
|
|
|
769
1095
|
target_plan: plan, other_plan: other.plan
|
|
770
1096
|
)).same_semantics.result
|
|
771
1097
|
end
|
|
1098
|
+
alias same_semantics same_semantics?
|
|
1099
|
+
alias sameSemantics same_semantics?
|
|
772
1100
|
|
|
773
1101
|
# @return [Integer] a hash of the logical plan.
|
|
774
1102
|
def semantic_hash
|
|
775
1103
|
analyze(semantic_hash: Proto::AnalyzePlanRequest::SemanticHash.new(plan: plan)).semantic_hash.result
|
|
776
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
|
|
777
1110
|
|
|
778
1111
|
# @api private - the executable plan rooted at this relation.
|
|
779
1112
|
# @return [Spark::Connect::Plan]
|
|
@@ -789,7 +1122,23 @@ module SparkConnect
|
|
|
789
1122
|
# @api private - build a derived DataFrame from a relation built by W-owned
|
|
790
1123
|
# facades (GroupedData, NaFunctions, ...).
|
|
791
1124
|
def build(**rel)
|
|
792
|
-
|
|
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))
|
|
793
1142
|
end
|
|
794
1143
|
|
|
795
1144
|
private
|
|
@@ -806,8 +1155,9 @@ module SparkConnect
|
|
|
806
1155
|
nil
|
|
807
1156
|
end
|
|
808
1157
|
|
|
809
|
-
def checkpoint_command(local:, eager:)
|
|
1158
|
+
def checkpoint_command(local:, eager:, storage_level: nil)
|
|
810
1159
|
cmd = Proto::CheckpointCommand.new(relation: @relation, local: local, eager: eager)
|
|
1160
|
+
cmd.storage_level = storage_level.to_proto if storage_level
|
|
811
1161
|
result = @session.client.execute_command(Proto::Command.new(checkpoint_command: cmd))
|
|
812
1162
|
cached = result.checkpoint_relation
|
|
813
1163
|
raise SparkConnectError, "Server did not return a checkpointed relation" unless cached
|
|
@@ -823,6 +1173,61 @@ module SparkConnect
|
|
|
823
1173
|
cols.flatten.map { |c| c.is_a?(Column) ? c : Functions.col(c.to_s) }
|
|
824
1174
|
end
|
|
825
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
|
+
|
|
826
1231
|
def to_sort_order(col)
|
|
827
1232
|
expr = col.to_expr
|
|
828
1233
|
if expr.expr_type == :sort_order
|