Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Development]
<!-- Do Not Erase This Section - Used for tracking unreleased changes -->

### 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
Expand Down
7 changes: 7 additions & 0 deletions graphistry/compute/gfql/row/ordering.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions graphistry/tests/compute/gfql/row/test_ordering.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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]
Loading