@@ -476,6 +476,72 @@ def test_duplicate_column_names_survive_arrow_fetch(self):
476476 row = cursor .fetchone ()
477477 assert list (row ) == [1 , 2 ]
478478
479+ @pytest .mark .parametrize ("extra_params" , [{}, {"use_sea" : True }])
480+ def test_double_nan_distinct_from_null_with_pandas (self , extra_params ):
481+ """GH #978: with pandas enabled, IEEE NaN in a DOUBLE column must come
482+ back as float('nan'), not None (which is reserved for SQL NULL)."""
483+ import math
484+
485+ query = (
486+ "SELECT * FROM VALUES (1, CAST('NaN' AS DOUBLE)), (2, CAST(NULL AS DOUBLE)), "
487+ "(3, CAST('Infinity' AS DOUBLE)), (4, CAST(1.5 AS DOUBLE)) AS t(id, d) ORDER BY id"
488+ )
489+ with self .cursor (extra_params ) as cursor :
490+ cursor .execute (query )
491+ rows = cursor .fetchall ()
492+
493+ assert len (rows ) == 4
494+ assert isinstance (rows [0 ].d , float ) and math .isnan (rows [0 ].d )
495+ assert rows [1 ].d is None
496+ assert rows [2 ].d == float ("inf" )
497+ assert rows [3 ].d == 1.5
498+
499+ @pytest .mark .parametrize ("extra_params" , [{}, {"use_sea" : True }])
500+ def test_float_nan_distinct_from_null_matches_disable_pandas (self , extra_params ):
501+ """GH #978: FLOAT (float32) NaN must be returned as NaN with pandas
502+ enabled, matching the _disable_pandas=True output."""
503+ import math
504+
505+ query = (
506+ "SELECT * FROM VALUES (1, CAST('NaN' AS FLOAT)), (2, CAST(NULL AS FLOAT)), "
507+ "(3, CAST('Infinity' AS FLOAT)), (4, CAST(1.5 AS FLOAT)) AS t(id, f) ORDER BY id"
508+ )
509+ with self .cursor (extra_params ) as cursor :
510+ cursor .execute (query )
511+ pandas_rows = cursor .fetchall ()
512+ with self .cursor (dict (extra_params , _disable_pandas = True )) as cursor :
513+ cursor .execute (query )
514+ plain_rows = cursor .fetchall ()
515+
516+ assert isinstance (pandas_rows [0 ].f , float ) and math .isnan (pandas_rows [0 ].f )
517+ assert pandas_rows [1 ].f is None
518+
519+ def _norm (v ):
520+ return "NaN" if isinstance (v , float ) and math .isnan (v ) else v
521+
522+ assert [tuple (_norm (v ) for v in r ) for r in pandas_rows ] == [
523+ tuple (_norm (v ) for v in r ) for r in plain_rows
524+ ]
525+
526+ @pytest .mark .parametrize ("extra_params" , [{}, {"use_sea" : True }])
527+ def test_nan_preserved_in_fetchone_and_fetchmany (self , extra_params ):
528+ """GH #978: fetchone/fetchmany share _convert_arrow_table and must also
529+ keep NaN distinct from NULL."""
530+ import math
531+
532+ query = "SELECT CAST('NaN' AS DOUBLE) AS d, CAST(NULL AS DOUBLE) AS n"
533+ with self .cursor (extra_params ) as cursor :
534+ cursor .execute (query )
535+ row = cursor .fetchone ()
536+ assert isinstance (row .d , float ) and math .isnan (row .d )
537+ assert row .n is None
538+
539+ cursor .execute (query )
540+ rows = cursor .fetchmany (1 )
541+ assert len (rows ) == 1
542+ assert isinstance (rows [0 ].d , float ) and math .isnan (rows [0 ].d )
543+ assert rows [0 ].n is None
544+
479545 def test_dml_rowcount (self ):
480546 """cursor.rowcount reports the affected-row count for DML
481547 (INSERT/UPDATE/DELETE) instead of the hardcoded -1, and resets
0 commit comments