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.
- checksums.yaml +4 -4
- data/PERFORMANCE_NOTES.md +53 -0
- data/README.md +33 -8
- data/Rakefile +11 -0
- data/benchmark/baseline.json +366 -0
- data/benchmark/harness.rb +109 -0
- data/benchmark/microbench.rb +344 -0
- data/benchmark/profile.rb +33 -0
- data/bin/purplelight +4 -4
- data/lib/purplelight/manifest.rb +68 -22
- data/lib/purplelight/partitioner.rb +11 -5
- data/lib/purplelight/queue.rb +9 -3
- data/lib/purplelight/snapshot.rb +91 -55
- data/lib/purplelight/telemetry.rb +0 -1
- data/lib/purplelight/version.rb +1 -1
- data/lib/purplelight/writer_csv.rb +76 -45
- data/lib/purplelight/writer_jsonl.rb +57 -24
- data/lib/purplelight/writer_parquet.rb +268 -145
- metadata +14 -15
|
@@ -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
|
-
|
|
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
|
|
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
|
|
79
|
-
|
|
80
|
-
@columns
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
95
|
+
batch
|
|
87
96
|
end
|
|
88
97
|
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
125
|
+
|
|
126
|
+
data_type ? data_type.build_array(values) : Arrow::ArrayBuilder.build(values)
|
|
114
127
|
end
|
|
115
128
|
|
|
116
|
-
def
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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]
|
|
150
|
-
|
|
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
|
|
335
|
+
return if buffered_count.zero?
|
|
158
336
|
|
|
159
|
-
while
|
|
337
|
+
while buffered_count >= @row_group_size
|
|
160
338
|
ensure_open!
|
|
161
|
-
group =
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
389
|
+
remaining = buffered_count
|
|
218
390
|
t_tbl = Thread.current[:pl_telemetry]&.start(:parquet_table_build_time)
|
|
219
|
-
|
|
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
|
-
|
|
402
|
+
write_record_batch(batch, @writer_path)
|
|
231
403
|
Thread.current[:pl_telemetry]&.finish(:parquet_write_time, t_w)
|
|
232
|
-
rows_written =
|
|
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
|
-
|
|
249
|
-
return nil unless
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
284
|
-
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
52
|
+
version: '2.24'
|
|
59
53
|
- - ">="
|
|
60
54
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 2.
|
|
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.
|
|
62
|
+
version: '2.24'
|
|
69
63
|
- - ">="
|
|
70
64
|
- !ruby/object:Gem::Version
|
|
71
|
-
version: 2.
|
|
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:
|
|
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: []
|