pybcsv 1.5.8__tar.gz → 1.5.10.dev0__tar.gz

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 (120) hide show
  1. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/PKG-INFO +39 -1
  2. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/README.md +38 -0
  3. pybcsv-1.5.10.dev0/VERSION.txt +1 -0
  4. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/_version.py +3 -3
  5. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/parquet_utils.py +88 -42
  6. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_parquet_conversion.py +38 -2
  7. pybcsv-1.5.8/VERSION.txt +0 -1
  8. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/BUILD_NOTES.md +0 -0
  9. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/CMakeLists.txt +0 -0
  10. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/LICENSE +0 -0
  11. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/MANIFEST.in +0 -0
  12. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/benchmarks/README.md +0 -0
  13. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/benchmarks/run_pybcsv_benchmarks.py +0 -0
  14. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/examples/README.md +0 -0
  15. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/examples/advanced_usage.py +0 -0
  16. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/examples/basic_usage.py +0 -0
  17. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/examples/pandas_integration.py +0 -0
  18. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/examples/performance_benchmark.py +0 -0
  19. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/bcsv.h +0 -0
  20. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/bcsv.hpp +0 -0
  21. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/bcsv_c_api.h +0 -0
  22. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/bitset.h +0 -0
  23. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/bitset.hpp +0 -0
  24. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/byte_buffer.h +0 -0
  25. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/checksum.hpp +0 -0
  26. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_file/file_codec_concept.h +0 -0
  27. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_file/file_codec_dispatch.h +0 -0
  28. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_file/file_codec_packet001.h +0 -0
  29. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_file/file_codec_packet_lz4_001.h +0 -0
  30. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_file/file_codec_packet_lz4_batch001.h +0 -0
  31. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_file/file_codec_stream001.h +0 -0
  32. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_file/file_codec_stream_lz4_001.h +0 -0
  33. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_row/row_codec_delta002.h +0 -0
  34. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_row/row_codec_delta002.hpp +0 -0
  35. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_row/row_codec_dispatch.h +0 -0
  36. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_row/row_codec_flat001.h +0 -0
  37. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_row/row_codec_flat001.hpp +0 -0
  38. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_row/row_codec_zoh001.h +0 -0
  39. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/codec_row/row_codec_zoh001.hpp +0 -0
  40. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/column_name_index.h +0 -0
  41. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/column_name_index.hpp +0 -0
  42. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/csv_reader.h +0 -0
  43. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/csv_reader.hpp +0 -0
  44. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/csv_writer.h +0 -0
  45. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/csv_writer.hpp +0 -0
  46. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/definitions.h +0 -0
  47. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/file_footer.h +0 -0
  48. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/file_header.h +0 -0
  49. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/file_header.hpp +0 -0
  50. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/layout.h +0 -0
  51. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/layout.hpp +0 -0
  52. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/layout_guard.h +0 -0
  53. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/lz4_block.hpp +0 -0
  54. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/lz4_stream.hpp +0 -0
  55. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/packet_header.h +0 -0
  56. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/reader.h +0 -0
  57. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/reader.hpp +0 -0
  58. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/reader_concept.h +0 -0
  59. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/row.h +0 -0
  60. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/row.hpp +0 -0
  61. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/row_visitors.h +0 -0
  62. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler.h +0 -0
  63. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler.hpp +0 -0
  64. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler_ast.h +0 -0
  65. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler_compiler.h +0 -0
  66. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler_parser.h +0 -0
  67. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler_tokenizer.h +0 -0
  68. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler_types.h +0 -0
  69. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler_vm.h +0 -0
  70. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler_vm.hpp +0 -0
  71. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/sampler/sampler_window.h +0 -0
  72. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/std_charconv_compat.h +0 -0
  73. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/vle.hpp +0 -0
  74. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/writer.h +0 -0
  75. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/writer.hpp +0 -0
  76. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/bcsv/writer_concept.h +0 -0
  77. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/lz4.c +0 -0
  78. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/lz4.h +0 -0
  79. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/lz4file.c +0 -0
  80. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/lz4file.h +0 -0
  81. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/lz4frame.c +0 -0
  82. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/lz4frame.h +0 -0
  83. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/lz4frame_static.h +0 -0
  84. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/lz4hc.c +0 -0
  85. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/lz4hc.h +0 -0
  86. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/xxhash.c +0 -0
  87. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/lz4-1.10.0/xxhash.h +0 -0
  88. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/xxHash-0.8.3/xxhash.c +0 -0
  89. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/include/xxHash-0.8.3/xxhash.h +0 -0
  90. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/__init__.py +0 -0
  91. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/__init__.pyi +0 -0
  92. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/__version__.py +0 -0
  93. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/_bcsv.pyi +0 -0
  94. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/bindings.cpp +0 -0
  95. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/pandas_utils.py +0 -0
  96. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/polars_utils.py +0 -0
  97. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pybcsv/py.typed +0 -0
  98. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/pyproject.toml +0 -0
  99. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/sync_headers.py +0 -0
  100. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/README.md +0 -0
  101. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/conftest.py +0 -0
  102. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_basic_functionality.py +0 -0
  103. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_count_rows.py +0 -0
  104. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_csv_writer_reader.py +0 -0
  105. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_cycle4_python_hardening.py +0 -0
  106. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_data_types.py +0 -0
  107. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_interoperability.py +0 -0
  108. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_minimal_string.py +0 -0
  109. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_performance_edge_cases.py +0 -0
  110. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_arrow.py +0 -0
  111. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_codecs.py +0 -0
  112. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_columnar.py +0 -0
  113. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_core.py +0 -0
  114. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_direct_access.py +0 -0
  115. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_errors.py +0 -0
  116. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_pandas.py +0 -0
  117. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_performance.py +0 -0
  118. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_polars.py +0 -0
  119. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_pybcsv_sampler.py +0 -0
  120. {pybcsv-1.5.8 → pybcsv-1.5.10.dev0}/tests/test_streaming_bindings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybcsv
3
- Version: 1.5.8
3
+ Version: 1.5.10.dev0
4
4
  Summary: High-performance Python bindings for the BCSV (Binary CSV) library with pandas integration
5
5
  Keywords: csv,binary,data-science,compression,performance,pandas,numpy,big-data,data-processing
6
6
  Author-Email: Tobias Weber <weber.tobias.md@gmail.com>
@@ -156,6 +156,44 @@ with pybcsv.ReaderDirectAccess() as da:
156
156
  print(da.read(100)) # alternative syntax
157
157
  ```
158
158
 
159
+ ## Parquet Conversion Tools (CLI)
160
+
161
+ Installing `pybcsv` provides two streaming command-line converters (require the
162
+ `arrow` extra: `pip install pybcsv[arrow]`). They stream in bounded batches, so
163
+ they handle files larger than memory.
164
+
165
+ ```bash
166
+ # Parquet → BCSV
167
+ parquet2bcsv input.parquet -o output.bcsv
168
+ # --row-codec {delta,zoh,flat} row codec (default: delta)
169
+ # --file-codec {packet_lz4_batch,...} file codec (default: packet_lz4_batch)
170
+ # --chunk-size N rows per streamed batch (default: 512000)
171
+ # -f/--force overwrite an existing output
172
+
173
+ # BCSV → Parquet
174
+ bcsv2parquet input.bcsv -o output.parquet
175
+ # --columns "a,b,c" select/reorder columns (order is honored)
176
+ # --slice 10:100 Python-style row slice
177
+ # --unflatten (default) reconstruct nested structs from dotted/bracketed names
178
+ # --no-unflatten keep flat columns (names like 'a.b', 'vals[0]')
179
+ # --parquet-compression {none,snappy,gzip,zstd,lz4}
180
+ ```
181
+
182
+ **Schema mapping.** Parquet structs and fixed-size lists are *flattened* to BCSV
183
+ columns using dotted (`location.lat`) and bracketed (`vals[0]`) names;
184
+ `bcsv2parquet --unflatten` reverses this. Notes and limitations:
185
+
186
+ - **No nulls:** BCSV has no null representation — a null in any converted column
187
+ is rejected with the offending row number. Filter nulls before converting.
188
+ - **Type widening:** `float16`/`bfloat16` widen to `float32`; `large_string`
189
+ maps to `string`.
190
+ - **Unsupported types** (variable-length lists, maps, timestamps, decimals,
191
+ dictionaries) are rejected with a clear error.
192
+ - **Column names ending in `_`** are rejected (the unflatten escape protocol
193
+ reserves trailing underscores).
194
+ - **Colliding names:** if a literal dotted column (`a.b`) and a struct path both
195
+ map to the same nested path, `--unflatten` fails loudly; use `--no-unflatten`.
196
+
159
197
  ## Available Types
160
198
 
161
199
  | Constant | Description |
@@ -110,6 +110,44 @@ with pybcsv.ReaderDirectAccess() as da:
110
110
  print(da.read(100)) # alternative syntax
111
111
  ```
112
112
 
113
+ ## Parquet Conversion Tools (CLI)
114
+
115
+ Installing `pybcsv` provides two streaming command-line converters (require the
116
+ `arrow` extra: `pip install pybcsv[arrow]`). They stream in bounded batches, so
117
+ they handle files larger than memory.
118
+
119
+ ```bash
120
+ # Parquet → BCSV
121
+ parquet2bcsv input.parquet -o output.bcsv
122
+ # --row-codec {delta,zoh,flat} row codec (default: delta)
123
+ # --file-codec {packet_lz4_batch,...} file codec (default: packet_lz4_batch)
124
+ # --chunk-size N rows per streamed batch (default: 512000)
125
+ # -f/--force overwrite an existing output
126
+
127
+ # BCSV → Parquet
128
+ bcsv2parquet input.bcsv -o output.parquet
129
+ # --columns "a,b,c" select/reorder columns (order is honored)
130
+ # --slice 10:100 Python-style row slice
131
+ # --unflatten (default) reconstruct nested structs from dotted/bracketed names
132
+ # --no-unflatten keep flat columns (names like 'a.b', 'vals[0]')
133
+ # --parquet-compression {none,snappy,gzip,zstd,lz4}
134
+ ```
135
+
136
+ **Schema mapping.** Parquet structs and fixed-size lists are *flattened* to BCSV
137
+ columns using dotted (`location.lat`) and bracketed (`vals[0]`) names;
138
+ `bcsv2parquet --unflatten` reverses this. Notes and limitations:
139
+
140
+ - **No nulls:** BCSV has no null representation — a null in any converted column
141
+ is rejected with the offending row number. Filter nulls before converting.
142
+ - **Type widening:** `float16`/`bfloat16` widen to `float32`; `large_string`
143
+ maps to `string`.
144
+ - **Unsupported types** (variable-length lists, maps, timestamps, decimals,
145
+ dictionaries) are rejected with a clear error.
146
+ - **Column names ending in `_`** are rejected (the unflatten escape protocol
147
+ reserves trailing underscores).
148
+ - **Colliding names:** if a literal dotted column (`a.b`) and a struct path both
149
+ map to the same nested path, `--unflatten` fails loudly; use `--no-unflatten`.
150
+
113
151
  ## Available Types
114
152
 
115
153
  | Constant | Description |
@@ -0,0 +1 @@
1
+ 1.5.9
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.5.8'
22
- __version_tuple__ = version_tuple = (1, 5, 8)
21
+ __version__ = version = '1.5.10.dev0'
22
+ __version_tuple__ = version_tuple = (1, 5, 10, 'dev0')
23
23
 
24
- __commit_id__ = commit_id = 'g255e3b738'
24
+ __commit_id__ = commit_id = 'g6ec074145'
@@ -11,6 +11,7 @@ import time
11
11
  from typing import List, Optional, Set, Tuple, Union
12
12
 
13
13
  import pyarrow as pa
14
+ import pyarrow.compute as pc
14
15
  import pyarrow.parquet as pq
15
16
 
16
17
  import pybcsv
@@ -252,8 +253,24 @@ def flatten_batch(
252
253
 
253
254
 
254
255
  def _extract_flat_array(batch: pa.RecordBatch, flat_name: str) -> pa.Array:
255
- """Navigate into nested columns to extract a flat array."""
256
- parts = _decompose_name(flat_name)
256
+ """Navigate into nested columns to extract a flat array.
257
+
258
+ For already-flattened Parquet files whose column names contain literal
259
+ dots, tries an exact match first. If found, returns the column
260
+ directly without decomposition.
261
+ """
262
+ # Fast path: flat_name matches a literal column name (common for
263
+ # pre-flattened Parquet files with dot-separated identifiers).
264
+ for i in range(len(batch.schema)):
265
+ if batch.schema.field(i).name == flat_name:
266
+ return batch.column(i)
267
+
268
+ # Undo collision-escape underscores before navigating: a struct path whose
269
+ # flattened name collided with a literal dotted column was escaped (e.g.
270
+ # 'a.b' -> 'a_.b') during flattening. The batch's actual struct is named
271
+ # 'a', so strip the escape suffixes to navigate it. Real identifiers can
272
+ # never end with '_' (rejected at flatten time), so this is unambiguous.
273
+ parts = _strip_escape_suffixes(_decompose_name(flat_name))
257
274
  root_name: str = parts[0] # type: ignore[assignment]
258
275
 
259
276
  col_idx = -1
@@ -275,9 +292,17 @@ def _extract_flat_array(batch: pa.RecordBatch, flat_name: str) -> pa.Array:
275
292
  f"List index {part} out of range [0, {list_size}) "
276
293
  f"for column '{flat_name}'."
277
294
  )
295
+ # FixedSizeList values are row-major: element `i` of row `r` lives
296
+ # at child[r*list_size + i]. Extracting element `part` across all
297
+ # rows is therefore a *strided* gather, not a contiguous slice.
298
+ # flatten() yields the logical, offset-adjusted row-major child.
278
299
  num_rows = len(arr)
279
- child_arr = arr.values
280
- arr = child_arr.slice(part * num_rows, num_rows)
300
+ flat_values = arr.flatten()
301
+ gather = pa.array(
302
+ [r * list_size + part for r in range(num_rows)],
303
+ type=pa.int64(),
304
+ )
305
+ arr = flat_values.take(gather)
281
306
  else:
282
307
  arr = arr[part]
283
308
  else:
@@ -451,6 +476,21 @@ def unflatten_schema_to_arrow(
451
476
  )
452
477
  for n in names:
453
478
  _check_underscore_name(n)
479
+ # Two distinct flat names that collapse to the same nested path (e.g. a
480
+ # literal 'a.b' column and a struct path escaped to 'a_.b') cannot be
481
+ # reconstructed unambiguously — the trie would silently merge them and drop
482
+ # data. Detect this and fail loudly; --no-unflatten preserves the columns.
483
+ _seen_paths: dict = {}
484
+ for n in names:
485
+ key = tuple(_strip_escape_suffixes(_decompose_name(n)))
486
+ if key in _seen_paths:
487
+ path_str = ".".join(str(p) for p in key)
488
+ raise ValueError(
489
+ f"Cannot unflatten: columns '{_seen_paths[key]}' and '{n}' both "
490
+ f"map to nested path '{path_str}'. Re-run with --no-unflatten to "
491
+ "keep the flat columns."
492
+ )
493
+ _seen_paths[key] = n
454
494
  trie = _build_trie(names, arrow_types)
455
495
  fields = _trie_to_arrow_field(trie)
456
496
  return pa.schema(fields)
@@ -524,8 +564,20 @@ def _build_nested_array(
524
564
  chunks.append(arr)
525
565
  else:
526
566
  raise ValueError(f"Cannot find array for '{bracket}'")
567
+ # `chunks[i]` holds element `i` of every row (element-major). The Arrow
568
+ # FixedSizeList child must be row-major (child[r*list_size + i]), so
569
+ # re-interleave: child[r*list_size + i] = chunks[i][r] = merged[i*num_rows + r].
527
570
  merged = pa.concat_arrays(chunks)
528
- return pa.FixedSizeListArray.from_arrays(merged, list_size)
571
+ num_rows = len(chunks[0])
572
+ gather = pa.array(
573
+ [
574
+ (p % list_size) * num_rows + (p // list_size)
575
+ for p in range(num_rows * list_size)
576
+ ],
577
+ type=pa.int64(),
578
+ )
579
+ child = merged.take(gather)
580
+ return pa.FixedSizeListArray.from_arrays(child, list_size)
529
581
 
530
582
  struct_fields = field.type
531
583
  child_arrays: List[pa.Array] = []
@@ -616,41 +668,28 @@ def validate_parquet_schema(pf: pq.ParquetFile) -> None:
616
668
 
617
669
 
618
670
  def _check_nulls(batch: pa.RecordBatch, row_offset: int) -> None:
619
- """Raise ValueError on first null using bitmap scan clamped to batch rows."""
671
+ """Raise ValueError on the first null in any column of the batch.
672
+
673
+ Uses pyarrow-native null location, which is offset-safe for sliced arrays
674
+ (a hand-rolled bitmap scan mis-attributes the row number when the array has
675
+ a non-zero offset, e.g. an element extracted from a FixedSizeList).
676
+ """
620
677
  for i in range(len(batch.schema)):
621
678
  col = batch.column(i)
622
679
  if col.null_count == 0:
623
680
  continue
624
681
 
625
682
  field_name = batch.schema.field(i).name
626
- offset = col.offset
627
- validity_buffer = col.buffers()[0]
628
-
629
- if validity_buffer is not None:
630
- bitmap = validity_buffer.to_pybytes()
631
- max_bits = offset + len(col)
632
- max_bytes = (max_bits + 7) // 8
633
-
634
- for byte_idx in range(max_bytes):
635
- byte_val = bitmap[byte_idx]
636
- if byte_val != 0xFF:
637
- for bit_pos in range(8):
638
- absolute_bit = byte_idx * 8 + bit_pos
639
- if absolute_bit >= max_bits:
640
- break
641
- logical_row = absolute_bit - offset
642
- if not (byte_val & (1 << bit_pos)):
643
- raise ValueError(
644
- f"Null value detected in column '{field_name}' "
645
- f"at row {row_offset + logical_row}. "
646
- "BCSV does not support nulls. Filter before conversion."
647
- )
648
- else:
649
- raise ValueError(
650
- f"Null value detected in column '{field_name}' "
651
- f"(null_count={col.null_count}). "
652
- "BCSV does not support nulls."
653
- )
683
+ first_null = pc.index(pc.is_null(col), True).as_py()
684
+ location = (
685
+ f" at row {row_offset + first_null}"
686
+ if first_null is not None and first_null >= 0
687
+ else ""
688
+ )
689
+ raise ValueError(
690
+ f"Null value detected in column '{field_name}'{location}. "
691
+ "BCSV does not support nulls. Filter before conversion."
692
+ )
654
693
 
655
694
 
656
695
  # ---- Selection & helpers ----
@@ -859,14 +898,20 @@ def bcsv_to_parquet(
859
898
  subset_layout: Optional[pybcsv.Layout] = None
860
899
  unflatten_schema: Optional[pa.Schema] = None
861
900
  if unflatten:
862
- if col_names and set(col_names) != set(bcsv_layout.get_column_names()):
901
+ if col_names and col_names != list(bcsv_layout.get_column_names()):
902
+ # Build the subset layout in the user-requested column order (col_names),
903
+ # NOT file order — otherwise the streamed output and the empty-file
904
+ # fallback (which uses arrow_schema, built from col_names) disagree, and
905
+ # a requested reordering is silently ignored.
906
+ type_by_name = dict(
907
+ zip(
908
+ bcsv_layout.get_column_names(),
909
+ bcsv_layout.get_column_types(),
910
+ )
911
+ )
863
912
  subset_layout = pybcsv.Layout()
864
- col_set = set(col_names)
865
- for n, t in zip(
866
- bcsv_layout.get_column_names(), bcsv_layout.get_column_types()
867
- ):
868
- if n in col_set:
869
- subset_layout.add_column(n, t)
913
+ for n in col_names:
914
+ subset_layout.add_column(n, type_by_name[n])
870
915
  else:
871
916
  subset_layout = bcsv_layout
872
917
  layout_names = list(subset_layout.get_column_names())
@@ -1135,7 +1180,8 @@ def bcsv2parquet_cli() -> None:
1135
1180
  "--row-group-size",
1136
1181
  type=int,
1137
1182
  default=None,
1138
- help="Row group size in PyArrow write_table (not supported in ParquetWriter streaming mode)",
1183
+ help="Max rows per Parquet row group (applied per streamed batch; "
1184
+ "values larger than --chunk-size are bounded by the batch size)",
1139
1185
  )
1140
1186
  mut_unflatten = parser.add_mutually_exclusive_group()
1141
1187
  mut_unflatten.add_argument(
@@ -278,8 +278,9 @@ class TestFlatSchemaToBcsvLayout(unittest.TestCase):
278
278
  continue # widened to float32 before reaching this function
279
279
  flat = [("x", arrow_type)]
280
280
  layout = _flat_schema_to_bcsv_layout(flat)
281
- self.assertEqual(len(layout.get_column_names()), 1,
282
- f"Missing mapping for {arrow_type}")
281
+ self.assertEqual(
282
+ len(layout.get_column_names()), 1, f"Missing mapping for {arrow_type}"
283
+ )
283
284
 
284
285
  def test_large_string_maps_to_string(self):
285
286
  """pa.large_string() must map to BCSV STRING (not raise)."""
@@ -477,6 +478,41 @@ class TestFlattenBatch(unittest.TestCase):
477
478
  self.assertEqual(result.schema.field(0).name, "b")
478
479
  self.assertEqual(result.schema.field(1).name, "a")
479
480
 
481
+ def test_flatten_already_flat_with_dots(self):
482
+ """Parquet file where columns are already flat with dot-separated names.
483
+
484
+ This is a regression test for column names like 'sim.ve.counter' where
485
+ the Parquet schema has no nesting but the name contains dots. The
486
+ flatten_batch function must recognize the name as a literal column name
487
+ and not decompose it into a non-existent struct path.
488
+ """
489
+ batch = pa.RecordBatch.from_arrays(
490
+ [
491
+ pa.array([1, 2, 3]),
492
+ pa.array([10.0, 20.0, 30.0]),
493
+ pa.array([100, 200, 300]),
494
+ ],
495
+ schema=pa.schema(
496
+ [
497
+ ("sim.ve.counter", pa.int64()),
498
+ ("sim.ve.value", pa.float64()),
499
+ ("time", pa.int64()),
500
+ ]
501
+ ),
502
+ )
503
+ flat_schema = [
504
+ ("sim.ve.counter", pa.int64()),
505
+ ("sim.ve.value", pa.float64()),
506
+ ("time", pa.int64()),
507
+ ]
508
+ result = flatten_batch(batch, flat_schema)
509
+ self.assertEqual(result.num_rows, 3)
510
+ self.assertEqual(result.num_columns, 3)
511
+ # Values should be identical since no transformation needed
512
+ self.assertEqual(result.column("sim.ve.counter").to_pylist(), [1, 2, 3])
513
+ self.assertEqual(result.column("sim.ve.value").to_pylist(), [10.0, 20.0, 30.0])
514
+ self.assertEqual(result.column("time").to_pylist(), [100, 200, 300])
515
+
480
516
 
481
517
  if __name__ == "__main__":
482
518
  unittest.main()
pybcsv-1.5.8/VERSION.txt DELETED
@@ -1 +0,0 @@
1
- 1.5.8
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes