44
55from typing import Any , Dict , List , Mapping , Optional , Sequence , Tuple , Union , cast
66
7- from graphistry .Engine import EngineAbstract , df_concat , df_cons , resolve_engine , safe_merge
7+ from graphistry .Engine import (
8+ EngineAbstract ,
9+ assign_constant_columns ,
10+ df_concat ,
11+ df_cons ,
12+ drop_columns ,
13+ frame_filter ,
14+ is_series_like ,
15+ ordered_left_join ,
16+ resolve_engine ,
17+ row_as_mapping ,
18+ safe_merge ,
19+ series_filter ,
20+ series_not_null_mask ,
21+ series_to_pylist ,
22+ )
823from graphistry .Plottable import Plottable
924from graphistry .compute .exceptions import GFQLValidationError , ErrorCode
1025from graphistry .compute .gfql .cypher .reentry .naming import _reentry_hidden_column_name
2338from graphistry .Engine import is_polars_df as _is_polars_df
2439
2540
26- def _series_is_series_like (s : object ) -> bool :
27- """True for a pandas/cuDF Series (``.dropna``) or a polars Series (module check).
28-
29- The reentry executor recovers carried node identities from a projection-meta ``ids`` Series
30- that is pandas under ``engine='pandas'`` and polars under ``engine='polars'``; both are valid."""
31- return hasattr (s , "dropna" ) or _is_polars_df (s )
32-
33-
34- # The four helpers below are engine-agnostic (pandas/cuDF/polars); `SeriesT`/`DataFrameT` are the
35- # repo's frame aliases (pandas types for mypy). The polars branches genuinely return polars objects
36- # mypy can't narrow to the alias, so the suppression lives here at the dispatch point — callers get
37- # a clean `SeriesT`/`DataFrameT` contract with no cast().
38- def _series_not_null_mask (s : SeriesT ) -> SeriesT :
39- """Non-null boolean mask, engine-aware (polars ``is_not_null`` vs pandas ``notna``)."""
40- if _is_polars_df (s ):
41- return s .is_not_null () # type: ignore[attr-defined,no-any-return]
42- return s .notna ()
43-
44-
45- def _series_filter (s : SeriesT , mask : SeriesT ) -> SeriesT :
46- """Filter a Series by a boolean mask, engine-aware, dropping the old index (pandas)."""
47- if _is_polars_df (s ):
48- return s .filter (mask ) # type: ignore[attr-defined,no-any-return]
49- return s [mask ].reset_index (drop = True )
50-
51-
52- def _frame_filter (df : DataFrameT , mask : SeriesT ) -> DataFrameT :
53- """Filter a DataFrame's rows by a boolean mask, engine-aware, dropping the old index."""
54- if _is_polars_df (df ):
55- return df .filter (mask ) # type: ignore[attr-defined,no-any-return]
56- return df .loc [mask ].reset_index (drop = True )
57-
58-
59- def _ordered_left_join (left : DataFrameT , right : DataFrameT , * , on : str ) -> DataFrameT :
60- """Left join preserving ``left`` row order, engine-aware. Polars ``.merge`` does not exist;
61- ``safe_merge`` (pandas/cuDF ``.merge``) cannot run on polars frames, so branch to
62- ``.join(..., maintain_order='left')`` which pins the WITH-row ordering the reentry seed needs.
63-
64- Under ``engine='polars'`` the carried ids come from the natively-projected (polars) prefix
65- while the base node table can still be pandas (the base graph is converted lazily), so align
66- ``right`` onto ``left``'s engine before the polars join."""
67- if _is_polars_df (left ):
68- from graphistry .Engine import Engine , df_to_engine
69- if not _is_polars_df (right ):
70- right = df_to_engine (right , Engine .POLARS )
71- return left .join (right , on = on , how = "left" , maintain_order = "left" ) # type: ignore[call-arg,no-any-return]
72- return safe_merge (left , right , on = on , how = "left" )
73-
74-
75- def _reentry_row (prefix_rows : DataFrameT , row_index : int ) -> Mapping [str , Any ]:
76- """One prefix row as a col->scalar mapping, engine-aware (``row[col]`` works for
77- both the pandas Series and the polars named-row dict)."""
78- if _is_polars_df (prefix_rows ):
79- return prefix_rows .row (row_index , named = True ) # type: ignore[attr-defined,no-any-return]
80- return prefix_rows .iloc [row_index ]
81-
82-
83- def _assign_constant_columns (df : DataFrameT , values : Dict [str , Any ]) -> DataFrameT :
84- """Broadcast scalar ``values`` as constant columns, engine-aware."""
85- if not values :
86- return df
87- if _is_polars_df (df ):
88- import polars as pl
89- return df .with_columns ([pl .lit (v ).alias (k ) for k , v in values .items ()]) # type: ignore[attr-defined,no-any-return]
90- return df .assign (** values )
91-
92-
93- def _drop_columns (df : Any , cols : Sequence [str ]) -> Any :
94- if _is_polars_df (df ):
95- return df .drop (list (cols ))
96- return df .drop (columns = list (cols ))
97-
98-
9941def _bind_reentry_graph (graph : Plottable , node_rows : Optional [DataFrameT ], * , empty_edges : bool = False ) -> Plottable :
10042 out = graph .bind ()
10143 out ._nodes = node_rows
@@ -232,22 +174,14 @@ def _optional_reentry_key(record: Dict[str, Any], columns: Tuple[str, ...]) -> T
232174
233175
234176def _records_for_columns (df : DataFrameT , columns : Tuple [str , ...]) -> List [Dict [str , Any ]]:
235- values_by_column = {column : _series_to_pylist (cast (SeriesT , df [column ])) for column in columns }
177+ values_by_column = {column : series_to_pylist (cast (SeriesT , df [column ])) for column in columns }
236178 row_count = len (df )
237179 return [
238180 {column : values_by_column [column ][row_index ] for column in columns }
239181 for row_index in range (row_count )
240182 ]
241183
242184
243- def _series_to_pylist (values : SeriesT ) -> List [Any ]:
244- if hasattr (values , "to_arrow" ):
245- return cast (List [Any ], values .to_arrow ().to_pylist ())
246- if hasattr (values , "tolist" ):
247- return cast (List [Any ], values .tolist ())
248- return list (values )
249-
250-
251185def _optional_reentry_key_value (value : Any ) -> Any :
252186 try :
253187 if value != value :
@@ -323,7 +257,7 @@ def compiled_query_reentry_state(
323257 )
324258 ids = meta ["ids" ]
325259 id_column = meta ["id_column" ]
326- if not _series_is_series_like (ids ):
260+ if not is_series_like (ids ):
327261 raise reentry_validation_error (
328262 "Cypher MATCH after WITH could not recover carried node identities from the prefix stage" ,
329263 value = output_name ,
@@ -457,10 +391,10 @@ def compiled_query_scalar_reentry_state(
457391 value = missing_column ,
458392 suggestion = "Project the scalar column explicitly before MATCH re-entry." ,
459393 )
460- row = _reentry_row (prefix_rows , row_index )
394+ row = row_as_mapping (prefix_rows , row_index )
461395 node_rows = cast (
462396 DataFrameT ,
463- _assign_constant_columns (
397+ assign_constant_columns (
464398 base_nodes ,
465399 {
466400 _reentry_hidden_column_name (output_name ): row [output_name ]
@@ -480,7 +414,7 @@ def freeform_broadcast_row_to_nodes(
480414 row_index : int ,
481415) -> Plottable :
482416 """Broadcast one free-form prefix row's hidden carries onto the base nodes."""
483- row = _reentry_row (prefix_rows , row_index )
417+ row = row_as_mapping (prefix_rows , row_index )
484418 broadcast_values : Dict [str , Any ] = {
485419 _reentry_hidden_column_name (col ): row [col ]
486420 for col in plan .scalar_columns
@@ -495,11 +429,11 @@ def freeform_broadcast_row_to_nodes(
495429 if broadcast_values :
496430 existing_hidden = [c for c in base_nodes .columns if isinstance (c , str ) and c .startswith ("__cypher_reentry_" )]
497431 node_rows = (
498- cast (DataFrameT , _drop_columns (base_nodes , existing_hidden ))
432+ cast (DataFrameT , drop_columns (base_nodes , existing_hidden ))
499433 if existing_hidden
500434 else base_nodes
501435 )
502- node_rows = cast (DataFrameT , _assign_constant_columns (node_rows , broadcast_values ))
436+ node_rows = cast (DataFrameT , assign_constant_columns (node_rows , broadcast_values ))
503437 else :
504438 node_rows = cast (DataFrameT , base_nodes )
505439
@@ -548,18 +482,18 @@ def aligned_reentry_rows(
548482 value = output_name ,
549483 suggestion = "Retry with a direct whole-row carry through WITH or inspect intermediate row-shaping before MATCH re-entry." ,
550484 )
551- if not _series_is_series_like (ids ):
485+ if not is_series_like (ids ):
552486 raise reentry_validation_error (
553487 "Cypher MATCH after WITH could not align carried node identities from the prefix stage" ,
554488 value = output_name ,
555489 suggestion = REENTRY_WHOLE_ROW_SUGGESTION ,
556490 )
557491
558- non_null_mask = _series_not_null_mask (ids )
559- carried_ids = _series_filter (ids , non_null_mask )
492+ non_null_mask = series_not_null_mask (ids )
493+ carried_ids = series_filter (ids , non_null_mask )
560494 if prefix_rows is None :
561495 return carried_ids , None
562- return carried_ids , _frame_filter (prefix_rows , non_null_mask )
496+ return carried_ids , frame_filter (prefix_rows , non_null_mask )
563497
564498
565499def reentry_carry_payload (
@@ -593,4 +527,4 @@ def ordered_reentry_start_nodes(
593527 id_column : str ,
594528) -> DataFrameT :
595529 # MATCH re-entry must preserve the WITH row order, not the base node-table order.
596- return _ordered_left_join (carried_node_ids , node_rows , on = id_column )
530+ return ordered_left_join (carried_node_ids , node_rows , on = id_column )
0 commit comments