|
| 1 | +import csv |
1 | 2 | import json |
2 | 3 | from datetime import datetime |
3 | 4 |
|
@@ -321,3 +322,163 @@ def test_write_table_rows_to_tabular_file_reads_ndjson(tmp_path): |
321 | 322 | assert table.column_names == ['entry_id', 'upload_id', 'value'] |
322 | 323 | assert table['entry_id'].to_pylist() == ['one', 'two'] |
323 | 324 | assert table['value'].to_pylist() == ['first', 'second'] |
| 325 | + |
| 326 | + |
| 327 | +def test_write_table_rows_to_tabular_file_builds_wide_multi_row_batches( |
| 328 | + tmp_path, |
| 329 | + monkeypatch, |
| 330 | +): |
| 331 | + rows_path = tmp_path / 'rows.ndjson' |
| 332 | + output_path = tmp_path / 'rows.parquet' |
| 333 | + quantity_defs = {f'value_{index:03}': Quantity(type=str) for index in range(64)} |
| 334 | + rows = [ |
| 335 | + { |
| 336 | + 'entry_id': f'entry_{index}', |
| 337 | + 'upload_id': 'upload', |
| 338 | + f'value_{index:03}': f'content_{index}', |
| 339 | + } |
| 340 | + for index in range(5) |
| 341 | + ] |
| 342 | + rows_path.write_text( |
| 343 | + ''.join(f'{json.dumps(row, separators=(",", ":"))}\n' for row in rows), |
| 344 | + encoding='utf-8', |
| 345 | + ) |
| 346 | + batch_sizes = [] |
| 347 | + original_converter = utils._table_rows_to_arrow_batch |
| 348 | + |
| 349 | + def capture_batch_size(batch_rows, *args, **kwargs): |
| 350 | + batch_sizes.append(len(batch_rows)) |
| 351 | + return original_converter(batch_rows, *args, **kwargs) |
| 352 | + |
| 353 | + monkeypatch.setattr(utils, '_table_rows_to_arrow_batch', capture_batch_size) |
| 354 | + count = utils.write_table_rows_to_tabular_file( |
| 355 | + rows_path, |
| 356 | + output_path, |
| 357 | + quantity_defs, |
| 358 | + max_buffer_bytes=1024 * 1024, |
| 359 | + max_buffer_rows=3, |
| 360 | + ) |
| 361 | + |
| 362 | + table = pq.read_table(output_path) |
| 363 | + parquet_file = pq.ParquetFile(output_path) |
| 364 | + expected_batch_sizes = [3, 2] |
| 365 | + assert count == len(rows) |
| 366 | + assert batch_sizes == expected_batch_sizes |
| 367 | + assert parquet_file.metadata.num_row_groups == len(expected_batch_sizes) |
| 368 | + assert table['entry_id'].to_pylist() == [row['entry_id'] for row in rows] |
| 369 | + |
| 370 | + |
| 371 | +def test_write_table_rows_to_tabular_file_flushes_on_input_bytes( |
| 372 | + tmp_path, |
| 373 | + monkeypatch, |
| 374 | +): |
| 375 | + rows_path = tmp_path / 'rows.ndjson' |
| 376 | + output_path = tmp_path / 'rows.parquet' |
| 377 | + rows = [ |
| 378 | + {'entry_id': f'entry_{index}', 'upload_id': 'upload', 'value': 'content'} |
| 379 | + for index in range(3) |
| 380 | + ] |
| 381 | + encoded_lines = [ |
| 382 | + f'{json.dumps(row, separators=(",", ":"))}\n'.encode() for row in rows |
| 383 | + ] |
| 384 | + rows_path.write_bytes(b''.join(encoded_lines)) |
| 385 | + batch_sizes = [] |
| 386 | + original_converter = utils._table_rows_to_arrow_batch |
| 387 | + |
| 388 | + def capture_batch_size(batch_rows, *args, **kwargs): |
| 389 | + batch_sizes.append(len(batch_rows)) |
| 390 | + return original_converter(batch_rows, *args, **kwargs) |
| 391 | + |
| 392 | + monkeypatch.setattr(utils, '_table_rows_to_arrow_batch', capture_batch_size) |
| 393 | + |
| 394 | + count = utils.write_table_rows_to_tabular_file( |
| 395 | + rows_path, |
| 396 | + output_path, |
| 397 | + {'value': Quantity(type=str)}, |
| 398 | + max_buffer_bytes=len(encoded_lines[0]) + len(encoded_lines[1]), |
| 399 | + ) |
| 400 | + |
| 401 | + expected_batch_sizes = [2, 1] |
| 402 | + assert count == len(rows) |
| 403 | + assert batch_sizes == expected_batch_sizes |
| 404 | + assert pq.ParquetFile(output_path).metadata.num_row_groups == len( |
| 405 | + expected_batch_sizes |
| 406 | + ) |
| 407 | + |
| 408 | + |
| 409 | +def test_write_table_rows_to_tabular_file_writes_oversized_row_immediately( |
| 410 | + tmp_path, |
| 411 | + monkeypatch, |
| 412 | +): |
| 413 | + rows_path = tmp_path / 'rows.ndjson' |
| 414 | + output_path = tmp_path / 'rows.parquet' |
| 415 | + rows = [ |
| 416 | + {'entry_id': 'large', 'upload_id': 'upload', 'value': 'x' * 1000}, |
| 417 | + {'entry_id': 'small', 'upload_id': 'upload', 'value': 'x'}, |
| 418 | + ] |
| 419 | + encoded_lines = [ |
| 420 | + f'{json.dumps(row, separators=(",", ":"))}\n'.encode() for row in rows |
| 421 | + ] |
| 422 | + rows_path.write_bytes(b''.join(encoded_lines)) |
| 423 | + batch_sizes = [] |
| 424 | + warnings = [] |
| 425 | + original_converter = utils._table_rows_to_arrow_batch |
| 426 | + |
| 427 | + def capture_batch_size(batch_rows, *args, **kwargs): |
| 428 | + batch_sizes.append(len(batch_rows)) |
| 429 | + return original_converter(batch_rows, *args, **kwargs) |
| 430 | + |
| 431 | + class CapturingLogger: |
| 432 | + def info(self, message): |
| 433 | + pass |
| 434 | + |
| 435 | + def warning(self, message, **kwargs): |
| 436 | + warnings.append((message, kwargs)) |
| 437 | + |
| 438 | + monkeypatch.setattr(utils, '_table_rows_to_arrow_batch', capture_batch_size) |
| 439 | + |
| 440 | + count = utils.write_table_rows_to_tabular_file( |
| 441 | + rows_path, |
| 442 | + output_path, |
| 443 | + {'value': Quantity(type=str)}, |
| 444 | + max_buffer_bytes=len(encoded_lines[1]) + 1, |
| 445 | + logger=CapturingLogger(), |
| 446 | + ) |
| 447 | + |
| 448 | + assert count == len(rows) |
| 449 | + assert batch_sizes == [1, 1] |
| 450 | + assert len(warnings) == 1 |
| 451 | + assert warnings[0][1]['entry_id'] == 'large' |
| 452 | + assert warnings[0][1]['row_input_bytes'] == len(encoded_lines[0]) |
| 453 | + |
| 454 | + |
| 455 | +def test_write_table_rows_to_tabular_file_stringifies_nested_csv_values(tmp_path): |
| 456 | + rows_path = tmp_path / 'rows.ndjson' |
| 457 | + output_path = tmp_path / 'rows.csv' |
| 458 | + values = [['one', 'two'], None, ['three']] |
| 459 | + rows = [ |
| 460 | + { |
| 461 | + 'entry_id': f'entry_{index}', |
| 462 | + 'upload_id': 'upload', |
| 463 | + 'values': value, |
| 464 | + } |
| 465 | + for index, value in enumerate(values) |
| 466 | + ] |
| 467 | + rows_path.write_text( |
| 468 | + ''.join(f'{json.dumps(row, separators=(",", ":"))}\n' for row in rows), |
| 469 | + encoding='utf-8', |
| 470 | + ) |
| 471 | + count = utils.write_table_rows_to_tabular_file( |
| 472 | + rows_path, |
| 473 | + output_path, |
| 474 | + {'values': Quantity(type=str, shape=['*'])}, |
| 475 | + max_buffer_rows=2, |
| 476 | + ) |
| 477 | + |
| 478 | + with output_path.open(newline='', encoding='utf-8') as output_file: |
| 479 | + output_rows = list(csv.DictReader(output_file)) |
| 480 | + assert count == len(rows) |
| 481 | + assert [row['entry_id'] for row in output_rows] == [row['entry_id'] for row in rows] |
| 482 | + assert [ |
| 483 | + json.loads(row['values']) if row['values'] else None for row in output_rows |
| 484 | + ] == values |
0 commit comments