spark-connect 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +173 -1
  3. data/README.md +37 -13
  4. data/lib/spark_connect/arrow.rb +5 -1
  5. data/lib/spark_connect/catalog.rb +157 -12
  6. data/lib/spark_connect/channel_builder.rb +119 -12
  7. data/lib/spark_connect/client.rb +237 -17
  8. data/lib/spark_connect/column.rb +143 -10
  9. data/lib/spark_connect/conf.rb +17 -1
  10. data/lib/spark_connect/data_frame.rb +447 -25
  11. data/lib/spark_connect/functions.rb +1948 -494
  12. data/lib/spark_connect/grouped_data.rb +33 -7
  13. data/lib/spark_connect/merge.rb +146 -0
  14. data/lib/spark_connect/na_functions.rb +43 -9
  15. data/lib/spark_connect/observation.rb +26 -5
  16. data/lib/spark_connect/pipelines.rb +101 -0
  17. data/lib/spark_connect/plan.rb +52 -0
  18. data/lib/spark_connect/proto/spark/connect/base_pb.rb +6 -1
  19. data/lib/spark_connect/proto/spark/connect/base_services_pb.rb +2 -0
  20. data/lib/spark_connect/proto/spark/connect/catalog_pb.rb +11 -1
  21. data/lib/spark_connect/proto/spark/connect/commands_pb.rb +1 -1
  22. data/lib/spark_connect/proto/spark/connect/expressions_pb.rb +3 -1
  23. data/lib/spark_connect/proto/spark/connect/pipelines_pb.rb +5 -1
  24. data/lib/spark_connect/proto/spark/connect/relations_pb.rb +4 -1
  25. data/lib/spark_connect/proto/spark/connect/types_pb.rb +3 -1
  26. data/lib/spark_connect/reader.rb +98 -17
  27. data/lib/spark_connect/session.rb +232 -30
  28. data/lib/spark_connect/stat_functions.rb +19 -9
  29. data/lib/spark_connect/storage_level.rb +79 -0
  30. data/lib/spark_connect/streaming.rb +335 -20
  31. data/lib/spark_connect/table_arg.rb +73 -0
  32. data/lib/spark_connect/tvf.rb +78 -0
  33. data/lib/spark_connect/types.rb +149 -0
  34. data/lib/spark_connect/version.rb +2 -2
  35. data/lib/spark_connect/writer.rb +80 -10
  36. data/lib/spark_connect.rb +4 -0
  37. data/proto/spark/connect/base.proto +93 -1
  38. data/proto/spark/connect/catalog.proto +81 -0
  39. data/proto/spark/connect/commands.proto +7 -0
  40. data/proto/spark/connect/expressions.proto +28 -0
  41. data/proto/spark/connect/pipelines.proto +92 -5
  42. data/proto/spark/connect/relations.proto +71 -1
  43. data/proto/spark/connect/types.proto +19 -0
  44. metadata +10 -3
@@ -5,7 +5,7 @@
5
5
  require 'google/protobuf'
6
6
 
7
7
 
8
- descriptor_data = "\n\x19spark/connect/types.proto\x12\rspark.connect\"\xbc\x1c\n\x08\x44\x61taType\x12,\n\x04null\x18\x01 \x01(\x0b\x32\x1c.spark.connect.DataType.NULLH\x00\x12\x30\n\x06\x62inary\x18\x02 \x01(\x0b\x32\x1e.spark.connect.DataType.BinaryH\x00\x12\x32\n\x07\x62oolean\x18\x03 \x01(\x0b\x32\x1f.spark.connect.DataType.BooleanH\x00\x12,\n\x04\x62yte\x18\x04 \x01(\x0b\x32\x1c.spark.connect.DataType.ByteH\x00\x12.\n\x05short\x18\x05 \x01(\x0b\x32\x1d.spark.connect.DataType.ShortH\x00\x12\x32\n\x07integer\x18\x06 \x01(\x0b\x32\x1f.spark.connect.DataType.IntegerH\x00\x12,\n\x04long\x18\x07 \x01(\x0b\x32\x1c.spark.connect.DataType.LongH\x00\x12.\n\x05\x66loat\x18\x08 \x01(\x0b\x32\x1d.spark.connect.DataType.FloatH\x00\x12\x30\n\x06\x64ouble\x18\t \x01(\x0b\x32\x1e.spark.connect.DataType.DoubleH\x00\x12\x32\n\x07\x64\x65\x63imal\x18\n \x01(\x0b\x32\x1f.spark.connect.DataType.DecimalH\x00\x12\x30\n\x06string\x18\x0b \x01(\x0b\x32\x1e.spark.connect.DataType.StringH\x00\x12,\n\x04\x63har\x18\x0c \x01(\x0b\x32\x1c.spark.connect.DataType.CharH\x00\x12\x33\n\x08var_char\x18\r \x01(\x0b\x32\x1f.spark.connect.DataType.VarCharH\x00\x12,\n\x04\x64\x61te\x18\x0e \x01(\x0b\x32\x1c.spark.connect.DataType.DateH\x00\x12\x36\n\ttimestamp\x18\x0f \x01(\x0b\x32!.spark.connect.DataType.TimestampH\x00\x12=\n\rtimestamp_ntz\x18\x10 \x01(\x0b\x32$.spark.connect.DataType.TimestampNTZH\x00\x12\x45\n\x11\x63\x61lendar_interval\x18\x11 \x01(\x0b\x32(.spark.connect.DataType.CalendarIntervalH\x00\x12H\n\x13year_month_interval\x18\x12 \x01(\x0b\x32).spark.connect.DataType.YearMonthIntervalH\x00\x12\x44\n\x11\x64\x61y_time_interval\x18\x13 \x01(\x0b\x32\'.spark.connect.DataType.DayTimeIntervalH\x00\x12.\n\x05\x61rray\x18\x14 \x01(\x0b\x32\x1d.spark.connect.DataType.ArrayH\x00\x12\x30\n\x06struct\x18\x15 \x01(\x0b\x32\x1e.spark.connect.DataType.StructH\x00\x12*\n\x03map\x18\x16 \x01(\x0b\x32\x1b.spark.connect.DataType.MapH\x00\x12\x32\n\x07variant\x18\x19 \x01(\x0b\x32\x1f.spark.connect.DataType.VariantH\x00\x12*\n\x03udt\x18\x17 \x01(\x0b\x32\x1b.spark.connect.DataType.UDTH\x00\x12\x34\n\x08geometry\x18\x1a \x01(\x0b\x32 .spark.connect.DataType.GeometryH\x00\x12\x36\n\tgeography\x18\x1b \x01(\x0b\x32!.spark.connect.DataType.GeographyH\x00\x12\x34\n\x08unparsed\x18\x18 \x01(\x0b\x32 .spark.connect.DataType.UnparsedH\x00\x12,\n\x04time\x18\x1c \x01(\x0b\x32\x1c.spark.connect.DataType.TimeH\x00\x1a+\n\x07\x42oolean\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a(\n\x04\x42yte\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a)\n\x05Short\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a+\n\x07Integer\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a(\n\x04Long\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a)\n\x05\x46loat\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a*\n\x06\x44ouble\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a=\n\x06String\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x12\x11\n\tcollation\x18\x02 \x01(\t\x1a*\n\x06\x42inary\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a(\n\x04NULL\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a-\n\tTimestamp\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a(\n\x04\x44\x61te\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a\x30\n\x0cTimestampNTZ\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1aN\n\x04Time\x12\x16\n\tprecision\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x02 \x01(\rB\x0c\n\n_precision\x1a\x34\n\x10\x43\x61lendarInterval\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a\x85\x01\n\x11YearMonthInterval\x12\x18\n\x0bstart_field\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\tend_field\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x03 \x01(\rB\x0e\n\x0c_start_fieldB\x0c\n\n_end_field\x1a\x83\x01\n\x0f\x44\x61yTimeInterval\x12\x18\n\x0bstart_field\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\tend_field\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x03 \x01(\rB\x0e\n\x0c_start_fieldB\x0c\n\n_end_field\x1a\x38\n\x04\x43har\x12\x0e\n\x06length\x18\x01 \x01(\x05\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1a;\n\x07VarChar\x12\x0e\n\x06length\x18\x01 \x01(\x05\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1ao\n\x07\x44\x65\x63imal\x12\x12\n\x05scale\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\tprecision\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x03 \x01(\rB\x08\n\x06_scaleB\x0c\n\n_precision\x1a}\n\x0bStructField\x12\x0c\n\x04name\x18\x01 \x01(\t\x12*\n\tdata_type\x18\x02 \x01(\x0b\x32\x17.spark.connect.DataType\x12\x10\n\x08nullable\x18\x03 \x01(\x08\x12\x15\n\x08metadata\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0b\n\t_metadata\x1a_\n\x06Struct\x12\x33\n\x06\x66ields\x18\x01 \x03(\x0b\x32#.spark.connect.DataType.StructField\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1ao\n\x05\x41rray\x12-\n\x0c\x65lement_type\x18\x01 \x01(\x0b\x32\x17.spark.connect.DataType\x12\x15\n\rcontains_null\x18\x02 \x01(\x08\x12 \n\x18type_variation_reference\x18\x03 \x01(\r\x1a\x9c\x01\n\x03Map\x12)\n\x08key_type\x18\x01 \x01(\x0b\x32\x17.spark.connect.DataType\x12+\n\nvalue_type\x18\x02 \x01(\x0b\x32\x17.spark.connect.DataType\x12\x1b\n\x13value_contains_null\x18\x03 \x01(\x08\x12 \n\x18type_variation_reference\x18\x04 \x01(\r\x1a:\n\x08Geometry\x12\x0c\n\x04srid\x18\x01 \x01(\x05\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1a;\n\tGeography\x12\x0c\n\x04srid\x18\x01 \x01(\x05\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1a+\n\x07Variant\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a\xe4\x01\n\x03UDT\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x16\n\tjvm_class\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x19\n\x0cpython_class\x18\x03 \x01(\tH\x01\x88\x01\x01\x12$\n\x17serialized_python_class\x18\x04 \x01(\tH\x02\x88\x01\x01\x12.\n\x08sql_type\x18\x05 \x01(\x0b\x32\x17.spark.connect.DataTypeH\x03\x88\x01\x01\x42\x0c\n\n_jvm_classB\x0f\n\r_python_classB\x1a\n\x18_serialized_python_classB\x0b\n\t_sql_type\x1a$\n\x08Unparsed\x12\x18\n\x10\x64\x61ta_type_string\x18\x01 \x01(\tB\x06\n\x04kindB6\n\x1eorg.apache.spark.connect.protoP\x01Z\x12internal/generatedb\x06proto3"
8
+ descriptor_data = "\n\x19spark/connect/types.proto\x12\rspark.connect\"\x8a\x1f\n\x08\x44\x61taType\x12,\n\x04null\x18\x01 \x01(\x0b\x32\x1c.spark.connect.DataType.NULLH\x00\x12\x30\n\x06\x62inary\x18\x02 \x01(\x0b\x32\x1e.spark.connect.DataType.BinaryH\x00\x12\x32\n\x07\x62oolean\x18\x03 \x01(\x0b\x32\x1f.spark.connect.DataType.BooleanH\x00\x12,\n\x04\x62yte\x18\x04 \x01(\x0b\x32\x1c.spark.connect.DataType.ByteH\x00\x12.\n\x05short\x18\x05 \x01(\x0b\x32\x1d.spark.connect.DataType.ShortH\x00\x12\x32\n\x07integer\x18\x06 \x01(\x0b\x32\x1f.spark.connect.DataType.IntegerH\x00\x12,\n\x04long\x18\x07 \x01(\x0b\x32\x1c.spark.connect.DataType.LongH\x00\x12.\n\x05\x66loat\x18\x08 \x01(\x0b\x32\x1d.spark.connect.DataType.FloatH\x00\x12\x30\n\x06\x64ouble\x18\t \x01(\x0b\x32\x1e.spark.connect.DataType.DoubleH\x00\x12\x32\n\x07\x64\x65\x63imal\x18\n \x01(\x0b\x32\x1f.spark.connect.DataType.DecimalH\x00\x12\x30\n\x06string\x18\x0b \x01(\x0b\x32\x1e.spark.connect.DataType.StringH\x00\x12,\n\x04\x63har\x18\x0c \x01(\x0b\x32\x1c.spark.connect.DataType.CharH\x00\x12\x33\n\x08var_char\x18\r \x01(\x0b\x32\x1f.spark.connect.DataType.VarCharH\x00\x12,\n\x04\x64\x61te\x18\x0e \x01(\x0b\x32\x1c.spark.connect.DataType.DateH\x00\x12\x36\n\ttimestamp\x18\x0f \x01(\x0b\x32!.spark.connect.DataType.TimestampH\x00\x12=\n\rtimestamp_ntz\x18\x10 \x01(\x0b\x32$.spark.connect.DataType.TimestampNTZH\x00\x12\x45\n\x11\x63\x61lendar_interval\x18\x11 \x01(\x0b\x32(.spark.connect.DataType.CalendarIntervalH\x00\x12H\n\x13year_month_interval\x18\x12 \x01(\x0b\x32).spark.connect.DataType.YearMonthIntervalH\x00\x12\x44\n\x11\x64\x61y_time_interval\x18\x13 \x01(\x0b\x32\'.spark.connect.DataType.DayTimeIntervalH\x00\x12.\n\x05\x61rray\x18\x14 \x01(\x0b\x32\x1d.spark.connect.DataType.ArrayH\x00\x12\x30\n\x06struct\x18\x15 \x01(\x0b\x32\x1e.spark.connect.DataType.StructH\x00\x12*\n\x03map\x18\x16 \x01(\x0b\x32\x1b.spark.connect.DataType.MapH\x00\x12\x32\n\x07variant\x18\x19 \x01(\x0b\x32\x1f.spark.connect.DataType.VariantH\x00\x12*\n\x03udt\x18\x17 \x01(\x0b\x32\x1b.spark.connect.DataType.UDTH\x00\x12\x34\n\x08geometry\x18\x1a \x01(\x0b\x32 .spark.connect.DataType.GeometryH\x00\x12\x36\n\tgeography\x18\x1b \x01(\x0b\x32!.spark.connect.DataType.GeographyH\x00\x12\x34\n\x08unparsed\x18\x18 \x01(\x0b\x32 .spark.connect.DataType.UnparsedH\x00\x12,\n\x04time\x18\x1c \x01(\x0b\x32\x1c.spark.connect.DataType.TimeH\x00\x12H\n\x13timestamp_ntz_nanos\x18\x1d \x01(\x0b\x32).spark.connect.DataType.TimestampNTZNanosH\x00\x12H\n\x13timestamp_ltz_nanos\x18\x1e \x01(\x0b\x32).spark.connect.DataType.TimestampLTZNanosH\x00\x1a+\n\x07\x42oolean\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a(\n\x04\x42yte\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a)\n\x05Short\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a+\n\x07Integer\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a(\n\x04Long\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a)\n\x05\x46loat\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a*\n\x06\x44ouble\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a=\n\x06String\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x12\x11\n\tcollation\x18\x02 \x01(\t\x1a*\n\x06\x42inary\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a(\n\x04NULL\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a-\n\tTimestamp\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a(\n\x04\x44\x61te\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a\x30\n\x0cTimestampNTZ\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1aN\n\x04Time\x12\x16\n\tprecision\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x02 \x01(\rB\x0c\n\n_precision\x1a[\n\x11TimestampNTZNanos\x12\x16\n\tprecision\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x02 \x01(\rB\x0c\n\n_precision\x1a[\n\x11TimestampLTZNanos\x12\x16\n\tprecision\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x02 \x01(\rB\x0c\n\n_precision\x1a\x34\n\x10\x43\x61lendarInterval\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a\x85\x01\n\x11YearMonthInterval\x12\x18\n\x0bstart_field\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\tend_field\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x03 \x01(\rB\x0e\n\x0c_start_fieldB\x0c\n\n_end_field\x1a\x83\x01\n\x0f\x44\x61yTimeInterval\x12\x18\n\x0bstart_field\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\tend_field\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x03 \x01(\rB\x0e\n\x0c_start_fieldB\x0c\n\n_end_field\x1a\x38\n\x04\x43har\x12\x0e\n\x06length\x18\x01 \x01(\x05\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1a;\n\x07VarChar\x12\x0e\n\x06length\x18\x01 \x01(\x05\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1ao\n\x07\x44\x65\x63imal\x12\x12\n\x05scale\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12\x16\n\tprecision\x18\x02 \x01(\x05H\x01\x88\x01\x01\x12 \n\x18type_variation_reference\x18\x03 \x01(\rB\x08\n\x06_scaleB\x0c\n\n_precision\x1a}\n\x0bStructField\x12\x0c\n\x04name\x18\x01 \x01(\t\x12*\n\tdata_type\x18\x02 \x01(\x0b\x32\x17.spark.connect.DataType\x12\x10\n\x08nullable\x18\x03 \x01(\x08\x12\x15\n\x08metadata\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x0b\n\t_metadata\x1a_\n\x06Struct\x12\x33\n\x06\x66ields\x18\x01 \x03(\x0b\x32#.spark.connect.DataType.StructField\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1ao\n\x05\x41rray\x12-\n\x0c\x65lement_type\x18\x01 \x01(\x0b\x32\x17.spark.connect.DataType\x12\x15\n\rcontains_null\x18\x02 \x01(\x08\x12 \n\x18type_variation_reference\x18\x03 \x01(\r\x1a\x9c\x01\n\x03Map\x12)\n\x08key_type\x18\x01 \x01(\x0b\x32\x17.spark.connect.DataType\x12+\n\nvalue_type\x18\x02 \x01(\x0b\x32\x17.spark.connect.DataType\x12\x1b\n\x13value_contains_null\x18\x03 \x01(\x08\x12 \n\x18type_variation_reference\x18\x04 \x01(\r\x1a:\n\x08Geometry\x12\x0c\n\x04srid\x18\x01 \x01(\x05\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1a;\n\tGeography\x12\x0c\n\x04srid\x18\x01 \x01(\x05\x12 \n\x18type_variation_reference\x18\x02 \x01(\r\x1a+\n\x07Variant\x12 \n\x18type_variation_reference\x18\x01 \x01(\r\x1a\xe4\x01\n\x03UDT\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x16\n\tjvm_class\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x19\n\x0cpython_class\x18\x03 \x01(\tH\x01\x88\x01\x01\x12$\n\x17serialized_python_class\x18\x04 \x01(\tH\x02\x88\x01\x01\x12.\n\x08sql_type\x18\x05 \x01(\x0b\x32\x17.spark.connect.DataTypeH\x03\x88\x01\x01\x42\x0c\n\n_jvm_classB\x0f\n\r_python_classB\x1a\n\x18_serialized_python_classB\x0b\n\t_sql_type\x1a$\n\x08Unparsed\x12\x18\n\x10\x64\x61ta_type_string\x18\x01 \x01(\tB\x06\n\x04kindB6\n\x1eorg.apache.spark.connect.protoP\x01Z\x12internal/generatedb\x06proto3"
9
9
 
10
10
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
11
11
  pool.add_serialized_file(descriptor_data)
@@ -27,6 +27,8 @@ module Spark
27
27
  DataType::Date = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("spark.connect.DataType.Date").msgclass
28
28
  DataType::TimestampNTZ = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("spark.connect.DataType.TimestampNTZ").msgclass
29
29
  DataType::Time = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("spark.connect.DataType.Time").msgclass
30
+ DataType::TimestampNTZNanos = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("spark.connect.DataType.TimestampNTZNanos").msgclass
31
+ DataType::TimestampLTZNanos = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("spark.connect.DataType.TimestampLTZNanos").msgclass
30
32
  DataType::CalendarInterval = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("spark.connect.DataType.CalendarInterval").msgclass
31
33
  DataType::YearMonthInterval = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("spark.connect.DataType.YearMonthInterval").msgclass
32
34
  DataType::DayTimeInterval = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("spark.connect.DataType.DayTimeInterval").msgclass
@@ -8,6 +8,7 @@ module SparkConnect
8
8
  # spark.read.format("csv").option("header", true).load("data.csv")
9
9
  # spark.read.json("events.json")
10
10
  # spark.read.table("my_table")
11
+ # spark.read.json(lines_df) # parse a DataFrame of JSON strings
11
12
  class DataFrameReader
12
13
  Proto = SparkConnect::Proto
13
14
 
@@ -29,21 +30,25 @@ module SparkConnect
29
30
  # Set the input schema (a {Types::StructType} or DDL string).
30
31
  # @return [self]
31
32
  def schema(schema)
32
- @schema = schema.is_a?(Types::StructType) ? schema.simple_string : schema.to_s
33
+ unless schema.is_a?(Types::StructType) || schema.is_a?(String)
34
+ raise IllegalArgumentError, "schema must be a StructType or DDL String, got #{schema.class}"
35
+ end
36
+
37
+ @schema = schema
33
38
  self
34
39
  end
35
40
 
36
- # Set a single read option.
41
+ # Set a single read option. A `nil` value is ignored.
37
42
  # @return [self]
38
43
  def option(key, value)
39
- @options[key.to_s] = value.to_s
44
+ @options[key.to_s] = value.to_s unless value.nil?
40
45
  self
41
46
  end
42
47
 
43
48
  # Set multiple read options.
44
49
  # @return [self]
45
50
  def options(opts)
46
- opts.each { |k, v| @options[k.to_s] = v.to_s }
51
+ opts.each { |k, v| option(k, v) }
47
52
  self
48
53
  end
49
54
 
@@ -52,10 +57,7 @@ module SparkConnect
52
57
  # @param paths [Array<String>]
53
58
  # @return [DataFrame]
54
59
  def load(*paths)
55
- ds = Proto::Read::DataSource.new(options: @options, paths: paths.flatten.map(&:to_s))
56
- ds.format = @format if @format
57
- ds.schema = @schema if @schema
58
- read_relation(data_source: ds)
60
+ read_relation(data_source: data_source(paths.flatten.map(&:to_s)))
59
61
  end
60
62
 
61
63
  # Read a registered table or view.
@@ -67,10 +69,31 @@ module SparkConnect
67
69
  read_relation(named_table: nt)
68
70
  end
69
71
 
70
- # @return [DataFrame] CSV at `paths`.
71
- def csv(*paths) = format("csv").load(*paths)
72
- # @return [DataFrame] JSON at `paths`.
73
- def json(*paths) = format("json").load(*paths)
72
+ # The row-level changes (Change Data Capture) of a table as a batch
73
+ # DataFrame (Spark 4.2+; the table's catalog must support change logs).
74
+ # Configure the range with options such as `startingVersion`,
75
+ # `endingVersion`, `startingTimestamp`, `endingTimestamp`,
76
+ # `deduplicationMode`, and `computeUpdates`.
77
+ #
78
+ # @param table_name [String]
79
+ # @return [DataFrame]
80
+ def changes(table_name)
81
+ raise AnalysisError.new("User specified schema not supported with `changes`.", error_class: "_LEGACY_ERROR_TEMP_1189") if @schema
82
+
83
+ rc = Proto::RelationChanges.new(unparsed_identifier: table_name.to_s, options: @options)
84
+ DataFrame.new(@session, PlanBuilder.relation(@session, relation_changes: rc))
85
+ end
86
+
87
+ # CSV at `paths`, or parse a single-string-column {DataFrame} of CSV lines.
88
+ # @return [DataFrame]
89
+ def csv(*paths) = text_source("csv", :PARSE_FORMAT_CSV, paths)
90
+ # JSON at `paths`, or parse a single-string-column {DataFrame} of JSON documents.
91
+ # @return [DataFrame]
92
+ def json(*paths) = text_source("json", :PARSE_FORMAT_JSON, paths)
93
+ # XML at `paths` (set `option("rowTag", ...)`), or parse a single-string-column
94
+ # {DataFrame} of XML records (Spark 4.2+ for the DataFrame form).
95
+ # @return [DataFrame]
96
+ def xml(*paths) = text_source("xml", :PARSE_FORMAT_XML, paths)
74
97
  # @return [DataFrame] Parquet at `paths`.
75
98
  def parquet(*paths) = format("parquet").load(*paths)
76
99
  # @return [DataFrame] ORC at `paths`.
@@ -78,19 +101,77 @@ module SparkConnect
78
101
  # @return [DataFrame] text at `paths` (one `value` column per line).
79
102
  def text(*paths) = format("text").load(*paths)
80
103
 
81
- # Read from a JDBC source.
104
+ JDBC_PARTITION_KEYS = %i[column lower_bound upper_bound num_partitions predicates].freeze
105
+
106
+ # Read from a JDBC source, optionally partitioned on a numeric column
107
+ # (`column:` with `lower_bound:`, `upper_bound:`, `num_partitions:`) or by a
108
+ # list of WHERE-clause `predicates:` (one partition each).
82
109
  #
110
+ # @example
111
+ # spark.read.jdbc(url, "people", "user" => "u", "password" => "p")
112
+ # spark.read.jdbc(url, "people", { "user" => "u" }, column: "id", lower_bound: 0, upper_bound: 100, num_partitions: 4)
83
113
  # @param url [String] the JDBC URL.
84
114
  # @param table [String] the table name (or subquery).
85
- # @param properties [Hash] connection properties (`user`, `password`, ...).
115
+ # @param properties [Hash] connection properties (`user`, `password`, ...);
116
+ # String-keyed keyword arguments are merged into it.
86
117
  # @return [DataFrame]
87
- def jdbc(url, table, properties = {})
88
- opts = { "url" => url, "dbtable" => table }.merge(properties.transform_keys(&:to_s))
89
- format("jdbc").options(opts).load
118
+ def jdbc(url, table, properties = {}, **opts)
119
+ # Symbol keys are partitioning options; anything else is a connection property.
120
+ partitioning, extra = opts.partition { |k, _| JDBC_PARTITION_KEYS.include?(k) }.map(&:to_h)
121
+ properties = properties.merge(extra)
122
+ column, lower_bound, upper_bound, num_partitions, predicates = partitioning.values_at(*JDBC_PARTITION_KEYS)
123
+ format("jdbc")
124
+ unless column.nil?
125
+ if lower_bound.nil? || upper_bound.nil? || num_partitions.nil?
126
+ raise IllegalArgumentError, "lower_bound, upper_bound, and num_partitions are required with column"
127
+ end
128
+
129
+ options("partitionColumn" => column, "lowerBound" => lower_bound, "upperBound" => upper_bound,
130
+ "numPartitions" => num_partitions)
131
+ end
132
+ options(properties.transform_keys(&:to_s))
133
+ options("url" => url, "dbtable" => table)
134
+ return load if predicates.nil? || !column.nil?
135
+
136
+ read_relation(data_source: data_source([], predicates: predicates.map(&:to_s)))
90
137
  end
91
138
 
92
139
  private
93
140
 
141
+ def data_source(paths, predicates: [])
142
+ ds = Proto::Read::DataSource.new(options: @options, paths: paths, predicates: predicates)
143
+ ds.format = @format if @format
144
+ ds.schema = schema_string if @schema
145
+ ds
146
+ end
147
+
148
+ # Like PySpark, a StructType is sent as its JSON form (which the server
149
+ # parses losslessly); a String is sent as given (DDL or JSON).
150
+ def schema_string
151
+ @schema.is_a?(Types::StructType) ? @schema.json : @schema
152
+ end
153
+
154
+ def text_source(fmt, parse_format, paths)
155
+ return parse(paths.first, parse_format) if paths.size == 1 && paths.first.is_a?(DataFrame)
156
+
157
+ format(fmt).load(*paths)
158
+ end
159
+
160
+ def parse(df, parse_format)
161
+ parse = Proto::Parse.new(input: df.relation, format: parse_format, options: @options)
162
+ parse.schema = parse_schema.to_proto if @schema
163
+ DataFrame.new(@session, PlanBuilder.relation(@session, parse: parse))
164
+ end
165
+
166
+ # The Parse relation carries a typed schema; resolve a DDL string through
167
+ # the server's parser.
168
+ def parse_schema
169
+ return @schema if @schema.is_a?(Types::StructType)
170
+
171
+ parsed = @session.client.analyze(ddl_parse: Proto::AnalyzePlanRequest::DDLParse.new(ddl_string: @schema)).ddl_parse.parsed
172
+ Types.from_proto(parsed)
173
+ end
174
+
94
175
  def read_relation(**read_kw)
95
176
  DataFrame.new(@session, PlanBuilder.relation(@session, read: Proto::Read.new(**read_kw)))
96
177
  end
@@ -25,26 +25,99 @@ module SparkConnect
25
25
  def initialize(client)
26
26
  @client = client
27
27
  @plan_id = -1
28
+ @plan_id_lock = Mutex.new
28
29
  @conf = RuntimeConfig.new(client)
30
+ @subquery_relations = {}
31
+ @release_session_on_close = true
29
32
  end
30
33
 
34
+ # Whether {#stop} releases the server-side session.
35
+ # @api private
36
+ attr_accessor :release_session_on_close
37
+
38
+ @default_session = nil
39
+ @session_lock = Mutex.new
40
+ ACTIVE_SESSION_KEY = :spark_connect_active_session
41
+
31
42
  class << self
32
43
  # @return [Builder] a new session builder.
33
44
  def builder
34
45
  Builder.new
35
46
  end
36
47
 
37
- # The currently active session set by {#set_active} / {Builder#get_or_create}.
48
+ # The active session of the current thread, else the process-wide
49
+ # default session (the first session built by {Builder#get_or_create} /
50
+ # {Builder#create}); stopped sessions are skipped.
51
+ #
52
+ # Unlike PySpark's `SparkSession.active()`, this returns nil (instead of
53
+ # raising) when there is no session, which is the idiomatic Ruby shape for
54
+ # an optional lookup.
55
+ #
38
56
  # @return [SparkSession, nil]
39
- attr_accessor :active
57
+ def active
58
+ get_active_session || default_session
59
+ end
60
+
61
+ # Make `session` the active session of the current thread (and the
62
+ # process-wide default when none is set). `nil` clears the active session.
63
+ # @param session [SparkSession, nil]
64
+ def active=(session)
65
+ Thread.current[ACTIVE_SESSION_KEY] = session
66
+ @session_lock.synchronize { @default_session ||= session } if session
67
+ end
68
+
69
+ # @return [SparkSession, nil] the active session of the current thread, if not stopped.
70
+ def get_active_session
71
+ s = Thread.current[ACTIVE_SESSION_KEY]
72
+ s && !s.stopped? ? s : nil
73
+ end
74
+ alias getActiveSession get_active_session
40
75
 
76
+ # @return [SparkSession, nil] the process-wide default session, if not stopped.
77
+ def default_session
78
+ s = @session_lock.synchronize { @default_session }
79
+ s && !s.stopped? ? s : nil
80
+ end
81
+
82
+ # Clear the process-wide default session (it is not stopped). @return [void]
83
+ def clear_default_session
84
+ @session_lock.synchronize { @default_session = nil }
85
+ nil
86
+ end
87
+
88
+ # Clear the current thread's active session (it is not stopped). @return [void]
89
+ def clear_active_session
90
+ Thread.current[ACTIVE_SESSION_KEY] = nil
91
+ end
92
+
93
+ # Forget `session` wherever it is the default/active one.
41
94
  # @api private
95
+ def forget(session)
96
+ @session_lock.synchronize { @default_session = nil if @default_session.equal?(session) }
97
+ Thread.current[ACTIVE_SESSION_KEY] = nil if Thread.current[ACTIVE_SESSION_KEY].equal?(session)
98
+ end
42
99
  end
43
100
 
44
101
  # Allocate the next unique plan id. Used by {PlanBuilder.relation}.
45
102
  # @return [Integer]
46
103
  def next_plan_id
47
- @plan_id += 1
104
+ @plan_id_lock.synchronize { @plan_id += 1 }
105
+ end
106
+
107
+ # @api private - remember a DataFrame's relation so a subquery expression
108
+ # that refers to it by plan id can be expanded into a `WithRelations`.
109
+ # @param relation [Spark::Connect::Relation]
110
+ # @return [Integer] the relation's plan id.
111
+ def register_subquery_relation(relation)
112
+ plan_id = relation.common.plan_id
113
+ @plan_id_lock.synchronize { @subquery_relations[plan_id] = relation }
114
+ plan_id
115
+ end
116
+
117
+ # @api private
118
+ # @return [Spark::Connect::Relation, nil]
119
+ def subquery_relation(plan_id)
120
+ @plan_id_lock.synchronize { @subquery_relations[plan_id] }
48
121
  end
49
122
 
50
123
  # @return [String] the client session id (UUID).
@@ -105,7 +178,13 @@ module SparkConnect
105
178
 
106
179
  # @return [StreamingQueryManager] the manager for this session's streaming queries.
107
180
  def streams
108
- StreamingQueryManager.new(self)
181
+ @streams ||= StreamingQueryManager.new(self)
182
+ end
183
+
184
+ # @return [TableValuedFunction] built-in table-valued functions
185
+ # (`spark.tvf.explode(...)`, `spark.tvf.range(...)`, ...).
186
+ def tvf
187
+ TableValuedFunction.new(self)
109
188
  end
110
189
 
111
190
  # Create a new Spark Declarative Pipeline (a dataflow graph) in this session.
@@ -134,6 +213,15 @@ module SparkConnect
134
213
  alias create_dataframe create_data_frame
135
214
  alias createDataFrame create_data_frame
136
215
 
216
+ # An empty {DataFrame} with the given schema.
217
+ #
218
+ # @param schema [Types::StructType, String] a schema or DDL string.
219
+ # @return [DataFrame]
220
+ def empty_data_frame(schema)
221
+ create_data_frame([], schema)
222
+ end
223
+ alias emptyDataFrame empty_data_frame
224
+
137
225
  # @return [RuntimeConfig] runtime configuration facade.
138
226
  attr_reader :conf
139
227
 
@@ -147,19 +235,60 @@ module SparkConnect
147
235
  @client.analyze(spark_version: Proto::AnalyzePlanRequest::SparkVersion.new).spark_version.version
148
236
  end
149
237
 
150
- # Make this the active/default session.
238
+ # Make this the active (and, if unset, the default) session.
151
239
  # @return [self]
152
240
  def set_active
153
241
  SparkSession.active = self
154
242
  self
155
243
  end
156
244
 
157
- # Start a brand-new session against the same endpoint (independent
158
- # server-side session id, configuration, and temporary views).
245
+ # Start a brand-new session against the same endpoint: a fresh server-side
246
+ # session with no configuration, temporary views, or functions carried over
247
+ # (Spark Connect `newSession` semantics, like PySpark 4.3). Configurations
248
+ # set through the builder are not reapplied.
159
249
  # @return [SparkSession]
160
250
  def new_session
161
- SparkSession.new(SparkConnectClient.new(@client.channel_builder))
251
+ session = SparkSession.new(@client.new_session_client)
252
+ session.release_session_on_close = true
253
+ session
254
+ end
255
+ alias newSession new_session
256
+
257
+ # Clone this session on the server: the new session starts with a copy of
258
+ # this session's SQL configurations, temporary views, registered functions,
259
+ # and catalog state, and is isolated from it afterwards (Spark 4.1+).
260
+ #
261
+ # @param new_session_id [String, nil] a UUID for the clone (generated when omitted).
262
+ # @return [SparkSession]
263
+ def clone_session(new_session_id = nil)
264
+ session = SparkSession.new(@client.clone_session(new_session_id))
265
+ session.release_session_on_close = true
266
+ session
162
267
  end
268
+ alias cloneSession clone_session
269
+
270
+ # Register a callable invoked with execution progress of every query run by
271
+ # this session. It is called with the keywords `stages:` (an Array of
272
+ # {StageInfo}), `inflight_tasks:`, `operation_id:`, and `done:`.
273
+ #
274
+ # @param handler [#call] a Proc/lambda or any object responding to `call`.
275
+ # @return [void]
276
+ def register_progress_handler(handler = nil, &block)
277
+ @client.register_progress_handler(handler || block)
278
+ end
279
+ alias registerProgressHandler register_progress_handler
280
+
281
+ # Remove a handler added with {#register_progress_handler}. @return [void]
282
+ def remove_progress_handler(handler)
283
+ @client.remove_progress_handler(handler)
284
+ end
285
+ alias removeProgressHandler remove_progress_handler
286
+
287
+ # Remove all progress handlers. @return [void]
288
+ def clear_progress_handlers
289
+ @client.clear_progress_handlers
290
+ end
291
+ alias clearProgressHandlers clear_progress_handlers
163
292
 
164
293
  # Interrupt all operations running in this session.
165
294
  # @return [Array<String>] the ids of the interrupted operations.
@@ -200,14 +329,24 @@ module SparkConnect
200
329
  @client.clear_tags
201
330
  end
202
331
 
203
- # Release the server-side session and stop the client.
332
+ # Release the server-side session and stop the client. Idempotent.
204
333
  # @return [void]
205
334
  def stop
206
- @client.release_session
207
- SparkSession.active = nil if SparkSession.active.equal?(self)
335
+ return nil if stopped?
336
+
337
+ @streams&.close
338
+ @client.release_session if @release_session_on_close
339
+ @client.close
340
+ SparkSession.forget(self)
208
341
  nil
209
342
  end
210
343
 
344
+ # @return [Boolean] whether {#stop} has been called.
345
+ def stopped?
346
+ @client.respond_to?(:closed?) && @client.closed?
347
+ end
348
+ alias is_stopped stopped?
349
+
211
350
  # @api private
212
351
  def create_data_frame_from_relation(relation)
213
352
  DataFrame.new(self, relation)
@@ -264,9 +403,14 @@ module SparkConnect
264
403
  # Fluent builder for {SparkSession}. Returned by {SparkSession.builder}.
265
404
  class SparkSession
266
405
  class Builder
406
+ # Options that only make sense when connecting and are never applied as
407
+ # session configurations.
408
+ CONNECT_ONLY_OPTIONS = %w[spark.remote spark.master].freeze
409
+
267
410
  def initialize
268
411
  @options = {}
269
412
  @remote = nil
413
+ @channel_builder = nil
270
414
  end
271
415
 
272
416
  # Set the connection string (`sc://...`).
@@ -276,42 +420,100 @@ module SparkConnect
276
420
  self
277
421
  end
278
422
 
423
+ # Use a custom {ChannelBuilder} (e.g. a subclass that customizes the gRPC
424
+ # channel) instead of a connection string.
425
+ # @param channel_builder [ChannelBuilder]
426
+ # @return [self]
427
+ def channel_builder(channel_builder)
428
+ @channel_builder = channel_builder
429
+ self
430
+ end
431
+ alias channelBuilder channel_builder
432
+
433
+ # Accepted for PySpark compatibility; the master is chosen by the Spark
434
+ # Connect server, so this is a no-op.
435
+ # @return [self]
436
+ def master(_master)
437
+ self
438
+ end
439
+
440
+ # Hive support is a server-side setting and cannot be enabled from a
441
+ # Spark Connect client (PySpark raises too).
442
+ # @raise [NotImplementedError]
443
+ def enable_hive_support
444
+ raise NotImplementedError, "enableHiveSupport is not supported over Spark Connect; configure the server instead"
445
+ end
446
+ alias enableHiveSupport enable_hive_support
447
+
279
448
  # Set the application name.
280
449
  # @return [self]
281
450
  def app_name(name)
282
451
  @options["spark.app.name"] = name
283
452
  self
284
453
  end
454
+ alias appName app_name
285
455
 
286
456
  # Set an arbitrary configuration option to apply after connecting.
457
+ # @overload config(key, value)
458
+ # @overload config(map)
459
+ # @param map [Hash] several options at once.
287
460
  # @return [self]
288
- def config(key, value)
289
- @options[key.to_s] = value
461
+ def config(key, value = nil)
462
+ if key.is_a?(Hash)
463
+ key.each { |k, v| @options[k.to_s] = v }
464
+ else
465
+ @options[key.to_s] = value
466
+ end
290
467
  self
291
468
  end
292
469
 
293
- # Build (or reuse the active) {SparkSession}.
294
- # @return [SparkSession]
295
- def get_or_create
296
- existing = SparkSession.active
297
- return existing if existing
298
-
299
- session = create
300
- SparkSession.active = session
301
- session
470
+ # Reuse the active (or default) session, else build a new one. Builder
471
+ # options are applied to the returned session either way, like PySpark.
472
+ # With a block, yields the session and stops it afterwards (the Ruby
473
+ # counterpart of PySpark's `with ... as spark:`), returning the block's value.
474
+ #
475
+ # @yieldparam session [SparkSession]
476
+ # @return [SparkSession, Object]
477
+ def get_or_create(&block)
478
+ session = SparkSession.active
479
+ if session
480
+ apply_options(session)
481
+ else
482
+ session = create
483
+ end
484
+ block ? use(session, &block) : session
302
485
  end
303
486
  alias getOrCreate get_or_create
304
487
 
305
- # Always build a new {SparkSession}.
306
- # @return [SparkSession]
307
- def create
308
- url = @remote || ENV["SPARK_REMOTE"] || "sc://localhost:15002"
309
- client = SparkConnectClient.new(ChannelBuilder.new(url))
310
- session = SparkSession.new(client)
311
- @options.each { |k, v| session.conf.set(k, v) unless k == "spark.app.name" }
312
- session
488
+ # Always build a new {SparkSession} (which becomes the active session,
489
+ # and the default one when none is set). With a block, behaves like
490
+ # {#get_or_create} with a block.
491
+ #
492
+ # @yieldparam session [SparkSession]
493
+ # @return [SparkSession, Object]
494
+ def create(&block)
495
+ raise IllegalArgumentError, "Set either remote(...) or channel_builder(...), not both" if @remote && @channel_builder
496
+
497
+ channel = @channel_builder || ChannelBuilder.new(@remote || ENV["SPARK_REMOTE"] || "sc://localhost:15002")
498
+ session = SparkSession.new(SparkConnectClient.new(channel))
499
+ SparkSession.active = session
500
+ apply_options(session)
501
+ block ? use(session, &block) : session
313
502
  end
314
503
  alias build create
504
+
505
+ private
506
+
507
+ def apply_options(session)
508
+ opts = @options.except(*CONNECT_ONLY_OPTIONS)
509
+ session.conf.set_all(opts, silent: true) unless opts.empty?
510
+ end
511
+
512
+ def use(session)
513
+ yield session
514
+ ensure
515
+ session.stop
516
+ end
315
517
  end
316
518
  end
317
519
  end
@@ -22,10 +22,13 @@ module SparkConnect
22
22
  scalar(@df.build(cov: Proto::StatCov.new(input: @df.relation, col1: col1.to_s, col2: col2.to_s)))
23
23
  end
24
24
 
25
- # Correlation of two columns (`method` is `"pearson"`).
25
+ # Correlation of two columns (`method` is `"pearson"`, the only supported one).
26
26
  # @return [Float]
27
- def corr(col1, col2, method = "pearson")
28
- rel = Proto::StatCorr.new(input: @df.relation, col1: col1.to_s, col2: col2.to_s, method: method)
27
+ def corr(col1, col2, method = nil)
28
+ method = "pearson" if method.nil? || method.to_s.empty?
29
+ raise IllegalArgumentError, "corr method must be 'pearson', got #{method.inspect}" unless method.to_s == "pearson"
30
+
31
+ rel = Proto::StatCorr.new(input: @df.relation, col1: col1.to_s, col2: col2.to_s, method: method.to_s)
29
32
  scalar(@df.build(corr: rel))
30
33
  end
31
34
 
@@ -40,8 +43,9 @@ module SparkConnect
40
43
  # @param cols [Array<String>]
41
44
  # @param support [Float]
42
45
  # @return [DataFrame]
43
- def freq_items(cols, support = 0.01)
44
- rel = Proto::StatFreqItems.new(input: @df.relation, cols: Array(cols).map(&:to_s), support: support)
46
+ def freq_items(cols, support = nil)
47
+ support = 0.01 if support.nil? || support.zero?
48
+ rel = Proto::StatFreqItems.new(input: @df.relation, cols: Array(cols).map(&:to_s), support: support.to_f)
45
49
  @df.build(freq_items: rel)
46
50
  end
47
51
 
@@ -52,10 +56,15 @@ module SparkConnect
52
56
  # @param relative_error [Float]
53
57
  # @return [Array<Float>, Array<Array<Float>>] one list per column.
54
58
  def approx_quantile(cols, probabilities, relative_error)
59
+ unless probabilities.all? { |p| p.is_a?(Numeric) && p.between?(0, 1) }
60
+ raise IllegalArgumentError, "probabilities must be numbers in [0, 1], got #{probabilities.inspect}"
61
+ end
62
+ raise IllegalArgumentError, "relative_error must be non-negative, got #{relative_error}" if relative_error.negative?
63
+
55
64
  single = !cols.is_a?(Array)
56
65
  rel = Proto::StatApproxQuantile.new(
57
66
  input: @df.relation, cols: Array(cols).map(&:to_s),
58
- probabilities: probabilities, relative_error: relative_error
67
+ probabilities: probabilities.map(&:to_f), relative_error: relative_error.to_f
59
68
  )
60
69
  row = @df.build(approx_quantile: rel).collect.first
61
70
  result = row.to_a
@@ -71,10 +80,11 @@ module SparkConnect
71
80
  def sample_by(col, fractions, seed = nil)
72
81
  col_expr = (col.is_a?(Column) ? col : Functions.col(col.to_s)).to_expr
73
82
  frac = fractions.map do |stratum, fraction|
74
- Proto::StatSampleBy::Fraction.new(stratum: Column.to_literal(stratum), fraction: fraction)
83
+ Proto::StatSampleBy::Fraction.new(stratum: Column.to_literal(stratum), fraction: fraction.to_f)
75
84
  end
76
- rel = Proto::StatSampleBy.new(input: @df.relation, col: col_expr, fractions: frac)
77
- rel.seed = seed if seed
85
+ # Like PySpark, pick the seed on the client so the sample is stable across actions.
86
+ rel = Proto::StatSampleBy.new(input: @df.relation, col: col_expr, fractions: frac,
87
+ seed: seed.nil? ? Random.rand(2**63) : Integer(seed))
78
88
  @df.build(sample_by: rel)
79
89
  end
80
90