stephen-a-stc opened a new issue, #54885:
URL: https://github.com/apache/arrow/issues/54885

   ### Describe the bug, including details regarding any error messages, 
version, and platform.
   
   ## Summary
   
   `pyarrow.Table.join()` crashes my Python session when 1 or more 
`large_string` columns are used as keys, and the data in those columns nears 
2gb.  
   Specifically, we can use the following inequality to define the bound of 
where this bug occurs
   
   > occurs when: `[num_rows]*[num_keys]*([key_length]+9) > [2 gigabytes]`
   > In words: if the amount of key data plus the additional 9 bytes per key 
exceeds 2gb, a fatal crash occurs.  
   
   I focused my investigation on 'left anti' joins, where the right table is 
near boundary, in which case the left table's size seems irrelevant.  Just 
before submission, I confirmed that this occurs with all join types when tested 
with a large right table.
   
   ### Expected Behaviour
   
   The table join function should support the use of `large_string` columns of 
any size as keys.  
   
   ### Actual Behaviour
   
   The join function causes a fatal crash when `large_string` key columns 
exceed a certain size which is below, but near, 2gb.  
   
   ## Tested With
   
   PyArrow versions tested: 
   - `24.0.1`
   - `25.0.0`
   - `26.0.0`
   
   Platform:
   - Windows
   - Linux
   - Microsoft Fabric (Spark RT 2.0)
   
   ## Minimal Reproducer
   
   ```python
   import faulthandler
   
   import numpy as np
   import pyarrow as pa
   
   faulthandler.enable()
   
   def make_join_test_data(
       left_rows=1_000_000,
       right_rows=50_000_000,
       num_keys=3,
       key_length=15,
   ):
       def build_table(n_rows):
           cols = {}
           for k in range(num_keys):
               cols[f"KEY{k+1}"] = pa.array(
                   [f"{i:0{key_length}d}" for i in range(n_rows)],
                   type=pa.large_string(),
               )
           cols["INTVAL"] = pa.array(np.arange(n_rows, dtype=np.int64))
           cols["DOUBLEVAL"] = pa.array(np.arange(n_rows, dtype=np.float64))
   
           return pa.table(cols)
   
       left = build_table(left_rows)
       right = build_table(right_rows)
   
       return left, right
   
   left, right = make_join_test_data(
       right_rows=534_865 + 1,  # remove the `+ 1` and it will work
       left_rows=10_000,
       num_keys=5,
       key_length=794,
   )
   join_keys = [col for col in right.column_names if col.startswith("KEY")]
   print(f"joining with {join_keys}")
   left.join(right, keys=join_keys, join_type="left anti")  # BUG: note that it 
fails with all join types
   print("it worked")
   ```
   
   ### Output (pyarrow 25.0.0 cp314-cp314-linux_x86_64 )
   
   ```plaintext
   joining with ['KEY1', 'KEY2', 'KEY3', 'KEY4', 'KEY5']
   terminate called after throwing an instance of 'std::length_error'
     what():  vector::_M_default_append
   Fatal Python error: Aborted
   
   Thread 0x00007fdc27af7740 [python] (most recent call first):
     File "/opt/conda/lib/python3.14/site-packages/pyarrow/acero.py", line 253 
in _perform_join
     File "/home/jovyan/try_repro.py", line 39 in <module>
   
   Current thread's C stack trace (most recent call first):
     Binary file "python", at _Py_DumpStack+0x4a [0x56532cc3e4ba]
     Binary file "python", at +0x18f928 [0x56532cc3e928]
     Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x45330 [0x7fdc27b41330]
     Binary file "/lib/x86_64-linux-gnu/libc.so.6", at pthread_kill+0x11c 
[0x7fdc27b9ac0c]
     Binary file "/lib/x86_64-linux-gnu/libc.so.6", at gsignal+0x1e 
[0x7fdc27b4127e]
     Binary file "/lib/x86_64-linux-gnu/libc.so.6", at abort+0xdf 
[0x7fdc27b248ff]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6",
 at +0xb2cd2 [0x7fdc25b17cd2]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6",
 at +0xc5a1a [0x7fdc25b2aa1a]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6",
 at _ZSt10unexpectedv+0x0 [0x7fdc25b1784d]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6",
 at +0xc5cb7 [0x7fdc25b2acb7]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6",
 at _ZSt20__throw_length_errorPKc+0x38 [0x7fdc25b1abe1]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_compute.so.2500",
 at +0x11b30d [0x7fdc1800430d]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_compute.so.2500",
 at 
_ZN5arrow7compute8internal10RowEncoder15EncodeAndAppendERKNS0_8ExecSpanE+0x13a 
[0x7fdc188f627a]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500",
 at 
_ZNK5arrow5acero22HashJoinDictBuildMulti11EncodeBatchEmRKNS0_20SchemaProjectionMapsINS0_18HashJoinProjectionEEERKNS_7compute9ExecBatchEPNS7_8internal10RowEncoderEPNS7_11ExecContextE+0x1a6
 [0x7fdc00a10126]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500",
 at +0x9e9cf [0x7fdc00a0a9cf]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500",
 at _ZN5arrow5acero17TaskSchedulerImpl11ExecuteTaskEmilPb+0x69 [0x7fdc00a5abc9]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500",
 at +0xfa151 [0x7fdc00a66151]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500",
 at +0xc6d15 [0x7fdc00a32d15]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow_acero.so.2500",
 at +0xbf557 [0x7fdc00a2b557]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow.so.2500", at 
+0xa57be2 [0x7fdc195fbbe2]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/pyarrow/../../../libarrow.so.2500", at 
+0xa5499c [0x7fdc195f899c]
     Binary file 
"/opt/conda/lib/python3.14/site-packages/numpy/_core/../../../../libstdc++.so.6",
 at +0xf5b86 [0x7fdc25b5ab86]
     Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x9cb84 [0x7fdc27b98b84]
     Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x129ecc 
[0x7fdc27c25ecc]
   
   Extension modules: numpy._core._multiarray_umath, 
numpy.linalg._umath_linalg, pyarrow.lib, numpy.random._common, 
numpy.random.bit_generator, numpy.random._bounded_integers, 
numpy.random._pcg64, numpy.random._generator, numpy.random._mt19937, 
numpy.random._philox, numpy.random._sfc64, numpy.random.mtrand, _cyutility, 
pandas._libs._cyutility, pandas._libs.tslibs.ccalendar, 
pandas._libs.tslibs.np_datetime, pandas._libs.tslibs.dtypes, 
pandas._libs.tslibs.base, pandas._libs.tslibs.nattype, 
pandas._libs.tslibs.timezones, pandas._libs.properties, 
pandas._libs.tslibs.fields, pandas._libs.tslibs.timedeltas, 
pandas._libs.tslibs.tzconversion, pandas._libs.tslibs.timestamps, 
pandas._libs.tslibs.offsets, pandas._libs.tslibs.strptime, 
pandas._libs.tslibs.parsing, pandas._libs.tslibs.conversion, 
pandas._libs.tslibs.period, pandas._libs.tslibs.vectorized, 
pandas._libs.ops_dispatch, pandas._libs.missing, pandas._libs.hashtable, 
pandas._libs.algos, pandas._libs.interval, pandas._libs.lib, pyarrow._
 compute, pandas._libs.ops, numexpr.interpreter, bottleneck.move, 
bottleneck.nonreduce, bottleneck.nonreduce_axis, bottleneck.reduce, 
pandas._libs.hashing, pandas._libs.arrays, pandas._libs.tslib, 
pandas._libs.sparse, pandas._libs.internals, pandas._libs.indexing, 
pandas._libs.index, pandas._libs.writers, pandas._libs.join, 
pandas._libs.window.aggregations, pandas._libs.window.indexers, 
pandas._libs.reshape, pandas._libs.groupby, pandas._libs.json, 
pandas._libs.parsers, pandas._libs.testing, pyarrow._acero, pyarrow._fs, 
pyarrow._csv, pyarrow._json, pyarrow._substrait, pyarrow._dataset, 
pyarrow._dataset_orc, pyarrow._parquet, pyarrow._azurefs, pyarrow._hdfs, 
pyarrow._gcsfs, pyarrow._s3fs, pyarrow._parquet_encryption, 
pyarrow._dataset_parquet_encryption, pyarrow._dataset_parquet (total: 75)
   Aborted
   ```
   
   ## Related Issues
   
   - https://github.com/apache/arrow/issues/33151
   - https://github.com/apache/arrow/issues/36879
   
   ## Investigation
   
   I suspect there is an `int32_t` where there may need to be something larger. 
 The stack trace leads to the C++ method 
`arrow.compute.internal.RowEncoder.EncodeAndAppend()` (found in 
[row_encoder_internal.cc](https://github.com/apache/arrow/blob/1776578067602ed059db9bd8c3e4fc126c4597f3/cpp/src/arrow/compute/row/row_encoder_internal.cc#L358)).
  This method makes use of a private member, `offsets_`, which is defined in 
[row_encoder_internal.h](https://github.com/apache/arrow/blob/1776578067602ed059db9bd8c3e4fc126c4597f3/cpp/src/arrow/compute/row/row_encoder_internal.h#L397)
 and contains the following note
   
   ```cpp
   // offsets_ vector stores the starting position (offset) of each encoded row
     // within the bytes_ vector. This allows for quick access to individual 
rows.
     //
     // The size would be num_rows + 1 if not empty, the last element is the 
total
     // length of the bytes_ vector.
     std::vector<int32_t> offsets_;
   ```
   
   This has me wondering, if the bytes vector exceeds a certain size, will the 
offset not be able to address it?
   
   ### Additional Reproductions
   
   I ran a number of different tests with varying numbers of keys, key length, 
rows of data, and different left table sizes.  The following code documents 
that and makes it easy to continue testing.  
   
   ```python
   """pyarrow.Table.join fails under certain conditions.
   
   pyarrow.Table.join fails when
       - type is 'left anti'
       - keys (1 or more) are `large_string`
       - the volume of string key data + metadata exceeds a threshold of 2gb
   
   The failure is not raised as an arrow exception but leads to 'Fatal Python 
error: Aborted'.
   The C Stack Trace leads to C++ method 
`arrow.compute.internal.RowEncoder.EncodeAndAppend()`.
   
   """
   import faulthandler
   from pprint import pprint
   
   import numpy as np
   import pyarrow as pa
   
   faulthandler.enable()
   
   def print_table_summary(name, t, keys=None):
       msg = f"\n{name}\n"
       msg += f"rows      : {t.num_rows:,}\n"
       msg += f"columns   : {t.num_columns}\n"
       msg += f"nbytes    : {t.nbytes} ({t.nbytes / 1024**3:.2f} GiB)\n"
       key_bytes=sum([col.nbytes for col in t.select(keys).columns])
       msg += f"key nbytes: {key_bytes} ({key_bytes / 1024**3:.2f} GiB)\n"
       msg += f"chunks    : {sum(len(c.chunks) for c in t.columns):,}\n"
   
       for col_name in t.column_names:
           col = t[col_name]
           msg += f"  {col_name:10}"
           msg += f"{str(col.type):14}"
           msg += f"chunks={len(col.chunks):<6,}"
           msg += f" bytes={col.nbytes:-16}\n"
       print(msg)
   
   def make_join_test_data(
       left_rows=1_000_000,
       right_rows=50_000_000,
       num_keys=3,
       key_length=15,
       final_key_length_adjustment=0,
   ):
       """Build tables as specified, each also includes 1 int and 1 double 
column."""
       print("building test data with specs:")
       pprint(locals())
       print()
       # determine expected outcome
       _2gb = 2*1024*1024*1024
       ptr_bytes=8
       null_bytes=1
       est_bytes = right_rows*num_keys*(key_length + ptr_bytes + null_bytes)
       if final_key_length_adjustment != 0:
           est_bytes += final_key_length_adjustment*num_keys
       est_buffer_free = _2gb - est_bytes
       if est_buffer_free <0:
           print("Expected outcome: FAILURE")
           print ("  Estimated join bytes exceed 2gb by ", -1*est_buffer_free, 
" bytes")
       else:
           print("Expected outcome: SUCCESS")
           print("  Join should pass with ", est_buffer_free, " bytes of extra 
space")
   
       def build_table(n_rows):
           cols = {}
           temp_col=[f"{i:0{key_length}d}" for i in range(n_rows)]
           temp_col[-1] = 
f"{(n_rows-1):0{key_length+final_key_length_adjustment}d}"
           for k in range(num_keys):
               cols[f"KEY{k+1}"] = pa.array(
                   temp_col,
                   type=pa.large_string(),
               )
           cols["INTVAL"] = pa.array(np.arange(n_rows, dtype=np.int64))
           cols["DOUBLEVAL"] = pa.array(np.arange(n_rows, dtype=np.float64))
   
           return pa.table(cols)
   
       left = build_table(left_rows)
       right = build_table(right_rows)
   
       return left, right
   
   # list of different shape tables at the point of failure
   runs = [
       {#0
           "triggers_bug": False,
           "args": {
               "right_rows": 37_025_580,
               "left_rows": 10_000,
               "num_keys": 2,
               "key_length": 20,
           },
       },
       {#1
           "triggers_bug": True,
           "args": {
               "right_rows": 37_025_580 + 1,
               "left_rows": 10_000,
               "num_keys": 2,
               "key_length": 20,
           },
       },
       {#2
           "triggers_bug": False,
           "args": {
               "right_rows": 534_865,
               "left_rows": 10_000,
               "num_keys": 5,
               "key_length": 794,
           },
       },
       {#3
           "triggers_bug": True,
           "args": {
               "right_rows": 534_865 + 1,
               "left_rows": 10_000,
               "num_keys": 5,
               "key_length": 794,
           },
       },
       {#4
           "triggers_bug": False,
           "args": {
               "right_rows": 1_337_162,
               "left_rows": 10_000,
               "num_keys": 2,
               "key_length": 794,
           },
       },
       {#5
           "triggers_bug": True,
           "args": {
               "right_rows": 1_337_162 + 1,
               "left_rows": 10_000,
               "num_keys": 2,
               "key_length": 794,
           },
       },
       {#6
           "triggers_bug": False,
           "args": {
               "right_rows": 2_674_325,
               "left_rows": 10_000,
               "num_keys": 1,
               "key_length": 794,
           },
       },
       {#7
           "triggers_bug": True,
           "args": {
               "right_rows": 2_674_325 + 1,
               "left_rows": 10_000,
               "num_keys": 1,
               "key_length": 794,
           },
       },
       {#8
           "triggers_bug": False,
           "args": {
               "right_rows": 37_025_580,
               "left_rows": 10_000,
               "num_keys": 2,
               "key_length": 20,
               "final_key_length_adjustment": 3,
           },
       },
       {#9
           "triggers_bug": True,
           "args": {
               "right_rows": 37_025_580,
               "left_rows": 10_000,
               "num_keys": 2,
               "key_length": 20,
               "final_key_length_adjustment": 4,
           },
       },
       {#10
           "triggers_bug": False,
           "args": {
               "right_rows": 534_465,
               "left_rows": 10_000,
               "num_keys": 2,
               "key_length": 2000,
           },
       },
       {#11
           "triggers_bug": True,
           "args": {
               "right_rows": 534_465 + 1,
               "left_rows": 10_000,
               "num_keys": 2,
               "key_length": 2000,
           },
       },
   ]
   
   #left, right = make_join_test_data(
   #    left_rows=10_000,
   #    right_rows=37_025_580+1,
   #    num_keys=2,
   #    key_length=20,
   #    #final_key_length_adjustment=3
   #)
   
   left, right = make_join_test_data(
       **runs[4]['args']
   )
   join_keys = [col for col in right.column_names if col.startswith("KEY")]
   
   print_table_summary("left", left, join_keys)
   print_table_summary("right", right, join_keys)
   print(f"joining with {join_keys}")
   
   # .join doesn't like too much `large_string` data
   left.join(
       right,
       keys=join_keys,
       join_type="left anti",
   )
   print("it worked")
   ```
   
   #### Sample Output (`runs[3]`)
   
   ```plaintext
   building test data with specs:
   {'final_key_length_adjustment': 0,
    'key_length': 794,
    'left_rows': 10000,
    'num_keys': 5,
    'right_rows': 534866}
   
   Expected outcome: FAILURE
     Estimated join bytes exceed 2gb by  3342  bytes
   
   left
   rows      : 10,000
   columns   : 7
   nbytes    : 40260000 (0.04 GiB)
   key nbytes: 40100000 (0.04 GiB)
   chunks    : 7
     KEY1      large_string  chunks=1      bytes=         8020000
     KEY2      large_string  chunks=1      bytes=         8020000
     KEY3      large_string  chunks=1      bytes=         8020000
     KEY4      large_string  chunks=1      bytes=         8020000
     KEY5      large_string  chunks=1      bytes=         8020000
     INTVAL    int64         chunks=1      bytes=           80000
     DOUBLEVAL double        chunks=1      bytes=           80000
   
   
   right
   rows      : 534,866
   columns   : 7
   nbytes    : 2153370516 (2.01 GiB)
   key nbytes: 2144812660 (2.00 GiB)
   chunks    : 7
     KEY1      large_string  chunks=1      bytes=       428962532
     KEY2      large_string  chunks=1      bytes=       428962532
     KEY3      large_string  chunks=1      bytes=       428962532
     KEY4      large_string  chunks=1      bytes=       428962532
     KEY5      large_string  chunks=1      bytes=       428962532
     INTVAL    int64         chunks=1      bytes=         4278928
     DOUBLEVAL double        chunks=1      bytes=         4278928
   
   joining with ['KEY1', 'KEY2', 'KEY3', 'KEY4', 'KEY5']
   Fatal Python error: Aborted
   
   Thread 0x00000c14 (most recent call first):
   .
   .
   .
   ```
   
   
   
   ### Component(s)
   
   C++, Python


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to