diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e7165fbff..973c0a4dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Development] +### Performance +- **GFQL temporal-detection dtype gate (#1650)**: `order_detect_temporal_mode` now short-circuits for numeric/bool/complex columns, which can never hold temporal *text*, instead of running an `astype(str)` + multi-regex `fullmatch` scan on every comparison. Eliminates spurious row-wise stringification in `where_rows`/comparison paths whose output never contains entity-text. Byte-identical results; measured `where_rows` speedups ~3.1× (pandas) and ~4.4–13.3× (cuDF, scaling with row count). Does not address whole-entity `RETURN a` text rendering, which is tracked separately. + ## [0.56.1 - 2026-05-27] ### Added diff --git a/graphistry/compute/gfql/row/ordering.py b/graphistry/compute/gfql/row/ordering.py index a2aafe4985..4b31c33cd4 100644 --- a/graphistry/compute/gfql/row/ordering.py +++ b/graphistry/compute/gfql/row/ordering.py @@ -214,6 +214,13 @@ def parse_stringified_list_series(series: Any) -> Optional[SeriesT]: def order_detect_temporal_mode(series: Any) -> Optional[str]: if not hasattr(series, "dropna"): return None + # Temporal values are encoded as *text* (Cypher date/datetime/time literals or + # constructor calls). Numeric/bool/complex columns can never match those regexes, + # so skip the astype(str) + multi-regex fullmatch scan for them. This avoids + # spuriously stringifying numeric columns on every comparison (issue #1650). + dtype_kind = getattr(getattr(series, "dtype", None), "kind", None) + if dtype_kind in ("i", "u", "f", "b", "c"): + return None non_null = series.dropna() if len(non_null) == 0 or not hasattr(non_null, "astype"): return None diff --git a/graphistry/tests/compute/gfql/row/test_ordering.py b/graphistry/tests/compute/gfql/row/test_ordering.py index c8c22ef75a..41aa782102 100644 --- a/graphistry/tests/compute/gfql/row/test_ordering.py +++ b/graphistry/tests/compute/gfql/row/test_ordering.py @@ -5,6 +5,7 @@ from graphistry.compute.ast import limit, order_by, rows, select from graphistry.compute.exceptions import GFQLTypeError +from graphistry.compute.gfql.row.ordering import order_detect_temporal_mode from graphistry.tests.test_compute import CGFull @@ -193,3 +194,36 @@ def test_row_pipeline_order_by_multi_key_stringified_list_with_scalar() -> None: assert result["num"].tolist() == [20, 10, 15, 5] leaked = [c for c in result.columns if "__gfql_sort_listparsed" in str(c)] assert leaked == [], f"aux columns leaked: {leaked}" + + +@pytest.mark.parametrize( + "series", + [ + pd.Series([1, 2, 3], dtype="int64"), + pd.Series([1, 2, 3], dtype="uint32"), + pd.Series([1.5, 2.5], dtype="float64"), + pd.Series([True, False], dtype="bool"), + ], + ids=["int64", "uint32", "float64", "bool"], +) +def test_order_detect_temporal_mode_skips_non_text_dtypes(series: pd.Series) -> None: + # Numeric/bool columns can never hold temporal *text*; the detector must + # short-circuit without the astype(str) + multi-regex scan (issue #1650). + assert order_detect_temporal_mode(series) is None + + +def test_order_detect_temporal_mode_still_detects_text_temporals() -> None: + # Gate must not regress detection on object/string columns. + assert order_detect_temporal_mode(pd.Series(["2020-01-01", "2020-02-02"], dtype="object")) == "date" + assert order_detect_temporal_mode(pd.Series(["abc", "def"], dtype="object")) is None + + +def test_where_rows_numeric_filter_returns_correct_rows() -> None: + # End-to-end: a numeric where_rows comparison still filters correctly with the + # temporal-detection gate in place. + nodes_df = pd.DataFrame({"id": [0, 1, 2, 3], "val": [10, 60, 51, 99]}) + edges_df = pd.DataFrame({"s": [0, 1], "d": [2, 3]}) + from graphistry.compute.ast import where_rows + + out = _mk_graph(nodes_df, edges_df).gfql([rows(), where_rows(expr="val > 50")])._nodes + assert sorted(out["val"].tolist()) == [51, 60, 99]