purplelight 0.1.13 → 0.1.17

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.
@@ -27,7 +27,7 @@ module Purplelight
27
27
  @rotate_rows = rotate_rows
28
28
 
29
29
  @closed = false
30
- @file_seq = 0
30
+ @file_seq = manifest ? manifest.parts.length : 0
31
31
  @part_index = nil
32
32
  @pq_writer = nil
33
33
  @rows_in_current_file = 0
@@ -37,8 +37,7 @@ module Purplelight
37
37
  end
38
38
 
39
39
  def write_many(array_of_docs)
40
- ensure_open!
41
- array_of_docs.each { |doc| @buffer_docs << doc }
40
+ @buffer_docs.concat(array_of_docs)
42
41
  flush_row_groups_if_needed
43
42
  end
44
43
 
@@ -61,6 +60,9 @@ module Purplelight
61
60
  def reset_buffers
62
61
  @buffer_docs = []
63
62
  @columns = nil
63
+ @buffer_head = 0
64
+ @arrow_schema = nil
65
+ @arrow_data_types = nil
64
66
  @writer_path = nil
65
67
  end
66
68
 
@@ -69,57 +71,235 @@ module Purplelight
69
71
 
70
72
  FileUtils.mkdir_p(@directory)
71
73
  @writer_path = next_part_path
72
- @part_index = @manifest&.open_part!(@writer_path) if @manifest
74
+ @part_index = @manifest.open_part!(@writer_path) if @manifest
73
75
  @rows_in_current_file = 0
74
76
  end
75
77
 
76
78
  # No-op; we now write once on close for simplicity
77
79
 
78
- def build_table(docs)
79
- # Infer columns
80
- @columns ||= infer_columns(docs)
81
- columns = {}
82
- @columns.each do |name|
83
- values = docs.map { |d| extract_value(d, name) }
84
- columns[name] = Arrow::ArrayBuilder.build(values)
80
+ def build_record_batch(documents)
81
+ @columns ||= infer_columns(documents)
82
+ arrays = @columns.each_with_index.map do |name, index|
83
+ values = documents.map { |document| extract_value(document, name) }
84
+ build_arrow_array(values, @arrow_data_types&.at(index))
85
+ end
86
+ batch = if @arrow_schema
87
+ Arrow::RecordBatch.new(@arrow_schema, arrays)
88
+ else
89
+ Arrow::RecordBatch.new(@columns.zip(arrays).to_h)
90
+ end
91
+ unless @arrow_schema
92
+ @arrow_schema = batch.schema
93
+ @arrow_data_types = @arrow_schema.fields.map(&:data_type)
85
94
  end
86
- Arrow::Table.new(columns)
95
+ batch
87
96
  end
88
97
 
89
- def write_table(table, path, append: false) # rubocop:disable Lint/UnusedMethodArgument
90
- # Stream via ArrowFileWriter when available to avoid building huge tables
91
- if defined?(Parquet::ArrowFileWriter)
92
- unless @pq_writer
93
- props = build_writer_properties_for_compression(@compression)
94
- @pq_writer = create_arrow_file_writer(table.schema, path, props)
95
- end
96
- # Prefer passing row_group_size; fallback to single-arg for older APIs
97
- begin
98
- @pq_writer.write_table(table, @row_group_size)
99
- rescue ArgumentError
100
- @pq_writer.write_table(table)
101
- end
102
- return
98
+ def build_arrow_array(values, data_type = nil)
99
+ first = values.find { |value| !value.nil? }
100
+ if first.is_a?(BSON::ObjectId)
101
+ return build_object_id_array(values) if values.all? { |value| value.nil? || value.is_a?(BSON::ObjectId) }
102
+
103
+ return build_string_array(values)
103
104
  end
104
- # Fallback to one-shot save when streaming API is not available
105
- if table.respond_to?(:save)
106
- begin
107
- table.save(path, format: :parquet, compression: normalize_parquet_compression_name(@compression))
108
- rescue StandardError
109
- table.save(path, format: :parquet)
110
- end
111
- return
105
+ return build_string_array(values) if data_type.is_a?(Arrow::StringDataType) ||
106
+ first.is_a?(String) || first.is_a?(Hash)
107
+ if (data_type.is_a?(Arrow::ListDataType) || first.is_a?(Array)) &&
108
+ values.all? { |value| value.nil? || value.is_a?(Array) } &&
109
+ (data_type || values.any? { |value| value && !value.empty? })
110
+ return build_list_array(values, data_type)
111
+ end
112
+ if (data_type.is_a?(Arrow::BooleanDataType) || first.equal?(true) || first.equal?(false)) &&
113
+ values.all? { |value| value.nil? || value.equal?(true) || value.equal?(false) }
114
+ return build_boolean_array(values)
115
+ end
116
+
117
+ integer_spec = integer_array_spec(data_type)
118
+ integer_values = (integer_spec || first.is_a?(Integer)) &&
119
+ values.all? { |value| value.nil? || value.is_a?(Integer) }
120
+ return build_integer_array(values, data_type, integer_spec) if integer_values
121
+ if (data_type.is_a?(Arrow::DoubleDataType) || first.is_a?(Float)) &&
122
+ values.all? { |value| value.nil? || value.is_a?(Float) }
123
+ return build_double_array(values)
112
124
  end
113
- raise 'Parquet writer not available in this environment'
125
+
126
+ data_type ? data_type.build_array(values) : Arrow::ArrayBuilder.build(values)
114
127
  end
115
128
 
116
- def finalize_current_part!
117
- return if @writer_path.nil?
129
+ def build_string_array(values)
130
+ offsets = Array.new(values.length + 1, 0)
131
+ data = +''.b
132
+ values.each_with_index do |value, index|
133
+ data << (value.is_a?(String) ? value : value.to_s) unless value.nil?
134
+ offsets[index + 1] = data.bytesize
135
+ end
136
+ validity, null_count = build_validity(values)
137
+ Arrow::StringArray.new(
138
+ values.length,
139
+ Arrow::Buffer.new(offsets.pack('l<*').freeze),
140
+ Arrow::Buffer.new(data.freeze),
141
+ validity,
142
+ null_count
143
+ )
144
+ end
145
+
146
+ def build_object_id_array(values)
147
+ offsets = Array.new(values.length + 1, 0)
148
+ raw = String.new(capacity: values.length * 12, encoding: Encoding::BINARY)
149
+ values.each_with_index do |value, index|
150
+ # #marshal_dump is BSON's public accessor for an ObjectId's 12 raw bytes.
151
+ raw << value.marshal_dump if value
152
+ offsets[index + 1] = raw.bytesize * 2
153
+ end
154
+ data = raw.unpack1('H*').force_encoding(Encoding::UTF_8)
155
+ validity, null_count = build_validity(values)
156
+ Arrow::StringArray.new(
157
+ values.length,
158
+ Arrow::Buffer.new(offsets.pack('l<*').freeze),
159
+ Arrow::Buffer.new(data.freeze),
160
+ validity,
161
+ null_count
162
+ )
163
+ end
164
+
165
+ def build_list_array(values, data_type)
166
+ offsets = Array.new(values.length + 1, 0)
167
+ flattened = []
168
+ values.each_with_index do |value, index|
169
+ flattened.concat(value) if value
170
+ offsets[index + 1] = flattened.length
171
+ end
172
+ child_type = data_type&.field&.data_type
173
+ child = build_arrow_array(flattened, child_type)
174
+ data_type ||= Arrow::ListDataType.new(child.value_data_type)
175
+ validity, null_count = build_validity(values)
176
+ Arrow::ListArray.new(
177
+ data_type,
178
+ values.length,
179
+ Arrow::Buffer.new(offsets.pack('l<*').freeze),
180
+ child,
181
+ validity,
182
+ null_count
183
+ )
184
+ end
185
+
186
+ def build_boolean_array(values)
187
+ data = "\0".b * ((values.length + 7) / 8)
188
+ values.each_with_index do |value, index|
189
+ next unless value
190
+
191
+ byte_index = index >> 3
192
+ data.setbyte(byte_index, data.getbyte(byte_index) | (1 << (index & 7)))
193
+ end
194
+ validity, null_count = build_validity(values)
195
+ Arrow::BooleanArray.new(values.length, Arrow::Buffer.new(data.freeze), validity, null_count)
196
+ end
197
+
198
+ def build_integer_array(values, data_type, spec)
199
+ non_null_values = values.compact
200
+ minimum, maximum = non_null_values.minmax
201
+ data_type ||= infer_integer_data_type(minimum, maximum)
202
+ spec ||= integer_array_spec(data_type)
203
+ return Arrow::ArrayBuilder.build(values) unless spec
204
+
205
+ array_class, pack_directive, lower_bound, upper_bound = spec
206
+ outside_bounds = minimum && (minimum < lower_bound || maximum > upper_bound)
207
+ raise RangeError, "integer range #{minimum}..#{maximum} exceeds #{data_type}" if outside_bounds
208
+
209
+ packed_values = if non_null_values.length == values.length
210
+ values
211
+ else
212
+ values.map { |value| value.nil? ? 0 : value }
213
+ end
214
+ validity, null_count = build_validity(values)
215
+ array_class.new(
216
+ values.length,
217
+ Arrow::Buffer.new(packed_values.pack(pack_directive).freeze),
218
+ validity,
219
+ null_count
220
+ )
221
+ end
222
+
223
+ def build_double_array(values)
224
+ packed_values = if values.include?(nil)
225
+ values.map { |value| value.nil? ? 0.0 : value }
226
+ else
227
+ values
228
+ end
229
+ validity, null_count = build_validity(values)
230
+ Arrow::DoubleArray.new(
231
+ values.length,
232
+ Arrow::Buffer.new(packed_values.pack('E*').freeze),
233
+ validity,
234
+ null_count
235
+ )
236
+ end
237
+
238
+ def build_validity(values)
239
+ null_count = values.count(nil)
240
+ return [nil, 0] if null_count.zero?
241
+
242
+ bytes = "\0".b * ((values.length + 7) / 8)
243
+ values.each_with_index do |value, index|
244
+ next if value.nil?
245
+
246
+ byte_index = index >> 3
247
+ bytes.setbyte(byte_index, bytes.getbyte(byte_index) | (1 << (index & 7)))
248
+ end
249
+ [Arrow::Buffer.new(bytes.freeze), null_count]
250
+ end
118
251
 
119
- if @pq_writer
120
- @pq_writer.close
121
- @pq_writer = nil
252
+ def infer_integer_data_type(minimum, maximum)
253
+ if minimum.negative?
254
+ return Arrow::Int8DataType.new if minimum >= -128 && maximum <= 127
255
+ return Arrow::Int16DataType.new if minimum >= -32_768 && maximum <= 32_767
256
+ return Arrow::Int32DataType.new if minimum >= -2_147_483_648 && maximum <= 2_147_483_647
257
+
258
+ int64_range = minimum >= -9_223_372_036_854_775_808 && maximum <= 9_223_372_036_854_775_807
259
+ return Arrow::Int64DataType.new if int64_range
260
+ else
261
+ return Arrow::UInt8DataType.new if maximum <= 255
262
+ return Arrow::UInt16DataType.new if maximum <= 65_535
263
+ return Arrow::UInt32DataType.new if maximum <= 4_294_967_295
264
+ return Arrow::UInt64DataType.new if maximum <= 18_446_744_073_709_551_615
122
265
  end
266
+ nil
267
+ end
268
+
269
+ def integer_array_spec(data_type)
270
+ case data_type
271
+ when Arrow::UInt8DataType
272
+ [Arrow::UInt8Array, 'C*', 0, 255]
273
+ when Arrow::UInt16DataType
274
+ [Arrow::UInt16Array, 'S<*', 0, 65_535]
275
+ when Arrow::UInt32DataType
276
+ [Arrow::UInt32Array, 'L<*', 0, 4_294_967_295]
277
+ when Arrow::UInt64DataType
278
+ [Arrow::UInt64Array, 'Q<*', 0, 18_446_744_073_709_551_615]
279
+ when Arrow::Int8DataType
280
+ [Arrow::Int8Array, 'c*', -128, 127]
281
+ when Arrow::Int16DataType
282
+ [Arrow::Int16Array, 's<*', -32_768, 32_767]
283
+ when Arrow::Int32DataType
284
+ [Arrow::Int32Array, 'l<*', -2_147_483_648, 2_147_483_647]
285
+ when Arrow::Int64DataType
286
+ [Arrow::Int64Array, 'q<*', -9_223_372_036_854_775_808, 9_223_372_036_854_775_807]
287
+ end
288
+ end
289
+
290
+ def write_record_batch(batch, path)
291
+ unless @pq_writer
292
+ properties = build_writer_properties_for_compression(@compression)
293
+ properties ||= Parquet::WriterProperties.new
294
+ properties.max_row_group_length = @row_group_size
295
+ @pq_writer = Parquet::ArrowFileWriter.open(batch.schema, path, properties)
296
+ end
297
+ @pq_writer.write(batch)
298
+ end
299
+
300
+ def finalize_current_part!
301
+ @pq_writer.close
302
+ @pq_writer = nil
123
303
  @manifest&.complete_part!(index: @part_index, checksum: nil)
124
304
  @file_seq += 1 unless @single_file
125
305
  @writer_path = nil
@@ -146,77 +326,69 @@ module Purplelight
146
326
  end
147
327
 
148
328
  def extract_value(doc, key)
149
- value = doc[key] || doc[key.to_sym]
150
- # Normalize common MongoDB/BSON types to Parquet-friendly values
151
- return value.to_s if defined?(BSON) && value.is_a?(BSON::ObjectId)
152
-
329
+ value = doc[key]
330
+ value = doc[key.to_sym] if value.nil? && !doc.key?(key)
153
331
  value
154
332
  end
155
333
 
156
334
  def flush_row_groups_if_needed
157
- return if @buffer_docs.empty?
335
+ return if buffered_count.zero?
158
336
 
159
- while @buffer_docs.length >= @row_group_size
337
+ while buffered_count >= @row_group_size
160
338
  ensure_open!
161
- group = @buffer_docs.shift(@row_group_size)
339
+ group = take_buffered(@row_group_size)
162
340
  if @rotate_rows && !@single_file && (@rows_in_current_file + group.length) > @rotate_rows
163
341
  # Write a partial chunk to fill the current file, then rotate and write the rest
164
342
  remaining_allowed = @rotate_rows - @rows_in_current_file
165
- if remaining_allowed.positive?
166
- part_a = group.first(remaining_allowed)
167
- t_tbl = Thread.current[:pl_telemetry]&.start(:parquet_table_build_time)
168
- table_a = build_table(part_a)
169
- Thread.current[:pl_telemetry]&.finish(:parquet_table_build_time, t_tbl)
170
-
171
- t_w = Thread.current[:pl_telemetry]&.start(:parquet_write_time)
172
- write_table(table_a, @writer_path, append: true)
173
- Thread.current[:pl_telemetry]&.finish(:parquet_write_time, t_w)
174
- @manifest&.add_progress_to_part!(index: @part_index, rows_delta: part_a.length, bytes_delta: 0)
175
- @rows_in_current_file += part_a.length
176
- end
343
+ part_a = group.first(remaining_allowed)
344
+ t_tbl = Thread.current[:pl_telemetry]&.start(:parquet_table_build_time)
345
+ batch_a = build_record_batch(part_a)
346
+ Thread.current[:pl_telemetry]&.finish(:parquet_table_build_time, t_tbl)
347
+
348
+ t_w = Thread.current[:pl_telemetry]&.start(:parquet_write_time)
349
+ write_record_batch(batch_a, @writer_path)
350
+ Thread.current[:pl_telemetry]&.finish(:parquet_write_time, t_w)
351
+ @manifest&.add_progress_to_part!(index: @part_index, rows_delta: part_a.length, bytes_delta: 0)
352
+ @rows_in_current_file += part_a.length
177
353
 
178
354
  finalize_current_part!
179
355
  ensure_open!
180
356
 
181
357
  part_b = group.drop(remaining_allowed)
182
- unless part_b.empty?
183
- t_tbl = Thread.current[:pl_telemetry]&.start(:parquet_table_build_time)
184
- table_b = build_table(part_b)
185
- Thread.current[:pl_telemetry]&.finish(:parquet_table_build_time, t_tbl)
186
-
187
- t_w = Thread.current[:pl_telemetry]&.start(:parquet_write_time)
188
- write_table(table_b, @writer_path, append: true)
189
- Thread.current[:pl_telemetry]&.finish(:parquet_write_time, t_w)
190
- @manifest&.add_progress_to_part!(index: @part_index, rows_delta: part_b.length, bytes_delta: 0)
191
- @rows_in_current_file += part_b.length
192
- maybe_rotate!
193
- end
358
+ t_tbl = Thread.current[:pl_telemetry]&.start(:parquet_table_build_time)
359
+ batch_b = build_record_batch(part_b)
360
+ Thread.current[:pl_telemetry]&.finish(:parquet_table_build_time, t_tbl)
361
+
362
+ t_w = Thread.current[:pl_telemetry]&.start(:parquet_write_time)
363
+ write_record_batch(batch_b, @writer_path)
364
+ Thread.current[:pl_telemetry]&.finish(:parquet_write_time, t_w)
365
+ @manifest&.add_progress_to_part!(index: @part_index, rows_delta: part_b.length, bytes_delta: 0)
366
+ @rows_in_current_file += part_b.length
194
367
  else
195
368
  t_tbl = Thread.current[:pl_telemetry]&.start(:parquet_table_build_time)
196
- table = build_table(group)
369
+ batch = build_record_batch(group)
197
370
  Thread.current[:pl_telemetry]&.finish(:parquet_table_build_time, t_tbl)
198
371
 
199
372
  t_w = Thread.current[:pl_telemetry]&.start(:parquet_write_time)
200
- write_table(table, @writer_path, append: true)
373
+ write_record_batch(batch, @writer_path)
201
374
  Thread.current[:pl_telemetry]&.finish(:parquet_write_time, t_w)
202
375
  @manifest&.add_progress_to_part!(index: @part_index, rows_delta: group.length, bytes_delta: 0)
203
376
  @rows_in_current_file += group.length
204
- maybe_rotate!
205
377
  end
378
+ maybe_rotate!
206
379
  end
207
380
  end
208
381
 
209
382
  def flush_all_row_groups
210
- return if @buffer_docs.empty?
383
+ return if buffered_count.zero?
211
384
 
212
385
  # Flush any full groups first
213
386
  flush_row_groups_if_needed
214
- return if @buffer_docs.empty?
215
387
 
216
388
  # Flush remaining as a final smaller group
217
- remaining = @buffer_docs.length
389
+ remaining = buffered_count
218
390
  t_tbl = Thread.current[:pl_telemetry]&.start(:parquet_table_build_time)
219
- table = build_table(@buffer_docs)
391
+ batch = build_record_batch(take_buffered(remaining))
220
392
  Thread.current[:pl_telemetry]&.finish(:parquet_table_build_time, t_tbl)
221
393
 
222
394
  ensure_open!
@@ -227,12 +399,13 @@ module Purplelight
227
399
  end
228
400
 
229
401
  t_w = Thread.current[:pl_telemetry]&.start(:parquet_write_time)
230
- write_table(table, @writer_path, append: true)
402
+ write_record_batch(batch, @writer_path)
231
403
  Thread.current[:pl_telemetry]&.finish(:parquet_write_time, t_w)
232
- rows_written = (table.respond_to?(:n_rows) ? table.n_rows : remaining)
404
+ rows_written = batch.n_rows
233
405
  @manifest&.add_progress_to_part!(index: @part_index, rows_delta: rows_written, bytes_delta: 0)
234
406
  @rows_in_current_file += rows_written
235
407
  @buffer_docs.clear
408
+ @buffer_head = 0
236
409
  maybe_rotate!
237
410
  end
238
411
 
@@ -245,76 +418,26 @@ module Purplelight
245
418
  end
246
419
 
247
420
  def build_writer_properties_for_compression(requested)
248
- codec_const = parquet_codec_constant(requested)
249
- return nil unless codec_const
250
-
251
- # Prefer WriterProperties builder if available
252
- begin
253
- if defined?(Parquet::WriterProperties) && Parquet::WriterProperties.respond_to?(:builder)
254
- builder = Parquet::WriterProperties.builder
255
- if builder.respond_to?(:compression)
256
- builder = builder.compression(codec_const)
257
- elsif builder.respond_to?(:set_compression)
258
- builder = builder.set_compression(codec_const)
259
- end
260
- return builder.build if builder.respond_to?(:build)
261
- end
262
- rescue StandardError
263
- # fall through to other strategies
264
- end
421
+ compression = normalize_parquet_compression_name(requested)
422
+ return nil unless compression
265
423
 
266
- # Alternative builder class naming fallback
267
- begin
268
- if defined?(Parquet::WriterPropertiesBuilder)
269
- b = Parquet::WriterPropertiesBuilder.new
270
- if b.respond_to?(:compression)
271
- b.compression(codec_const)
272
- elsif b.respond_to?(:set_compression)
273
- b.set_compression(codec_const)
274
- end
275
- return b.build if b.respond_to?(:build)
276
- end
277
- rescue StandardError
278
- # ignore
279
- end
280
- nil
424
+ properties = Parquet::WriterProperties.new
425
+ properties.set_compression(compression == 'none' ? 'uncompressed' : compression)
426
+ properties
281
427
  end
282
428
 
283
- def create_arrow_file_writer(schema, path, props)
284
- attempts = []
285
- if props
286
- attempts << -> { Parquet::ArrowFileWriter.open(schema, path, props) }
287
- attempts << -> { Parquet::ArrowFileWriter.open(schema, path, properties: props) }
288
- end
289
- attempts << -> { Parquet::ArrowFileWriter.open(schema, path) }
290
-
291
- attempts.each do |call|
292
- return call.call
293
- rescue StandardError
294
- next
295
- end
296
- raise 'failed to open Parquet::ArrowFileWriter'
429
+ def buffered_count
430
+ @buffer_docs.length - @buffer_head
297
431
  end
298
432
 
299
- def parquet_codec_constant(requested)
300
- name = normalize_parquet_compression_name(requested)
301
- return nil unless name
302
-
303
- up = case name
304
- when 'zstd', 'zstandard' then 'ZSTD'
305
- when 'gzip' then 'GZIP'
306
- when 'snappy' then 'SNAPPY'
307
- when 'none' then 'UNCOMPRESSED'
308
- else name.upcase
309
- end
310
- candidates = %w[CompressionType Compression CompressionCodec]
311
- candidates.each do |mod|
312
- m = Parquet.const_get(mod)
313
- return m.const_get(up) if m.const_defined?(up)
314
- rescue StandardError
315
- next
433
+ def take_buffered(count)
434
+ documents = @buffer_docs.slice(@buffer_head, count)
435
+ @buffer_head += count
436
+ if @buffer_head >= @row_group_size && @buffer_head * 2 >= @buffer_docs.length
437
+ @buffer_docs = @buffer_docs.drop(@buffer_head)
438
+ @buffer_head = 0
316
439
  end
317
- nil
440
+ documents
318
441
  end
319
442
 
320
443
  def normalize_parquet_compression_name(requested)
@@ -324,7 +447,7 @@ module Purplelight
324
447
  return 'none' if s == 'none'
325
448
  return 'gzip' if s == 'gzip'
326
449
  return 'snappy' if s == 'snappy'
327
- return 'zstd' if %w[zstd zstandard].include?(s)
450
+ return 'zstd' if s == 'zstd'
328
451
 
329
452
  nil
330
453
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: purplelight
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.13
4
+ version: 0.1.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Nicholson
@@ -10,25 +10,19 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: csv
13
+ name: bigdecimal
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '3.3'
19
16
  - - ">="
20
17
  - !ruby/object:Gem::Version
21
- version: 3.3.5
18
+ version: '3.1'
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
- - - "~>"
27
- - !ruby/object:Gem::Version
28
- version: '3.3'
29
23
  - - ">="
30
24
  - !ruby/object:Gem::Version
31
- version: 3.3.5
25
+ version: '3.1'
32
26
  - !ruby/object:Gem::Dependency
33
27
  name: logger
34
28
  requirement: !ruby/object:Gem::Requirement
@@ -55,20 +49,20 @@ dependencies:
55
49
  requirements:
56
50
  - - "~>"
57
51
  - !ruby/object:Gem::Version
58
- version: '2.21'
52
+ version: '2.24'
59
53
  - - ">="
60
54
  - !ruby/object:Gem::Version
61
- version: 2.21.3
55
+ version: 2.24.1
62
56
  type: :runtime
63
57
  prerelease: false
64
58
  version_requirements: !ruby/object:Gem::Requirement
65
59
  requirements:
66
60
  - - "~>"
67
61
  - !ruby/object:Gem::Version
68
- version: '2.21'
62
+ version: '2.24'
69
63
  - - ">="
70
64
  - !ruby/object:Gem::Version
71
- version: 2.21.3
65
+ version: 2.24.1
72
66
  description: High-throughput, resumable snapshots of MongoDB collections with partitioning,
73
67
  multi-threaded readers, and size-based sharded outputs.
74
68
  email:
@@ -79,8 +73,13 @@ extensions: []
79
73
  extra_rdoc_files: []
80
74
  files:
81
75
  - LICENSE
76
+ - PERFORMANCE_NOTES.md
82
77
  - README.md
83
78
  - Rakefile
79
+ - benchmark/baseline.json
80
+ - benchmark/harness.rb
81
+ - benchmark/microbench.rb
82
+ - benchmark/profile.rb
84
83
  - bin/purplelight
85
84
  - lib/purplelight.rb
86
85
  - lib/purplelight/errors.rb
@@ -114,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
113
  - !ruby/object:Gem::Version
115
114
  version: '0'
116
115
  requirements: []
117
- rubygems_version: 3.6.7
116
+ rubygems_version: 4.0.6
118
117
  specification_version: 4
119
118
  summary: Snapshot MongoDB collections efficiently to JSONL/CSV/Parquet
120
119
  test_files: []