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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SparkConnect
|
|
4
|
+
# Built-in table-valued functions, returned by {SparkSession#tvf}. Mirrors
|
|
5
|
+
# PySpark's `spark.tvf` (`TableValuedFunction`). Arguments are {Column}s or
|
|
6
|
+
# column names; wrap literals with {Functions.lit}.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# spark.tvf.explode(F.array(F.lit(1), F.lit(2))).show
|
|
10
|
+
# spark.tvf.sql_keywords.limit(5).collect
|
|
11
|
+
class TableValuedFunction
|
|
12
|
+
Proto = SparkConnect::Proto
|
|
13
|
+
|
|
14
|
+
# @param session [SparkSession]
|
|
15
|
+
def initialize(session)
|
|
16
|
+
@session = session
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Same as {SparkSession#range}.
|
|
20
|
+
# @return [DataFrame]
|
|
21
|
+
def range(start, end_ = nil, step = 1, num_partitions = nil)
|
|
22
|
+
@session.range(start, end_, step, num_partitions)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
%w[explode explode_outer inline inline_outer posexplode posexplode_outer variant_explode
|
|
26
|
+
variant_explode_outer].each do |name|
|
|
27
|
+
define_method(name) { |input| call(name, input) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @!method explode(collection)
|
|
31
|
+
# One row per element of an array (or entry of a map). @return [DataFrame]
|
|
32
|
+
# @!method explode_outer(collection)
|
|
33
|
+
# Like {#explode}, but a null/empty input yields one row of nulls. @return [DataFrame]
|
|
34
|
+
# @!method inline(input)
|
|
35
|
+
# One row per struct of an array of structs. @return [DataFrame]
|
|
36
|
+
# @!method inline_outer(input)
|
|
37
|
+
# Like {#inline}, keeping null/empty inputs. @return [DataFrame]
|
|
38
|
+
# @!method posexplode(collection)
|
|
39
|
+
# Like {#explode} with the element position. @return [DataFrame]
|
|
40
|
+
# @!method posexplode_outer(collection)
|
|
41
|
+
# Like {#posexplode}, keeping null/empty inputs. @return [DataFrame]
|
|
42
|
+
# @!method variant_explode(input)
|
|
43
|
+
# One row per element/field of a VARIANT array/object. @return [DataFrame]
|
|
44
|
+
# @!method variant_explode_outer(input)
|
|
45
|
+
# Like {#variant_explode}, keeping null/empty inputs. @return [DataFrame]
|
|
46
|
+
|
|
47
|
+
# The given fields of a JSON string, one column each.
|
|
48
|
+
# @return [DataFrame]
|
|
49
|
+
def json_tuple(input, *fields)
|
|
50
|
+
raise IllegalArgumentError, "json_tuple requires at least one field" if fields.empty?
|
|
51
|
+
|
|
52
|
+
call("json_tuple", input, *fields)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Separate `fields` into `n` rows.
|
|
56
|
+
# @return [DataFrame]
|
|
57
|
+
def stack(n, *fields) = call("stack", n, *fields)
|
|
58
|
+
|
|
59
|
+
# @return [DataFrame] the collations supported by the server.
|
|
60
|
+
def collations = call("collations")
|
|
61
|
+
|
|
62
|
+
# @return [DataFrame] the SQL keywords of the server.
|
|
63
|
+
def sql_keywords = call("sql_keywords")
|
|
64
|
+
|
|
65
|
+
# @return [DataFrame] the Python worker logs (when enabled on the server).
|
|
66
|
+
def python_worker_logs = call("python_worker_logs")
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def call(name, *args)
|
|
71
|
+
tvf = Proto::UnresolvedTableValuedFunction.new(
|
|
72
|
+
function_name: name, arguments: args.map { |a| Functions._col(a).to_expr }
|
|
73
|
+
)
|
|
74
|
+
relation = PlanBuilder.relation(@session, unresolved_table_valued_function: tvf)
|
|
75
|
+
DataFrame.new(@session, PlanBuilder.with_subquery_references(@session, relation))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/spark_connect/types.rb
CHANGED
|
@@ -149,6 +149,104 @@ module SparkConnect
|
|
|
149
149
|
def to_proto = Types.wrap(timestamp_ntz: Proto::DataType::TimestampNTZ.new)
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
+
# Shared behaviour of the nanosecond-capable timestamp types (Spark 4.3+,
|
|
153
|
+
# a preview behind `spark.sql.timestampNanosTypes.enabled`).
|
|
154
|
+
class TimestampNanosBase < DataType
|
|
155
|
+
MIN_PRECISION = 7
|
|
156
|
+
MAX_PRECISION = 9
|
|
157
|
+
|
|
158
|
+
# @return [Integer] fractional-second digits, 7..9 (9 = nanoseconds).
|
|
159
|
+
attr_reader :precision
|
|
160
|
+
|
|
161
|
+
def initialize(precision = MAX_PRECISION)
|
|
162
|
+
super()
|
|
163
|
+
unless precision.is_a?(Integer) && precision.between?(MIN_PRECISION, MAX_PRECISION)
|
|
164
|
+
raise IllegalArgumentError, "#{self.class.name.split('::').last} precision must be 7, 8, or 9, got #{precision.inspect}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
@precision = precision
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def simple_string = "#{type_name}(#{precision})"
|
|
171
|
+
def json_value = simple_string
|
|
172
|
+
def ==(other) = other.instance_of?(self.class) && other.precision == precision
|
|
173
|
+
def hash = [self.class, precision].hash
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# TIMESTAMP_NTZ(p): a zone-less local date-time with nanosecond-capable precision.
|
|
177
|
+
class TimestampNTZNanosType < TimestampNanosBase
|
|
178
|
+
def type_name = "timestamp_ntz"
|
|
179
|
+
def to_proto = Types.wrap(timestamp_ntz_nanos: Proto::DataType::TimestampNTZNanos.new(precision: precision))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# TIMESTAMP_LTZ(p): an instant with nanosecond-capable precision.
|
|
183
|
+
class TimestampLTZNanosType < TimestampNanosBase
|
|
184
|
+
def type_name = "timestamp_ltz"
|
|
185
|
+
def to_proto = Types.wrap(timestamp_ltz_nanos: Proto::DataType::TimestampLTZNanos.new(precision: precision))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# TIME(p): a time of day without a date or time zone (Spark 4.1+).
|
|
189
|
+
class TimeType < DataType
|
|
190
|
+
# @return [Integer] fractional-second digits, 0..6.
|
|
191
|
+
attr_reader :precision
|
|
192
|
+
|
|
193
|
+
def initialize(precision = 6)
|
|
194
|
+
super()
|
|
195
|
+
@precision = precision
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def simple_string = "time(#{precision})"
|
|
199
|
+
def type_name = "time"
|
|
200
|
+
def json_value = simple_string
|
|
201
|
+
def to_proto = Types.wrap(time: Proto::DataType::Time.new(precision: precision))
|
|
202
|
+
def ==(other) = other.is_a?(TimeType) && other.precision == precision
|
|
203
|
+
def hash = [TimeType, precision].hash
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Shared behaviour of the spatial types (Spark 4.1+). An SRID of
|
|
207
|
+
# {MIXED_SRID} (`"ANY"`) allows values of any reference system.
|
|
208
|
+
class SpatialBase < DataType
|
|
209
|
+
MIXED_SRID = -1
|
|
210
|
+
|
|
211
|
+
# @return [Integer]
|
|
212
|
+
attr_reader :srid
|
|
213
|
+
|
|
214
|
+
def initialize(srid)
|
|
215
|
+
super()
|
|
216
|
+
@srid = srid.to_s.casecmp?("any") ? MIXED_SRID : Integer(srid)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def simple_string = srid == MIXED_SRID ? "#{type_name}(any)" : "#{type_name}(#{srid})"
|
|
220
|
+
def ==(other) = other.instance_of?(self.class) && other.srid == srid
|
|
221
|
+
def hash = [self.class, srid].hash
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
# The coordinate reference system id used by Spark's JSON schema form.
|
|
226
|
+
def crs
|
|
227
|
+
case srid
|
|
228
|
+
when MIXED_SRID then "SRID:ANY"
|
|
229
|
+
when 4326 then "OGC:CRS84"
|
|
230
|
+
when 0 then "SRID:0"
|
|
231
|
+
else "EPSG:#{srid}"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# GEOMETRY(srid): a planar (Cartesian) spatial value.
|
|
237
|
+
class GeometryType < SpatialBase
|
|
238
|
+
def type_name = "geometry"
|
|
239
|
+
def json_value = "geometry(#{crs})"
|
|
240
|
+
def to_proto = Types.wrap(geometry: Proto::DataType::Geometry.new(srid: srid))
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# GEOGRAPHY(srid): a geodesic (spherical) spatial value.
|
|
244
|
+
class GeographyType < SpatialBase
|
|
245
|
+
def type_name = "geography"
|
|
246
|
+
def json_value = "geography(#{crs}, SPHERICAL)"
|
|
247
|
+
def to_proto = Types.wrap(geography: Proto::DataType::Geography.new(srid: srid))
|
|
248
|
+
end
|
|
249
|
+
|
|
152
250
|
class VariantType < DataType
|
|
153
251
|
def type_name = "variant"
|
|
154
252
|
def to_proto = Types.wrap(variant: Proto::DataType::Variant.new)
|
|
@@ -254,6 +352,33 @@ module SparkConnect
|
|
|
254
352
|
def to_proto = Types.wrap(calendar_interval: Proto::DataType::CalendarInterval.new)
|
|
255
353
|
end
|
|
256
354
|
|
|
355
|
+
# A user-defined type, described by its class name(s) and the SQL type it
|
|
356
|
+
# is stored as. Values decode as their `sql_type`.
|
|
357
|
+
class UserDefinedType < DataType
|
|
358
|
+
attr_reader :jvm_class, :python_class, :sql_type
|
|
359
|
+
|
|
360
|
+
def initialize(sql_type, jvm_class: nil, python_class: nil)
|
|
361
|
+
super()
|
|
362
|
+
@sql_type = sql_type
|
|
363
|
+
@jvm_class = jvm_class
|
|
364
|
+
@python_class = python_class
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def simple_string = sql_type ? sql_type.simple_string : "udt"
|
|
368
|
+
def type_name = "udt"
|
|
369
|
+
|
|
370
|
+
def to_proto
|
|
371
|
+
udt = Proto::DataType::UDT.new(type: "udt")
|
|
372
|
+
udt.jvm_class = jvm_class if jvm_class
|
|
373
|
+
udt.python_class = python_class if python_class
|
|
374
|
+
udt.sql_type = sql_type.to_proto if sql_type
|
|
375
|
+
Types.wrap(udt: udt)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def ==(other) = other.is_a?(UserDefinedType) && other.sql_type == sql_type && other.jvm_class == jvm_class
|
|
379
|
+
def hash = [UserDefinedType, sql_type, jvm_class].hash
|
|
380
|
+
end
|
|
381
|
+
|
|
257
382
|
# An array type. `element_type` is the type of every element; `contains_null`
|
|
258
383
|
# indicates whether the array may contain `null` values.
|
|
259
384
|
class ArrayType < DataType
|
|
@@ -440,6 +565,21 @@ module SparkConnect
|
|
|
440
565
|
def date = DateType.new
|
|
441
566
|
def timestamp = TimestampType.new
|
|
442
567
|
def timestamp_ntz = TimestampNTZType.new
|
|
568
|
+
# @param precision [Integer] fractional-second digits, 7..9 (Spark 4.3 preview).
|
|
569
|
+
# @return [TimestampNTZNanosType]
|
|
570
|
+
def timestamp_ntz_nanos(precision = 9) = TimestampNTZNanosType.new(precision)
|
|
571
|
+
# @param precision [Integer] fractional-second digits, 7..9 (Spark 4.3 preview).
|
|
572
|
+
# @return [TimestampLTZNanosType]
|
|
573
|
+
def timestamp_ltz_nanos(precision = 9) = TimestampLTZNanosType.new(precision)
|
|
574
|
+
# @param precision [Integer] fractional-second digits, 0..6 (Spark 4.1+).
|
|
575
|
+
# @return [TimeType]
|
|
576
|
+
def time(precision = 6) = TimeType.new(precision)
|
|
577
|
+
# @param srid [Integer, String] spatial reference id, or `"ANY"` (Spark 4.1+).
|
|
578
|
+
# @return [GeometryType]
|
|
579
|
+
def geometry(srid = 4326) = GeometryType.new(srid)
|
|
580
|
+
# @param srid [Integer, String] spatial reference id, or `"ANY"` (Spark 4.1+).
|
|
581
|
+
# @return [GeographyType]
|
|
582
|
+
def geography(srid = 4326) = GeographyType.new(srid)
|
|
443
583
|
def variant = VariantType.new
|
|
444
584
|
def decimal(precision = 10, scale = 0) = DecimalType.new(precision, scale)
|
|
445
585
|
def array(element_type, contains_null: true) = ArrayType.new(element_type, contains_null: contains_null)
|
|
@@ -468,6 +608,15 @@ module SparkConnect
|
|
|
468
608
|
when :date then DateType.new
|
|
469
609
|
when :timestamp then TimestampType.new
|
|
470
610
|
when :timestamp_ntz then TimestampNTZType.new
|
|
611
|
+
when :timestamp_ntz_nanos then TimestampNTZNanosType.new(sub.has_precision? ? sub.precision : 9)
|
|
612
|
+
when :timestamp_ltz_nanos then TimestampLTZNanosType.new(sub.has_precision? ? sub.precision : 9)
|
|
613
|
+
when :time then TimeType.new(sub.has_precision? ? sub.precision : 6)
|
|
614
|
+
when :geometry then GeometryType.new(sub.srid)
|
|
615
|
+
when :geography then GeographyType.new(sub.srid)
|
|
616
|
+
when :udt
|
|
617
|
+
UserDefinedType.new(sub.has_sql_type? ? from_proto(sub.sql_type) : nil,
|
|
618
|
+
jvm_class: sub.has_jvm_class? ? sub.jvm_class : nil,
|
|
619
|
+
python_class: sub.has_python_class? ? sub.python_class : nil)
|
|
471
620
|
when :variant then VariantType.new
|
|
472
621
|
when :calendar_interval then CalendarIntervalType.new
|
|
473
622
|
when :day_time_interval then DayTimeIntervalType.new(sub.start_field || 0, sub.end_field || 3)
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module SparkConnect
|
|
4
4
|
# The released version of the spark-connect gem.
|
|
5
|
-
VERSION = "0.
|
|
5
|
+
VERSION = "0.4.0"
|
|
6
6
|
|
|
7
7
|
# The Apache Spark version whose Spark Connect protocol definitions this
|
|
8
8
|
# client is generated against. The client aims to be wire-compatible with
|
|
9
9
|
# Spark Connect servers of this major/minor line and newer.
|
|
10
|
-
SPARK_VERSION = "4.
|
|
10
|
+
SPARK_VERSION = "4.3.0"
|
|
11
11
|
end
|
data/lib/spark_connect/writer.rb
CHANGED
|
@@ -31,6 +31,8 @@ module SparkConnect
|
|
|
31
31
|
@sort_columns = []
|
|
32
32
|
@bucket_cols = nil
|
|
33
33
|
@num_buckets = nil
|
|
34
|
+
@clustering_columns = []
|
|
35
|
+
@schema_evolution = false
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
# @return [self] set the output format.
|
|
@@ -40,25 +42,48 @@ module SparkConnect
|
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
# @return [self] set the save mode (`:append`, `:overwrite`, `:ignore`,
|
|
43
|
-
# `:error`).
|
|
45
|
+
# `:error`). `nil` keeps the current mode.
|
|
44
46
|
def mode(save_mode)
|
|
47
|
+
return self if save_mode.nil?
|
|
48
|
+
|
|
45
49
|
@mode = SAVE_MODES.fetch(save_mode.to_s.downcase.to_sym) do
|
|
46
50
|
raise IllegalArgumentError, "Unknown save mode: #{save_mode}"
|
|
47
51
|
end
|
|
48
52
|
self
|
|
49
53
|
end
|
|
50
54
|
|
|
51
|
-
# @return [self] set a write option.
|
|
55
|
+
# @return [self] set a write option (a `nil` value is ignored).
|
|
52
56
|
def option(key, value)
|
|
53
|
-
@options[key.to_s] = value.to_s
|
|
57
|
+
@options[key.to_s] = value.to_s unless value.nil?
|
|
54
58
|
self
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
# @return [self] set multiple write options.
|
|
58
62
|
def options(opts)
|
|
59
|
-
opts.each { |k, v|
|
|
63
|
+
opts.each { |k, v| option(k, v) }
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [self] cluster the output (e.g. liquid clustering) by these columns.
|
|
68
|
+
def cluster_by(*cols)
|
|
69
|
+
cols = cols.flatten.map(&:to_s)
|
|
70
|
+
raise IllegalArgumentError, "cluster_by needs one or more clustering columns" if cols.empty?
|
|
71
|
+
|
|
72
|
+
@clustering_columns = cols
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
alias clusterBy cluster_by
|
|
76
|
+
|
|
77
|
+
# Enable automatic schema evolution for this write (Spark 4.2+); the target
|
|
78
|
+
# table must declare the `AUTOMATIC_SCHEMA_EVOLUTION` capability. This is
|
|
79
|
+
# the Scala API's `withSchemaEvolution` (PySpark exposes it only on
|
|
80
|
+
# {MergeIntoWriter}).
|
|
81
|
+
# @return [self]
|
|
82
|
+
def with_schema_evolution
|
|
83
|
+
@schema_evolution = true
|
|
60
84
|
self
|
|
61
85
|
end
|
|
86
|
+
alias withSchemaEvolution with_schema_evolution
|
|
62
87
|
|
|
63
88
|
# @return [self] partition the output by these columns.
|
|
64
89
|
def partition_by(*cols)
|
|
@@ -115,13 +140,27 @@ module SparkConnect
|
|
|
115
140
|
def csv(path) = format("csv").save(path)
|
|
116
141
|
def orc(path) = format("orc").save(path)
|
|
117
142
|
def text(path) = format("text").save(path)
|
|
143
|
+
# @return [void] convenience for `format("xml").save(path)` (set `option("rowTag", ...)`).
|
|
144
|
+
def xml(path) = format("xml").save(path)
|
|
145
|
+
|
|
146
|
+
# Write to a JDBC table.
|
|
147
|
+
#
|
|
148
|
+
# @param url [String]
|
|
149
|
+
# @param table [String]
|
|
150
|
+
# @param mode [Symbol, String, nil] the save mode.
|
|
151
|
+
# @param properties [Hash] connection properties (`user`, `password`, ...).
|
|
152
|
+
# @return [void]
|
|
153
|
+
def jdbc(url, table, mode = nil, properties = {})
|
|
154
|
+
format("jdbc").mode(mode).options(properties.transform_keys(&:to_s)).options("url" => url, "dbtable" => table).save
|
|
155
|
+
end
|
|
118
156
|
|
|
119
157
|
private
|
|
120
158
|
|
|
121
159
|
def base_operation
|
|
122
160
|
op = WO.new(
|
|
123
161
|
input: @df.relation, mode: @mode, options: @options,
|
|
124
|
-
partitioning_columns: @partitioning_columns, sort_column_names: @sort_columns
|
|
162
|
+
partitioning_columns: @partitioning_columns, sort_column_names: @sort_columns,
|
|
163
|
+
clustering_columns: @clustering_columns, with_schema_evolution: @schema_evolution
|
|
125
164
|
)
|
|
126
165
|
op.source = @source if @source
|
|
127
166
|
op.bucket_by = WO::BucketBy.new(bucket_column_names: @bucket_cols, num_buckets: @num_buckets) if @num_buckets
|
|
@@ -153,6 +192,8 @@ module SparkConnect
|
|
|
153
192
|
@options = {}
|
|
154
193
|
@table_properties = {}
|
|
155
194
|
@partitioning = []
|
|
195
|
+
@clustering_columns = []
|
|
196
|
+
@schema_evolution = false
|
|
156
197
|
end
|
|
157
198
|
|
|
158
199
|
# @return [self] set the table provider/format.
|
|
@@ -161,9 +202,15 @@ module SparkConnect
|
|
|
161
202
|
self
|
|
162
203
|
end
|
|
163
204
|
|
|
164
|
-
# @return [self] set a write option.
|
|
205
|
+
# @return [self] set a write option (a `nil` value is ignored).
|
|
165
206
|
def option(key, value)
|
|
166
|
-
@options[key.to_s] = value.to_s
|
|
207
|
+
@options[key.to_s] = value.to_s unless value.nil?
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# @return [self] set multiple write options.
|
|
212
|
+
def options(opts)
|
|
213
|
+
opts.each { |k, v| option(k, v) }
|
|
167
214
|
self
|
|
168
215
|
end
|
|
169
216
|
|
|
@@ -172,12 +219,32 @@ module SparkConnect
|
|
|
172
219
|
@table_properties[key.to_s] = value.to_s
|
|
173
220
|
self
|
|
174
221
|
end
|
|
222
|
+
alias tableProperty table_property
|
|
175
223
|
|
|
176
|
-
#
|
|
177
|
-
|
|
224
|
+
# Partition the table by columns or partition transforms (`F.years("ts")`,
|
|
225
|
+
# `F.bucket(4, "id")`, ...).
|
|
226
|
+
# @return [self]
|
|
227
|
+
def partitioned_by(*cols)
|
|
178
228
|
@partitioning = cols.flatten.map { |c| (c.is_a?(Column) ? c : Functions.col(c.to_s)).to_expr }
|
|
179
229
|
self
|
|
180
230
|
end
|
|
231
|
+
alias partition_by partitioned_by
|
|
232
|
+
alias partitionedBy partitioned_by
|
|
233
|
+
|
|
234
|
+
# @return [self] cluster the table by these columns.
|
|
235
|
+
def cluster_by(*cols)
|
|
236
|
+
@clustering_columns = cols.flatten.map(&:to_s)
|
|
237
|
+
self
|
|
238
|
+
end
|
|
239
|
+
alias clusterBy cluster_by
|
|
240
|
+
|
|
241
|
+
# Enable automatic schema evolution for this write (Spark 4.2+).
|
|
242
|
+
# @return [self]
|
|
243
|
+
def with_schema_evolution
|
|
244
|
+
@schema_evolution = true
|
|
245
|
+
self
|
|
246
|
+
end
|
|
247
|
+
alias withSchemaEvolution with_schema_evolution
|
|
181
248
|
|
|
182
249
|
# Create the table. @return [void]
|
|
183
250
|
def create = run(:MODE_CREATE)
|
|
@@ -185,19 +252,22 @@ module SparkConnect
|
|
|
185
252
|
def replace = run(:MODE_REPLACE)
|
|
186
253
|
# Create or replace the table. @return [void]
|
|
187
254
|
def create_or_replace = run(:MODE_CREATE_OR_REPLACE)
|
|
255
|
+
alias createOrReplace create_or_replace
|
|
188
256
|
# Append rows. @return [void]
|
|
189
257
|
def append = run(:MODE_APPEND)
|
|
190
258
|
# Overwrite rows matching `condition`. @return [void]
|
|
191
259
|
def overwrite(condition) = run(:MODE_OVERWRITE, overwrite_condition: Column.to_col(condition).to_expr)
|
|
192
260
|
# Dynamically overwrite partitions. @return [void]
|
|
193
261
|
def overwrite_partitions = run(:MODE_OVERWRITE_PARTITIONS)
|
|
262
|
+
alias overwritePartitions overwrite_partitions
|
|
194
263
|
|
|
195
264
|
private
|
|
196
265
|
|
|
197
266
|
def run(mode, overwrite_condition: nil)
|
|
198
267
|
op = WO2.new(
|
|
199
268
|
input: @df.relation, table_name: @table, mode: mode,
|
|
200
|
-
options: @options, table_properties: @table_properties, partitioning_columns: @partitioning
|
|
269
|
+
options: @options, table_properties: @table_properties, partitioning_columns: @partitioning,
|
|
270
|
+
clustering_columns: @clustering_columns, with_schema_evolution: @schema_evolution
|
|
201
271
|
)
|
|
202
272
|
op.provider = @provider if @provider
|
|
203
273
|
op.overwrite_condition = overwrite_condition if overwrite_condition
|
data/lib/spark_connect.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "spark_connect/types"
|
|
|
9
9
|
require_relative "spark_connect/row"
|
|
10
10
|
require_relative "spark_connect/plan"
|
|
11
11
|
require_relative "spark_connect/column"
|
|
12
|
+
require_relative "spark_connect/storage_level"
|
|
12
13
|
require_relative "spark_connect/window"
|
|
13
14
|
require_relative "spark_connect/functions"
|
|
14
15
|
require_relative "spark_connect/arrow"
|
|
@@ -25,6 +26,9 @@ require_relative "spark_connect/streaming"
|
|
|
25
26
|
require_relative "spark_connect/pipelines"
|
|
26
27
|
require_relative "spark_connect/catalog"
|
|
27
28
|
require_relative "spark_connect/data_frame"
|
|
29
|
+
require_relative "spark_connect/table_arg"
|
|
30
|
+
require_relative "spark_connect/merge"
|
|
31
|
+
require_relative "spark_connect/tvf"
|
|
28
32
|
require_relative "spark_connect/session"
|
|
29
33
|
|
|
30
34
|
# spark-connect is a pure-Ruby client for {https://spark.apache.org/docs/latest/spark-connect-overview.html
|
|
@@ -491,6 +491,12 @@ message ExecutePlanResponse {
|
|
|
491
491
|
repeated Expression.Literal values = 2;
|
|
492
492
|
repeated string keys = 3;
|
|
493
493
|
int64 plan_id = 4;
|
|
494
|
+
// (Optional) The index of the root error in errors.
|
|
495
|
+
// The field will not be set if there are no errors.
|
|
496
|
+
optional int32 root_error_idx = 5;
|
|
497
|
+
// A list of errors that occurred while collecting the observed metrics.
|
|
498
|
+
// If the length is 0, it means no errors occurred.
|
|
499
|
+
repeated FetchErrorDetailsResponse.Error errors = 6;
|
|
494
500
|
}
|
|
495
501
|
|
|
496
502
|
message ResultComplete {
|
|
@@ -1213,11 +1219,94 @@ message CloneSessionResponse {
|
|
|
1213
1219
|
|
|
1214
1220
|
// Session id of the new cloned session.
|
|
1215
1221
|
string new_session_id = 3;
|
|
1216
|
-
|
|
1222
|
+
|
|
1217
1223
|
// Server-side session ID of the new cloned session.
|
|
1218
1224
|
string new_server_side_session_id = 4;
|
|
1219
1225
|
}
|
|
1220
1226
|
|
|
1227
|
+
// Next ID: 6
|
|
1228
|
+
message GetStatusRequest {
|
|
1229
|
+
// (Required)
|
|
1230
|
+
//
|
|
1231
|
+
// The session_id specifies a Spark session for a user identified by user_context.user_id.
|
|
1232
|
+
// The id should be an UUID string of the format `00112233-4455-6677-8899-aabbccddeeff`
|
|
1233
|
+
string session_id = 1;
|
|
1234
|
+
|
|
1235
|
+
// (Required)
|
|
1236
|
+
//
|
|
1237
|
+
// user_context.user_id and session_id both identify a unique remote spark session on the
|
|
1238
|
+
// server side.
|
|
1239
|
+
UserContext user_context = 2;
|
|
1240
|
+
|
|
1241
|
+
// (Optional)
|
|
1242
|
+
//
|
|
1243
|
+
// Provides optional information about the client sending the request. This field
|
|
1244
|
+
// can be used for language or version specific information and is only intended for
|
|
1245
|
+
// logging purposes and will not be interpreted by the server.
|
|
1246
|
+
optional string client_type = 3;
|
|
1247
|
+
|
|
1248
|
+
// (Optional)
|
|
1249
|
+
//
|
|
1250
|
+
// Server-side generated idempotency key from the previous responses (if any). Server
|
|
1251
|
+
// can use this to validate that the server side session has not changed.
|
|
1252
|
+
optional string client_observed_server_side_session_id = 4;
|
|
1253
|
+
|
|
1254
|
+
// (Optional)
|
|
1255
|
+
//
|
|
1256
|
+
// Get status of operations in the session.
|
|
1257
|
+
optional OperationStatusRequest operation_status = 5;
|
|
1258
|
+
|
|
1259
|
+
// Extension point for custom status request types.
|
|
1260
|
+
repeated google.protobuf.Any extensions = 999;
|
|
1261
|
+
|
|
1262
|
+
message OperationStatusRequest {
|
|
1263
|
+
// Get status of operations with these operation_ids.
|
|
1264
|
+
// If unset or empty, returns status of all operations in the session.
|
|
1265
|
+
repeated string operation_ids = 1;
|
|
1266
|
+
|
|
1267
|
+
// Extension point for custom operation-level status requests.
|
|
1268
|
+
repeated google.protobuf.Any extensions = 999;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Next ID: 4
|
|
1273
|
+
message GetStatusResponse {
|
|
1274
|
+
// Session id of the session for which the status was requested.
|
|
1275
|
+
string session_id = 1;
|
|
1276
|
+
|
|
1277
|
+
// Server-side generated idempotency key that the client can use to assert that the server side
|
|
1278
|
+
// session has not changed.
|
|
1279
|
+
string server_side_session_id = 2;
|
|
1280
|
+
|
|
1281
|
+
// Status information about requested operations.
|
|
1282
|
+
repeated OperationStatus operation_statuses = 3;
|
|
1283
|
+
|
|
1284
|
+
// Extension point for custom status response types.
|
|
1285
|
+
repeated google.protobuf.Any extensions = 999;
|
|
1286
|
+
|
|
1287
|
+
// Status information for a single operation.
|
|
1288
|
+
message OperationStatus {
|
|
1289
|
+
// The operation_id of the operation.
|
|
1290
|
+
string operation_id = 1;
|
|
1291
|
+
|
|
1292
|
+
// The current status of the operation.
|
|
1293
|
+
OperationState state = 2;
|
|
1294
|
+
|
|
1295
|
+
// Extension point for custom operation-level status fields.
|
|
1296
|
+
repeated google.protobuf.Any extensions = 999;
|
|
1297
|
+
|
|
1298
|
+
enum OperationState {
|
|
1299
|
+
OPERATION_STATE_UNSPECIFIED = 0;
|
|
1300
|
+
OPERATION_STATE_UNKNOWN = 1;
|
|
1301
|
+
OPERATION_STATE_RUNNING = 2;
|
|
1302
|
+
OPERATION_STATE_TERMINATING = 3;
|
|
1303
|
+
OPERATION_STATE_SUCCEEDED = 4;
|
|
1304
|
+
OPERATION_STATE_FAILED = 5;
|
|
1305
|
+
OPERATION_STATE_CANCELLED = 6;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1221
1310
|
// Main interface for the SparkConnect service.
|
|
1222
1311
|
service SparkConnectService {
|
|
1223
1312
|
|
|
@@ -1272,4 +1361,7 @@ service SparkConnectService {
|
|
|
1272
1361
|
// The request can optionally specify a custom session ID for the cloned session (must be
|
|
1273
1362
|
// a valid UUID). If not provided, a new UUID will be generated automatically.
|
|
1274
1363
|
rpc CloneSession(CloneSessionRequest) returns (CloneSessionResponse) {}
|
|
1364
|
+
|
|
1365
|
+
// Get status information of different types.
|
|
1366
|
+
rpc GetStatus(GetStatusRequest) returns (GetStatusResponse) {}
|
|
1275
1367
|
}
|
|
@@ -55,6 +55,16 @@ message Catalog {
|
|
|
55
55
|
CurrentCatalog current_catalog = 24;
|
|
56
56
|
SetCurrentCatalog set_current_catalog = 25;
|
|
57
57
|
ListCatalogs list_catalogs = 26;
|
|
58
|
+
DropTable drop_table = 27;
|
|
59
|
+
DropView drop_view = 28;
|
|
60
|
+
CreateDatabase create_database = 29;
|
|
61
|
+
DropDatabase drop_database = 30;
|
|
62
|
+
ListPartitions list_partitions = 31;
|
|
63
|
+
ListViews list_views = 32;
|
|
64
|
+
GetTableProperties get_table_properties = 33;
|
|
65
|
+
GetCreateTableString get_create_table_string = 34;
|
|
66
|
+
TruncateTable truncate_table = 35;
|
|
67
|
+
AnalyzeTable analyze_table = 36;
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
70
|
|
|
@@ -241,3 +251,74 @@ message ListCatalogs {
|
|
|
241
251
|
// (Optional) The pattern that the catalog name needs to match
|
|
242
252
|
optional string pattern = 1;
|
|
243
253
|
}
|
|
254
|
+
|
|
255
|
+
// See `spark.catalog.dropTable`
|
|
256
|
+
message DropTable {
|
|
257
|
+
// (Required)
|
|
258
|
+
string table_name = 1;
|
|
259
|
+
bool if_exists = 2;
|
|
260
|
+
bool purge = 3;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// See `spark.catalog.dropView`
|
|
264
|
+
message DropView {
|
|
265
|
+
// (Required)
|
|
266
|
+
string view_name = 1;
|
|
267
|
+
bool if_exists = 2;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// See `spark.catalog.createDatabase`
|
|
271
|
+
message CreateDatabase {
|
|
272
|
+
// (Required)
|
|
273
|
+
string db_name = 1;
|
|
274
|
+
bool if_not_exists = 2;
|
|
275
|
+
map<string, string> properties = 3;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// See `spark.catalog.dropDatabase`
|
|
279
|
+
message DropDatabase {
|
|
280
|
+
// (Required)
|
|
281
|
+
string db_name = 1;
|
|
282
|
+
bool if_exists = 2;
|
|
283
|
+
bool cascade = 3;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// See `spark.catalog.listPartitions`
|
|
287
|
+
message ListPartitions {
|
|
288
|
+
// (Required)
|
|
289
|
+
string table_name = 1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// See `spark.catalog.listViews`
|
|
293
|
+
message ListViews {
|
|
294
|
+
// (Optional)
|
|
295
|
+
optional string db_name = 1;
|
|
296
|
+
// (Optional) The pattern that the view name needs to match
|
|
297
|
+
optional string pattern = 2;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// See `spark.catalog.getTableProperties`
|
|
301
|
+
message GetTableProperties {
|
|
302
|
+
// (Required)
|
|
303
|
+
string table_name = 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// See `spark.catalog.getCreateTableString`
|
|
307
|
+
message GetCreateTableString {
|
|
308
|
+
// (Required)
|
|
309
|
+
string table_name = 1;
|
|
310
|
+
bool as_serde = 2;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// See `spark.catalog.truncateTable`
|
|
314
|
+
message TruncateTable {
|
|
315
|
+
// (Required)
|
|
316
|
+
string table_name = 1;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// See `spark.catalog.analyzeTable`
|
|
320
|
+
message AnalyzeTable {
|
|
321
|
+
// (Required)
|
|
322
|
+
string table_name = 1;
|
|
323
|
+
bool no_scan = 2;
|
|
324
|
+
}
|
|
@@ -144,6 +144,9 @@ message WriteOperation {
|
|
|
144
144
|
// (Optional) Columns used for clustering the table.
|
|
145
145
|
repeated string clustering_columns = 10;
|
|
146
146
|
|
|
147
|
+
// (Optional) Whether schema evolution is enabled for the write.
|
|
148
|
+
bool with_schema_evolution = 11;
|
|
149
|
+
|
|
147
150
|
message SaveTable {
|
|
148
151
|
// (Required) The table name.
|
|
149
152
|
string table_name = 1;
|
|
@@ -211,6 +214,9 @@ message WriteOperationV2 {
|
|
|
211
214
|
|
|
212
215
|
// (Optional) Columns used for clustering the table.
|
|
213
216
|
repeated string clustering_columns = 9;
|
|
217
|
+
|
|
218
|
+
// (Optional) Whether schema evolution is enabled for the write.
|
|
219
|
+
bool with_schema_evolution = 10;
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
// Starts write stream operation as streaming query. Query ID and Run ID of the streaming
|
|
@@ -232,6 +238,7 @@ message WriteStreamOperationStart {
|
|
|
232
238
|
bool available_now = 6;
|
|
233
239
|
bool once = 7;
|
|
234
240
|
string continuous_checkpoint_interval = 8;
|
|
241
|
+
string real_time_batch_duration = 100;
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
string output_mode = 9;
|