parquet 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b40af9f0e3f469b64b66a59e050c801ab97ee7aad4927179c9923deeab278d7
4
- data.tar.gz: 18cf939257c201b9485ce009194098260cfffd5b5db8f3e92d59e243b3e54b58
3
+ metadata.gz: 9d6e1c27061809c378723ba1917a02d427034cbcca336cb13018b6782f02a39f
4
+ data.tar.gz: 8682402a2c0007384c851a7a3d5f68f2b758f11ca3b6c196fbae3153f3751848
5
5
  SHA512:
6
- metadata.gz: 0aad32ede402031bb3a6cb9ad617a76c0fc16e03305ad8bf0eea60db31b256cd47a0875d36f690c6cd030735646b880c70490eee461b712e1ad9c20e9cf2f847
7
- data.tar.gz: 83d099424fe612c95d6f16f544594804aed5e5f8e987aa2cac8eff686852fc67c90dec747b8ea8d6137e4833faa02651f1a4f9a0f964e3c1c1d1c08064d70b4d
6
+ metadata.gz: 19075351b7cdc6616c896979d9b4d871aa599f8c8c0d13e52e48aa75d3df3f94f6a1824ae27b81b9321329183a5ad4b2db38647711583b0e8bf885f3f2ab0a3f
7
+ data.tar.gz: e3240b3224aac0e536a8c88e65cf022ded919afb6b8cd193f0f1e064934e1449a13d3ca0c36a9c559dc48a780b76e390dba45b313b4deb3a19ec4f764b61dffa
data/Cargo.lock CHANGED
@@ -1105,6 +1105,7 @@ dependencies = [
1105
1105
  "serde",
1106
1106
  "tempfile",
1107
1107
  "thiserror",
1108
+ "thrift",
1108
1109
  "triomphe",
1109
1110
  "uuid",
1110
1111
  ]
data/README.md CHANGED
@@ -145,6 +145,33 @@ puts metadata["row_groups"].size # Number of row groups
145
145
 
146
146
  ## Writing Parquet Files
147
147
 
148
+ `write_rows` and `write_columns` stream an enumerable to a path or writable IO
149
+ and return `nil`. Each row is an array in schema order. For column writing, each
150
+ yielded batch contains one array per field, all of the same length.
151
+
152
+ Both methods require `schema:` and `write_to:`. `schema:` accepts the array form
153
+ shown below, a `Parquet::Schema` DSL result, or a `fields` schema hash. Use `nil`
154
+ or `[]` to infer string columns named `f0`, `f1`, ... from the first row or
155
+ batch. Empty input requires an explicit schema.
156
+
157
+ `compression:` accepts `"none"`, `"uncompressed"`, `"snappy"`, `"gzip"`,
158
+ `"lz4"`, `"zstd"`, and `"brotli"`; `nil` defaults to Snappy.
159
+ `flush_threshold:` defaults to 100 MiB. `logger:` accepts a Ruby logger with
160
+ `debug`, `info`, `warn`, and `error` methods.
161
+
162
+ `write_rows` also accepts `batch_size:`, `sample_size:`, and `string_cache:`.
163
+ Without `batch_size:`, sizing starts at 1,000 rows and adapts to row size; the
164
+ cap is 1,000,000 rows and lower for wide schemas. Sampling defaults to 100 rows
165
+ and is capped at 10,000. `string_cache: true` uses a capacity of 100, or you can
166
+ pass a capacity up to 65,536; `nil` and `false` disable it.
167
+
168
+ Path output is staged and atomically published only after the complete file has
169
+ been written. On Unix, replacing an existing path preserves its uid, gid, and
170
+ mode and uses standard last-committer-wins rename semantics; creating a path is
171
+ no-clobber. Extended attributes and ACLs are not preserved. IO output is first
172
+ staged on disk, then copied to the IO; a failed copy may leave the IO partially
173
+ written.
174
+
148
175
  ### Row-wise Writing
149
176
 
150
177
  Best for: Streaming data, converting from other formats, memory-constrained environments
@@ -159,17 +186,109 @@ schema = [
159
186
  ]
160
187
 
161
188
  # Stream data from any enumerable
162
- rows = CSV.foreach("input.csv").map do |row|
189
+ rows = CSV.foreach("input.csv").lazy.map do |row|
163
190
  [row[0].to_i, row[1], row[2] == "true", row[3].to_f]
164
191
  end
165
192
 
166
193
  Parquet.write_rows(rows,
167
194
  schema: schema,
168
195
  write_to: "output.parquet",
169
- batch_size: 5000 # Positive rows per batch (default: 1000)
196
+ batch_size: 5000
197
+ )
198
+ ```
199
+
200
+ ### Repacking Existing Parquet Files
201
+
202
+ Concatenate Parquet files and re-split them into differently sized files without
203
+ translating rows through Ruby.
204
+
205
+ ```ruby
206
+ Parquet.repack(
207
+ ["input-0.parquet", "input-1.parquet"],
208
+ output_dir: "repacked",
209
+ rows_per_file: 100_000
170
210
  )
211
+ # => [{ "path" => "repacked/batch-0.parquet", "num_rows" => 100_000 },
212
+ # { "path" => "repacked/batch-1.parquet", "num_rows" => 42_137 }]
213
+ ```
214
+
215
+ The outputs hold exactly the input rows, in input order. Every output but the
216
+ last holds `rows_per_file` rows, and there is always at least one output even
217
+ when the inputs are empty. Omit `rows_per_file:` to concatenate everything into
218
+ a single file.
219
+
220
+ Each output's Parquet schema is identical to the first input's, and that input's
221
+ file-level key/value metadata (`ARROW:schema`, `pandas`, and so on) is carried
222
+ over. Inputs must agree on leaf column shape — path, physical and logical type,
223
+ nesting — but may differ in key/value metadata and Parquet field ids.
224
+
225
+ #### Compression and copying
226
+
227
+ With no `compression:`, each column keeps its own codec — Parquet records one
228
+ per column, and a file may legitimately use several. Naming a codec applies it
229
+ to every column instead:
230
+
231
+ ```ruby
232
+ Parquet.repack("input.parquet", output_dir: "out", compression: "zstd")
171
233
  ```
172
234
 
235
+ Keeping the inputs' codecs also lets repack copy whole row groups into the
236
+ output byte-for-byte, skipping decompression and re-encoding entirely. A row
237
+ group is copied when it fits the output's remaining row budget, is large enough
238
+ to be worth copying, and the request did not ask for a different codec;
239
+ otherwise its rows are decoded and re-encoded. Both routes produce the same
240
+ rows, so which one runs is not something callers need to reason about — but the
241
+ copy route is dramatically faster, so a plain concatenation is close to an
242
+ I/O-bound copy.
243
+
244
+ Small row groups are deliberately merged rather than copied: copying them
245
+ one-for-one would make a compaction of many small files reproduce exactly the
246
+ fragmentation it was meant to remove.
247
+
248
+ #### Output directory ownership
249
+
250
+ `repack` owns the `{output_file_prefix}-{n}.parquet` names in `output_dir`. If
251
+ any already exist it raises `ArgumentError` rather than mixing two runs' files
252
+ in one directory:
253
+
254
+ ```ruby
255
+ Parquet.repack("input.parquet", output_dir: "out", rows_per_file: 1000, overwrite: true)
256
+ ```
257
+
258
+ `overwrite: true` replaces that set and deletes members left over from a longer
259
+ earlier run, so the returned list always equals what a reader finds in the
260
+ directory. Files outside the set are never touched.
261
+
262
+ #### Bounds
263
+
264
+ `max_read_rows_per_chunk:` (default 8192, reduced for wide schemas) bounds rows
265
+ buffered while reading; output row groups are bounded in rows by the same slot
266
+ budget. Both are resource controls: varying them cannot change the returned
267
+ list, the rows, the schema, or the codecs. They do shift compressed byte counts
268
+ and page boundaries, which are representation rather than contract.
269
+
270
+ Input metadata is read one file at a time, so peak memory is set by the widest
271
+ single file and its row-group size, not by how many files you pass or how many
272
+ rows they hold in total. Note that the row-group bound is in rows, not bytes: a
273
+ schema with very large values still buffers one row group's worth of encoded
274
+ data.
275
+
276
+ Reading and writing run with the GVL released, so other Ruby threads keep
277
+ running and `Interrupt` / `Timeout` are honoured. An interrupted call leaves no
278
+ output behind.
279
+
280
+ A repacked output is capped at 32,767 row groups. Merging small groups keeps
281
+ that limit out of reach in practice; if a single output would exceed it,
282
+ repack raises rather than writing an unreadable file, and `rows_per_file:` is
283
+ the way out.
284
+
285
+ #### Page indexes and other optional structures
286
+
287
+ Every row group in a Parquet file must agree on whether it carries a page index,
288
+ and a copied row group can only contribute the index its source had. So an
289
+ output carries one exactly when every contributing input does. Bloom filters are
290
+ not carried over on either route.
291
+
173
292
  ### Column-wise Writing
174
293
 
175
294
  Best for: Pre-columnar data, better compression, higher performance
@@ -196,13 +315,10 @@ schema = [
196
315
  Parquet.write_columns(batches.each,
197
316
  schema: schema,
198
317
  write_to: "output.parquet",
199
- compression: "snappy" # Options: none, snappy, gzip, lz4, zstd
318
+ compression: "snappy"
200
319
  )
201
320
  ```
202
321
 
203
- `write_columns` also accepts `logger:` with the same Ruby logger interface as
204
- row writes.
205
-
206
322
  ## Data Types
207
323
 
208
324
  ### Basic Types
@@ -385,14 +501,19 @@ Control memory usage with flush thresholds:
385
501
  Parquet.write_rows(huge_dataset.each,
386
502
  schema: schema,
387
503
  write_to: "output.parquet",
388
- batch_size: 1000, # Positive rows before considering flush
389
- flush_threshold: 32 * 1024**2 # Flush if batch exceeds 32MB
504
+ batch_size: 1_000,
505
+ flush_threshold: 32 * 1024**2 # 32 MiB
390
506
  )
391
507
  ```
392
508
 
393
- Write batch and sample sizes are bounded before buffer allocation. Very large
394
- batch sizes are rejected, and wide schemas have a lower effective batch cap so
395
- the writer cannot reserve unbounded per-column value slots.
509
+ Writer-owned memory stays bounded as the file grows. `flush_threshold` controls
510
+ the converted-value buffer; a single larger row may exceed it temporarily. The
511
+ row-group target is at least 8 MiB, and each file may contain up to 32,768 row
512
+ groups.
513
+
514
+ Encoded data and completed row-group metadata are staged on disk, so large
515
+ writes need temporary disk space. Ruby still owns the current row or batch, and
516
+ an in-memory destination such as `StringIO` holds the output in memory.
396
517
 
397
518
  ## Architecture
398
519
 
@@ -275,6 +275,12 @@ pub fn write_columns(args: &[Value]) -> Result<Value, MagnusError> {
275
275
  parquet_ruby_adapter::writer::write_columns(&ruby, write_args)
276
276
  }
277
277
 
278
+ pub fn repack(args: &[Value]) -> Result<Value, MagnusError> {
279
+ let ruby = Ruby::get().expect("Ruby FFI entry point runs while the Ruby GVL is held");
280
+
281
+ parquet_ruby_adapter::repack::repack(&ruby, args)
282
+ }
283
+
278
284
  fn reject_row_only_column_write_options(
279
285
  write_args: &parquet_ruby_adapter::types::ParquetWriteArgs,
280
286
  ) -> Result<(), MagnusError> {
@@ -3,7 +3,7 @@ mod allocator;
3
3
 
4
4
  use magnus::{function, method, Error, Ruby};
5
5
 
6
- use crate::adapter_ffi::{each_column, each_row, write_columns, write_rows};
6
+ use crate::adapter_ffi::{each_column, each_row, repack, write_columns, write_rows};
7
7
  use parquet_ruby_adapter::metadata::parse_metadata;
8
8
 
9
9
  /// Initializes the Ruby extension and defines methods.
@@ -19,6 +19,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
19
19
  module.define_module_function("each_column", method!(each_column, -1))?;
20
20
  module.define_module_function("write_rows", function!(write_rows, -1))?;
21
21
  module.define_module_function("write_columns", function!(write_columns, -1))?;
22
+ module.define_module_function("repack", function!(repack, -1))?;
22
23
 
23
24
  Ok(())
24
25
  }
@@ -16,9 +16,8 @@ ordered-float = "5.0.0"
16
16
  parquet = { version = "58.3.0", features = ["arrow", "zstd", "lz4", "snap"] }
17
17
  rand = "0.9.1"
18
18
  serde = { version = "1.0", features = ["derive"] }
19
+ tempfile = "3.8"
19
20
  thiserror = "2.0"
21
+ thrift = { version = "0.17", default-features = false }
20
22
  triomphe = "0.1.15"
21
23
  uuid = { version = "1.0", features = ["v4"] }
22
-
23
- [dev-dependencies]
24
- tempfile = "3.8"
@@ -46,6 +46,7 @@ pub mod arrow_conversion;
46
46
  pub mod error;
47
47
  pub mod reader;
48
48
  pub mod schema;
49
+ mod streaming_file_writer;
49
50
  pub mod traits;
50
51
  pub mod value;
51
52
  pub mod writer;
@@ -57,4 +58,6 @@ pub use error::{ErrorContext, ParquetError, Result};
57
58
  pub use reader::Reader;
58
59
  pub use schema::{PrimitiveType, Repetition, Schema, SchemaBuilder, SchemaNode};
59
60
  pub use value::ParquetValue;
60
- pub use writer::{Writer, WriterBuilder, MAX_BATCH_SIZE, MAX_SAMPLE_SIZE};
61
+ pub use writer::{
62
+ max_batch_size_for_column_count, Writer, WriterBuilder, MAX_BATCH_SIZE, MAX_SAMPLE_SIZE,
63
+ };