polars-df 0.25.0 → 0.26.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/Cargo.lock +270 -97
  4. data/LICENSE.txt +1 -1
  5. data/README.md +1 -3
  6. data/ext/polars/Cargo.toml +19 -18
  7. data/ext/polars/src/catalog/unity.rs +15 -20
  8. data/ext/polars/src/conversion/any_value.rs +53 -29
  9. data/ext/polars/src/conversion/chunked_array.rs +58 -56
  10. data/ext/polars/src/conversion/datetime.rs +58 -7
  11. data/ext/polars/src/conversion/mod.rs +200 -150
  12. data/ext/polars/src/dataframe/export.rs +15 -12
  13. data/ext/polars/src/dataframe/general.rs +25 -7
  14. data/ext/polars/src/dataframe/map.rs +6 -4
  15. data/ext/polars/src/error.rs +1 -1
  16. data/ext/polars/src/expr/array.rs +0 -24
  17. data/ext/polars/src/expr/datatype.rs +13 -3
  18. data/ext/polars/src/expr/datetime.rs +4 -4
  19. data/ext/polars/src/expr/general.rs +35 -15
  20. data/ext/polars/src/expr/list.rs +0 -26
  21. data/ext/polars/src/expr/rolling.rs +24 -0
  22. data/ext/polars/src/functions/business.rs +2 -2
  23. data/ext/polars/src/functions/io.rs +4 -3
  24. data/ext/polars/src/functions/lazy.rs +65 -46
  25. data/ext/polars/src/functions/meta.rs +6 -5
  26. data/ext/polars/src/functions/mod.rs +0 -1
  27. data/ext/polars/src/functions/range.rs +13 -0
  28. data/ext/polars/src/functions/utils.rs +4 -2
  29. data/ext/polars/src/interop/arrow/mod.rs +4 -2
  30. data/ext/polars/src/interop/arrow/to_rb.rs +1 -1
  31. data/ext/polars/src/interop/numo/to_numo_series.rs +26 -25
  32. data/ext/polars/src/io/scan_options.rs +6 -3
  33. data/ext/polars/src/io/sink_options.rs +2 -0
  34. data/ext/polars/src/lazyframe/general.rs +243 -17
  35. data/ext/polars/src/lazyframe/optflags.rs +2 -1
  36. data/ext/polars/src/lib.rs +39 -35
  37. data/ext/polars/src/map/lazy.rs +5 -2
  38. data/ext/polars/src/map/series.rs +19 -18
  39. data/ext/polars/src/on_startup.rs +25 -6
  40. data/ext/polars/src/ruby/numo.rs +3 -4
  41. data/ext/polars/src/ruby/plan_callback.rs +1 -4
  42. data/ext/polars/src/ruby/rb_modules.rs +2 -4
  43. data/ext/polars/src/ruby/ruby_udf.rs +7 -9
  44. data/ext/polars/src/ruby/utils.rs +12 -1
  45. data/ext/polars/src/series/aggregation.rs +13 -1
  46. data/ext/polars/src/series/construction.rs +31 -50
  47. data/ext/polars/src/series/export.rs +33 -38
  48. data/ext/polars/src/series/general.rs +6 -6
  49. data/ext/polars/src/series/map.rs +3 -2
  50. data/ext/polars/src/series/scatter.rs +4 -4
  51. data/ext/polars/src/utils.rs +31 -7
  52. data/lib/polars/array_expr.rb +23 -7
  53. data/lib/polars/array_name_space.rb +16 -2
  54. data/lib/polars/binary_name_space.rb +32 -0
  55. data/lib/polars/collect_batches.rb +4 -0
  56. data/lib/polars/data_frame.rb +144 -11
  57. data/lib/polars/data_type_group.rb +5 -0
  58. data/lib/polars/date_time_expr.rb +91 -3
  59. data/lib/polars/date_time_name_space.rb +7 -1
  60. data/lib/polars/expr.rb +247 -44
  61. data/lib/polars/functions/business.rb +2 -2
  62. data/lib/polars/functions/datatype.rb +30 -0
  63. data/lib/polars/functions/eager.rb +80 -7
  64. data/lib/polars/functions/lazy.rb +97 -2
  65. data/lib/polars/functions/range/linear_space.rb +118 -0
  66. data/lib/polars/io/csv.rb +27 -5
  67. data/lib/polars/io/database.rb +2 -3
  68. data/lib/polars/io/ipc.rb +2 -2
  69. data/lib/polars/io/lines.rb +172 -0
  70. data/lib/polars/io/parquet.rb +1 -1
  71. data/lib/polars/io/sink_options.rb +5 -2
  72. data/lib/polars/lazy_frame.rb +517 -14
  73. data/lib/polars/list_expr.rb +21 -7
  74. data/lib/polars/list_name_space.rb +16 -2
  75. data/lib/polars/query_opt_flags.rb +23 -5
  76. data/lib/polars/selectors.rb +2 -2
  77. data/lib/polars/series.rb +176 -19
  78. data/lib/polars/sql_context.rb +2 -2
  79. data/lib/polars/string_cache.rb +19 -72
  80. data/lib/polars/string_expr.rb +1 -7
  81. data/lib/polars/string_name_space.rb +1 -7
  82. data/lib/polars/utils/construction/series.rb +24 -39
  83. data/lib/polars/utils/convert.rb +16 -6
  84. data/lib/polars/utils/parse.rb +7 -0
  85. data/lib/polars/utils/reduce_balanced.rb +43 -0
  86. data/lib/polars/utils/various.rb +5 -0
  87. data/lib/polars/version.rb +1 -1
  88. data/lib/polars.rb +2 -1
  89. metadata +4 -17
  90. data/ext/polars/src/functions/string_cache.rs +0 -24
@@ -262,6 +262,59 @@ module Polars
262
262
  function.(self, *args, **kwargs, &block)
263
263
  end
264
264
 
265
+ # Allows to alter the lazy frame during the plan stage with the resolved schema.
266
+ #
267
+ # In contrast to `pipe`, this method does not execute `function` immediately but
268
+ # only during the plan stage. This allows to use the resolved schema of the input
269
+ # to dynamically alter the lazy frame. This also means that any exceptions raised
270
+ # by `function` will only be emitted during the plan stage.
271
+ #
272
+ # @note
273
+ # This functionality is considered **unstable**. It may be changed at any
274
+ # point without it being considered a breaking change.
275
+ #
276
+ # @param function [Object]
277
+ # Callable; will receive the frame as the first parameter and the resolved
278
+ # schema as the second parameter.
279
+ #
280
+ # @return [LazyFrame]
281
+ #
282
+ # @example
283
+ # cast_to_float_if_necessary = lambda do |lf, schema|
284
+ # required_casts =
285
+ # schema.filter_map do |name, dtype|
286
+ # Polars.col(name).cast(Polars::Float64) if !dtype.float?
287
+ # end
288
+ # lf.with_columns(required_casts)
289
+ # end
290
+ # lf = Polars::LazyFrame.new(
291
+ # {"a" => [1.0, 2.0], "b" => ["1.0", "2.5"], "c" => [2.0, 3.0]},
292
+ # schema: {"a" => Polars::Float64, "b" => Polars::String, "c" => Polars::Float32}
293
+ # )
294
+ # lf.pipe_with_schema(cast_to_float_if_necessary).collect
295
+ # # =>
296
+ # # shape: (2, 3)
297
+ # # ┌─────┬─────┬─────┐
298
+ # # │ a ┆ b ┆ c │
299
+ # # │ --- ┆ --- ┆ --- │
300
+ # # │ f64 ┆ f64 ┆ f32 │
301
+ # # ╞═════╪═════╪═════╡
302
+ # # │ 1.0 ┆ 1.0 ┆ 2.0 │
303
+ # # │ 2.0 ┆ 2.5 ┆ 3.0 │
304
+ # # └─────┴─────┴─────┘
305
+ def pipe_with_schema(function)
306
+ wrapper = lambda do |lf_and_schema|
307
+ # The last index is because we return a list for multiple inputs
308
+ # to make `pipe_with_schemas` (plural) work, but we don't use that
309
+ function.(
310
+ _from_rbldf(lf_and_schema[0][0]),
311
+ lf_and_schema[1][0]
312
+ )._ldf
313
+ end
314
+
315
+ _from_rbldf(_ldf.pipe_with_schema(wrapper))
316
+ end
317
+
265
318
  # Creates a summary of statistics for a LazyFrame, returning a DataFrame.
266
319
  #
267
320
  # @param percentiles [Array]
@@ -1109,7 +1162,8 @@ module Polars
1109
1162
  lazy: false,
1110
1163
  arrow_schema: nil,
1111
1164
  engine: "auto",
1112
- optimizations: DEFAULT_QUERY_OPT_FLAGS
1165
+ optimizations: DEFAULT_QUERY_OPT_FLAGS,
1166
+ _sinked_paths_callback: nil
1113
1167
  )
1114
1168
  engine = _select_engine(engine)
1115
1169
 
@@ -1151,7 +1205,8 @@ module Polars
1151
1205
  maintain_order: maintain_order,
1152
1206
  sync_on_close: sync_on_close,
1153
1207
  storage_options: storage_options,
1154
- credential_provider: credential_provider_builder
1208
+ credential_provider: credential_provider_builder,
1209
+ sinked_paths_callback: _sinked_paths_callback
1155
1210
  )
1156
1211
 
1157
1212
  ldf_rb = _ldf.sink_parquet(
@@ -1175,6 +1230,133 @@ module Polars
1175
1230
  LazyFrame._from_rbldf(ldf_rb)
1176
1231
  end
1177
1232
 
1233
+ # Sink DataFrame as delta table.
1234
+ #
1235
+ # @note
1236
+ # This functionality is considered **unstable**. It may be changed
1237
+ # at any point without it being considered a breaking change.
1238
+ #
1239
+ # @param target [Object]
1240
+ # URI of a table or a DeltaTable object.
1241
+ # @param mode ['error', 'append', 'overwrite', 'ignore', 'merge']
1242
+ # How to handle existing data.
1243
+ #
1244
+ # - If 'error', throw an error if the table already exists (default).
1245
+ # - If 'append', will add new data.
1246
+ # - If 'overwrite', will replace table with new data.
1247
+ # - If 'ignore', will not write anything if table already exists.
1248
+ # - If 'merge', return a `TableMerger` object to merge data from the DataFrame
1249
+ # with the existing data.
1250
+ # @param storage_options [Object]
1251
+ # Extra options for the storage backends supported by `deltalake`.
1252
+ # For cloud storages, this may include configurations for authentication etc.
1253
+ #
1254
+ # - See a list of supported storage options for S3 [here](https://docs.rs/object_store/latest/object_store/aws/enum.AmazonS3ConfigKey.html#variants).
1255
+ # - See a list of supported storage options for GCS [here](https://docs.rs/object_store/latest/object_store/gcp/enum.GoogleConfigKey.html#variants).
1256
+ # - See a list of supported storage options for Azure [here](https://docs.rs/object_store/latest/object_store/azure/enum.AzureConfigKey.html#variants).
1257
+ # @param credential_provider [Object]
1258
+ # Provide a function that can be called to provide cloud storage
1259
+ # credentials. The function is expected to return a dictionary of
1260
+ # credential keys along with an optional credential expiry time.
1261
+ # @param delta_write_options [Hash]
1262
+ # Additional keyword arguments while writing a Delta lake Table.
1263
+ # See a list of supported write options [here](https://delta-io.github.io/delta-rs/api/delta_writer/#deltalake.write_deltalake).
1264
+ # @param delta_merge_options [Hash]
1265
+ # Keyword arguments which are required to `MERGE` a Delta lake Table.
1266
+ # See a list of supported merge options [here](https://delta-io.github.io/delta-rs/api/delta_table/#deltalake.DeltaTable.merge).
1267
+ # @param optimizations [Object]
1268
+ # The optimization passes done during query optimization.
1269
+ #
1270
+ # @return [Object]
1271
+ #
1272
+ # @example Sink a large than fits into memory dataset to a Delta Lake table.
1273
+ # lf = Polars.scan_parquet(
1274
+ # "/path/to/my_larger_than_ram_file.parquet"
1275
+ # )
1276
+ # table_path = "/path/to/delta-table/"
1277
+ # lf.sink_delta(table_path)
1278
+ def sink_delta(
1279
+ target,
1280
+ mode: "error",
1281
+ storage_options: nil,
1282
+ credential_provider: "auto",
1283
+ delta_write_options: nil,
1284
+ delta_merge_options: nil,
1285
+ optimizations: DEFAULT_QUERY_OPT_FLAGS
1286
+ )
1287
+ Polars.send(:_check_if_delta_available)
1288
+
1289
+ # TODO
1290
+ # _check_for_unsupported_types(collect_schema.dtypes)
1291
+
1292
+ if Utils.pathlike?(target)
1293
+ target = Polars.send(:_resolve_delta_lake_uri, target.to_s, strict: false)
1294
+ end
1295
+
1296
+ _init_credential_provider_builder = Polars.method(:_init_credential_provider_builder)
1297
+
1298
+ if !target.is_a?(DeltaLake::Table)
1299
+ credential_provider_builder = _init_credential_provider_builder.(
1300
+ credential_provider, target, storage_options, "sink_delta"
1301
+ )
1302
+ elsif !credential_provider.nil? && credential_provider != "auto"
1303
+ msg = "cannot use credential_provider when passing a DeltaTable object"
1304
+ raise ArgumentError, msg
1305
+ else
1306
+ credential_provider_builder = nil
1307
+ end
1308
+
1309
+ credential_provider_creds = {}
1310
+
1311
+ if credential_provider_builder
1312
+ raise Todo
1313
+ end
1314
+
1315
+ # We aren't calling into polars-native write functions so we just update
1316
+ # the storage_options here.
1317
+ storage_options =
1318
+ if !storage_options.nil? || !credential_provider_builder.nil?
1319
+ (storage_options || {}).merge(credential_provider_creds)
1320
+ else
1321
+ nil
1322
+ end
1323
+
1324
+ stream = collect_batches(
1325
+ engine: "streaming",
1326
+ maintain_order: true,
1327
+ chunk_size: nil,
1328
+ lazy: true,
1329
+ optimizations: optimizations
1330
+ )
1331
+
1332
+ if mode == "merge"
1333
+ if delta_merge_options.nil?
1334
+ msg = "you need to pass delta_merge_options with at least a given predicate for `MERGE` to work."
1335
+ raise ArgumentError, msg
1336
+ end
1337
+ if target.is_a?(::String)
1338
+ dt = DeltaLake::Table.new(target, storage_options: storage_options)
1339
+ else
1340
+ dt = target
1341
+ end
1342
+
1343
+ dt.merge(stream, **delta_merge_options)
1344
+ else
1345
+ if delta_write_options.nil?
1346
+ delta_write_options = {}
1347
+ end
1348
+
1349
+ DeltaLake.write(
1350
+ target,
1351
+ stream,
1352
+ mode: mode,
1353
+ storage_options: storage_options,
1354
+ **delta_write_options
1355
+ )
1356
+ nil
1357
+ end
1358
+ end
1359
+
1178
1360
  # Evaluate the query in streaming mode and write to an IPC file.
1179
1361
  #
1180
1362
  # This allows streaming results that are larger than RAM to be written to disk.
@@ -1591,6 +1773,71 @@ module Polars
1591
1773
  LazyFrame._from_rbldf(ldf_rb)
1592
1774
  end
1593
1775
 
1776
+ # Evaluate the query and call a user-defined function for every ready batch.
1777
+ #
1778
+ # This allows streaming results that are larger than RAM in certain cases.
1779
+ #
1780
+ # @note
1781
+ # This functionality is considered **unstable**. It may be changed
1782
+ # at any point without it being considered a breaking change.
1783
+ #
1784
+ # @note
1785
+ # This method is much slower than native sinks. Only use it if you cannot
1786
+ # implement your logic otherwise.
1787
+ #
1788
+ # @param chunk_size [Integer]
1789
+ # The number of rows that are buffered before the callback is called.
1790
+ # @param maintain_order [Boolean]
1791
+ # Maintain the order in which data is processed.
1792
+ # Setting this to `false` will be slightly faster.
1793
+ # @param lazy [Boolean]
1794
+ # Wait to start execution until `collect` is called.
1795
+ # @param engine [String]
1796
+ # Select the engine used to process the query, optional.
1797
+ # At the moment, if set to `"auto"` (default), the query is run
1798
+ # using the polars streaming engine. Polars will also
1799
+ # attempt to use the engine set by the `POLARS_ENGINE_AFFINITY`
1800
+ # environment variable. If it cannot run the query using the
1801
+ # selected engine, the query is run using the polars streaming
1802
+ # engine.
1803
+ # @param optimizations [Object]
1804
+ # The optimization passes done during query optimization.
1805
+ #
1806
+ # This has no effect if `lazy` is set to `True`.
1807
+ #
1808
+ # @return [Object]
1809
+ #
1810
+ # @example
1811
+ # lf = Polars.scan_csv("/path/to/my_larger_than_ram_file.csv")
1812
+ # lf.sink_batches { |df| p df }
1813
+ def sink_batches(
1814
+ chunk_size: nil,
1815
+ maintain_order: true,
1816
+ lazy: false,
1817
+ engine: "auto",
1818
+ optimizations: DEFAULT_QUERY_OPT_FLAGS,
1819
+ &function
1820
+ )
1821
+ _wrap = lambda do |rbdf|
1822
+ df = Utils.wrap_df(rbdf)
1823
+ !!function.(df)
1824
+ end
1825
+
1826
+ ldf = _ldf.sink_batches(
1827
+ _wrap,
1828
+ maintain_order,
1829
+ chunk_size
1830
+ )
1831
+
1832
+ if !lazy
1833
+ ldf = ldf.with_optimizations(optimizations._rboptflags)
1834
+ lf = LazyFrame._from_rbldf(ldf)
1835
+ lf.collect(engine: engine)
1836
+ return nil
1837
+ end
1838
+ LazyFrame._from_rbldf(ldf)
1839
+ end
1840
+
1594
1841
  # Evaluate the query in streaming mode and get a generator that returns chunks.
1595
1842
  #
1596
1843
  # This allows streaming results that are larger than RAM to be written to disk.
@@ -3234,6 +3481,78 @@ module Polars
3234
3481
  )
3235
3482
  end
3236
3483
 
3484
+ # Selects rows from this LazyFrame at the given indices.
3485
+ #
3486
+ # @note
3487
+ # This functionality is experimental. It may be
3488
+ # changed at any point without it being considered a breaking change.
3489
+ #
3490
+ # @param indices [Object]
3491
+ # The indices of the rows to select.
3492
+ #
3493
+ # Due to the lack of a `LazySeries` it's permitted to pass a single-width
3494
+ # `LazyFrame` as indices as well.
3495
+ # @param null_on_oob [Boolean]
3496
+ # If true when an index is out-of-bounds a null row will be generated
3497
+ # instead of raising an error.
3498
+ #
3499
+ # @return [LazyFrame]
3500
+ #
3501
+ # @example
3502
+ # lf = Polars::LazyFrame.new({"x" => [2, 1, 0], "s" => ["foo", "bar", "baz"]})
3503
+ # lf.gather([2, 0, 0]).collect
3504
+ # # =>
3505
+ # # shape: (3, 2)
3506
+ # # ┌─────┬─────┐
3507
+ # # │ x ┆ s │
3508
+ # # │ --- ┆ --- │
3509
+ # # │ i64 ┆ str │
3510
+ # # ╞═════╪═════╡
3511
+ # # │ 0 ┆ baz │
3512
+ # # │ 2 ┆ foo │
3513
+ # # │ 2 ┆ foo │
3514
+ # # └─────┴─────┘
3515
+ #
3516
+ # @example
3517
+ # lf.gather([0, 10, 1], null_on_oob: true).collect
3518
+ # # =>
3519
+ # # shape: (3, 2)
3520
+ # # ┌──────┬──────┐
3521
+ # # │ x ┆ s │
3522
+ # # │ --- ┆ --- │
3523
+ # # │ i64 ┆ str │
3524
+ # # ╞══════╪══════╡
3525
+ # # │ 2 ┆ foo │
3526
+ # # │ null ┆ null │
3527
+ # # │ 1 ┆ bar │
3528
+ # # └──────┴──────┘
3529
+ #
3530
+ # @example
3531
+ # idxs = Polars::LazyFrame.new({"i" => [1, 10, 0], "b" => [true, false, true]})
3532
+ # lf.gather(idxs.filter(Polars.col("b")).select(Polars.col("i"))).collect
3533
+ # # =>
3534
+ # # shape: (2, 2)
3535
+ # # ┌─────┬─────┐
3536
+ # # │ x ┆ s │
3537
+ # # │ --- ┆ --- │
3538
+ # # │ i64 ┆ str │
3539
+ # # ╞═════╪═════╡
3540
+ # # │ 1 ┆ bar │
3541
+ # # │ 2 ┆ foo │
3542
+ # # └─────┴─────┘
3543
+ def gather(indices, null_on_oob: false)
3544
+ if !indices.is_a?(LazyFrame)
3545
+ if indices.is_a?(::Array)
3546
+ indices_expr = F.lit(Series.new("", indices, dtype: Int64))
3547
+ else
3548
+ indices_expr = wrap_expr(Utils.parse_into_expression(indices))
3549
+ end
3550
+ indices = select(indices_expr)
3551
+ end
3552
+
3553
+ _from_rbldf(_ldf.gather(indices._ldf, null_on_oob))
3554
+ end
3555
+
3237
3556
  # Add or overwrite multiple columns in a DataFrame.
3238
3557
  #
3239
3558
  # @param exprs [Object]
@@ -4398,6 +4717,8 @@ module Polars
4398
4717
  # @param separator [String]
4399
4718
  # Used as separator/delimiter in generated column names in case of multiple
4400
4719
  # `values` columns.
4720
+ # @param column_naming ['auto', 'combine']
4721
+ # How resulting column names will be constructed.
4401
4722
  #
4402
4723
  # @return [LazyFrame]
4403
4724
  #
@@ -4437,7 +4758,8 @@ module Polars
4437
4758
  values: nil,
4438
4759
  aggregate_function: nil,
4439
4760
  maintain_order: false,
4440
- separator: "_"
4761
+ separator: "_",
4762
+ column_naming: "auto"
4441
4763
  )
4442
4764
  if index.nil? && values.nil?
4443
4765
  msg = "`pivot` needs either `index or `values` needs to be specified"
@@ -4511,7 +4833,8 @@ module Polars
4511
4833
  values_selector._rbselector,
4512
4834
  agg._rbexpr,
4513
4835
  maintain_order,
4514
- separator
4836
+ separator,
4837
+ column_naming
4515
4838
  )
4516
4839
  )
4517
4840
  end
@@ -4527,7 +4850,8 @@ module Polars
4527
4850
  #
4528
4851
  # @param on [Object]
4529
4852
  # Column(s) or selector(s) to use as values variables; if `on`
4530
- # is empty all columns that are not in `index` will be used.
4853
+ # is empty no columns will be used. If set to `nil` (default)
4854
+ # all columns that are not in `index` will be used.
4531
4855
  # @param index [Object]
4532
4856
  # Column(s) or selector(s) to use as identifier variables.
4533
4857
  # @param variable_name [String]
@@ -4575,12 +4899,12 @@ module Polars
4575
4899
  warn "The `streamable` parameter for `LazyFrame.unpivot` is deprecated"
4576
4900
  end
4577
4901
 
4578
- selector_on = on.nil? ? Selectors.empty : Utils.parse_list_into_selector(on)
4902
+ selector_on = on.nil? ? nil : Utils.parse_list_into_selector(on)._rbselector
4579
4903
  selector_index = index.nil? ? Selectors.empty : Utils.parse_list_into_selector(index)
4580
4904
 
4581
4905
  _from_rbldf(
4582
4906
  _ldf.unpivot(
4583
- selector_on._rbselector,
4907
+ selector_on,
4584
4908
  selector_index._rbselector,
4585
4909
  value_name,
4586
4910
  variable_name
@@ -4660,10 +4984,10 @@ module Polars
4660
4984
  # # │ -2 ┆ 199998 │
4661
4985
  # # └─────────┴────────┘
4662
4986
  def map_batches(
4663
- predicate_pushdown: true,
4664
- projection_pushdown: true,
4665
- slice_pushdown: true,
4666
- no_optimizations: false,
4987
+ predicate_pushdown: false,
4988
+ projection_pushdown: false,
4989
+ slice_pushdown: false,
4990
+ no_optimizations: nil,
4667
4991
  schema: nil,
4668
4992
  validate_output_schema: true,
4669
4993
  streamable: false,
@@ -4775,7 +5099,7 @@ module Polars
4775
5099
  # # │ foo ┆ 1 ┆ a ┆ true ┆ [1, 2] ┆ baz │
4776
5100
  # # │ bar ┆ 2 ┆ b ┆ null ┆ [3] ┆ womp │
4777
5101
  # # └────────┴─────┴─────┴──────┴───────────┴───────┘
4778
- def unnest(columns, *more_columns, separator: nil)
5102
+ def unnest(columns = nil, *more_columns, separator: nil)
4779
5103
  subset = Utils.parse_list_into_selector(columns) | Utils.parse_list_into_selector(
4780
5104
  more_columns
4781
5105
  )
@@ -4794,6 +5118,10 @@ module Polars
4794
5118
  # Other DataFrame that must be merged
4795
5119
  # @param key [String]
4796
5120
  # Key that is sorted.
5121
+ # @param maintain_order [Boolean]
5122
+ # If `true`, the output is guaranteed to have left-biased ordering
5123
+ # for equal keys: rows from the left frame appear before rows from
5124
+ # the right frame when their keys are equal.
4797
5125
  #
4798
5126
  # @return [LazyFrame]
4799
5127
  #
@@ -4820,8 +5148,8 @@ module Polars
4820
5148
  # # │ steve ┆ 42 │
4821
5149
  # # │ elise ┆ 44 │
4822
5150
  # # └────────┴─────┘
4823
- def merge_sorted(other, key)
4824
- _from_rbldf(_ldf.merge_sorted(other._ldf, key))
5151
+ def merge_sorted(other, key, maintain_order: false)
5152
+ _from_rbldf(_ldf.merge_sorted(other._ldf, key, maintain_order))
4825
5153
  end
4826
5154
 
4827
5155
  # Flag a column as sorted.
@@ -5126,6 +5454,181 @@ module Polars
5126
5454
  _from_rbldf(_ldf.count)
5127
5455
  end
5128
5456
 
5457
+ # Match or evolve the schema of a LazyFrame into a specific schema.
5458
+ #
5459
+ # By default, match_to_schema returns an error if the input schema does not
5460
+ # exactly match the target schema. It also allows columns to be freely reordered,
5461
+ # with additional coercion rules available through optional parameters.
5462
+ #
5463
+ # @note
5464
+ # This functionality is considered **unstable**. It may be changed
5465
+ # at any point without it being considered a breaking change.
5466
+ #
5467
+ # @param schema [Object]
5468
+ # Target schema to match or evolve to.
5469
+ # @param missing_columns [Object]
5470
+ # Raise of insert missing columns from the input with respect to the `schema`.
5471
+ #
5472
+ # This can also be an expression per column with what to insert if it is
5473
+ # missing.
5474
+ # @param missing_struct_fields [Object]
5475
+ # Raise of insert missing struct fields from the input with respect to the
5476
+ # `schema`.
5477
+ # @param extra_columns [Object]
5478
+ # Raise of ignore extra columns from the input with respect to the `schema`.
5479
+ # @param extra_struct_fields [Object]
5480
+ # Raise of ignore extra struct fields from the input with respect to the
5481
+ # `schema`.
5482
+ # @param integer_cast [Object]
5483
+ # Forbid of upcast for integer columns from the input to the respective column
5484
+ # in `schema`.
5485
+ # @param float_cast [Object]
5486
+ # Forbid of upcast for float columns from the input to the respective column
5487
+ # in `schema`.
5488
+ #
5489
+ # @return [LazyFrame]
5490
+ #
5491
+ # @example Ensuring the schema matches
5492
+ # lf = Polars::LazyFrame.new({"a" => [1, 2, 3], "b" => ["A", "B", "C"]})
5493
+ # lf.match_to_schema({"a" => Polars::Int64, "b" => Polars::String}).collect
5494
+ # # =>
5495
+ # # shape: (3, 2)
5496
+ # # ┌─────┬─────┐
5497
+ # # │ a ┆ b │
5498
+ # # │ --- ┆ --- │
5499
+ # # │ i64 ┆ str │
5500
+ # # ╞═════╪═════╡
5501
+ # # │ 1 ┆ A │
5502
+ # # │ 2 ┆ B │
5503
+ # # │ 3 ┆ C │
5504
+ # # └─────┴─────┘
5505
+ #
5506
+ # @example Adding missing columns
5507
+ # Polars::LazyFrame.new({"a" => [1, 2, 3]})
5508
+ # .match_to_schema(
5509
+ # {"a" => Polars::Int64, "b" => Polars::String},
5510
+ # missing_columns: "insert"
5511
+ # )
5512
+ # .collect
5513
+ # # =>
5514
+ # # shape: (3, 2)
5515
+ # # ┌─────┬──────┐
5516
+ # # │ a ┆ b │
5517
+ # # │ --- ┆ --- │
5518
+ # # │ i64 ┆ str │
5519
+ # # ╞═════╪══════╡
5520
+ # # │ 1 ┆ null │
5521
+ # # │ 2 ┆ null │
5522
+ # # │ 3 ┆ null │
5523
+ # # └─────┴──────┘
5524
+ #
5525
+ # @example
5526
+ # Polars::LazyFrame.new({"a" => [1, 2, 3]})
5527
+ # .match_to_schema(
5528
+ # {"a" => Polars::Int64, "b" => Polars::String},
5529
+ # missing_columns: {"b" => Polars.col("a").cast(Polars::String)}
5530
+ # )
5531
+ # .collect
5532
+ # # =>
5533
+ # # shape: (3, 2)
5534
+ # # ┌─────┬─────┐
5535
+ # # │ a ┆ b │
5536
+ # # │ --- ┆ --- │
5537
+ # # │ i64 ┆ str │
5538
+ # # ╞═════╪═════╡
5539
+ # # │ 1 ┆ 1 │
5540
+ # # │ 2 ┆ 2 │
5541
+ # # │ 3 ┆ 3 │
5542
+ # # └─────┴─────┘
5543
+ #
5544
+ # @example Removing extra columns
5545
+ # Polars::LazyFrame.new({"a" => [1, 2, 3], "b" => ["A", "B", "C"]})
5546
+ # .match_to_schema(
5547
+ # {"a" => Polars::Int64},
5548
+ # extra_columns: "ignore"
5549
+ # )
5550
+ # .collect
5551
+ # # =>
5552
+ # # shape: (3, 1)
5553
+ # # ┌─────┐
5554
+ # # │ a │
5555
+ # # │ --- │
5556
+ # # │ i64 │
5557
+ # # ╞═════╡
5558
+ # # │ 1 │
5559
+ # # │ 2 │
5560
+ # # │ 3 │
5561
+ # # └─────┘
5562
+ #
5563
+ # @example Upcasting integers and floats
5564
+ # Polars::LazyFrame.new(
5565
+ # {"a" => [1, 2, 3], "b" => [1.0, 2.0, 3.0]},
5566
+ # schema: {"a" => Polars::Int32, "b" => Polars::Float32}
5567
+ # )
5568
+ # .match_to_schema(
5569
+ # {"a" => Polars::Int64, "b" => Polars::Float64},
5570
+ # integer_cast: "upcast",
5571
+ # float_cast: "upcast"
5572
+ # )
5573
+ # .collect
5574
+ # # =>
5575
+ # # shape: (3, 2)
5576
+ # # ┌─────┬─────┐
5577
+ # # │ a ┆ b │
5578
+ # # │ --- ┆ --- │
5579
+ # # │ i64 ┆ f64 │
5580
+ # # ╞═════╪═════╡
5581
+ # # │ 1 ┆ 1.0 │
5582
+ # # │ 2 ┆ 2.0 │
5583
+ # # │ 3 ┆ 3.0 │
5584
+ # # └─────┴─────┘
5585
+ def match_to_schema(
5586
+ schema,
5587
+ missing_columns: "raise",
5588
+ missing_struct_fields: "raise",
5589
+ extra_columns: "raise",
5590
+ extra_struct_fields: "raise",
5591
+ integer_cast: "forbid",
5592
+ float_cast: "forbid"
5593
+ )
5594
+ prepare_missing_columns = lambda do |value|
5595
+ if value.is_a?(Expr)
5596
+ value._rbexpr
5597
+ else
5598
+ value
5599
+ end
5600
+ end
5601
+
5602
+ if schema.is_a?(Hash)
5603
+ schema_prep = Schema.new(schema)
5604
+ else
5605
+ schema_prep = schema
5606
+ end
5607
+
5608
+ if missing_columns.is_a?(Hash)
5609
+ missing_columns_rbexpr =
5610
+ missing_columns.to_h do |key, value|
5611
+ [key.to_s, prepare_missing_columns.(value)]
5612
+ end
5613
+ elsif missing_columns.is_a?(Expr)
5614
+ missing_columns_rbexpr = prepare_missing_columns.(missing_columns)
5615
+ else
5616
+ missing_columns_rbexpr = missing_columns
5617
+ end
5618
+
5619
+ LazyFrame._from_rbldf(
5620
+ _ldf.match_to_schema(
5621
+ schema_prep,
5622
+ missing_columns_rbexpr,
5623
+ missing_struct_fields,
5624
+ extra_columns,
5625
+ extra_struct_fields,
5626
+ integer_cast,
5627
+ float_cast
5628
+ )
5629
+ )
5630
+ end
5631
+
5129
5632
  private
5130
5633
 
5131
5634
  def initialize_copy(other)