Repository navigation
0.59.1
PyPI: https://pypi.org/project/graphistry/0.59.1/ · Docs: https://pygraphistry.readthedocs.io/en/latest/gfql/performance.html
Highlights: GFQL performance release — the in-memory bulk-hop regression fix, the re-measured GraphBench / SNB Interactive boards against Kuzu, Memgraph, and Neo4j, the GraphFrames ladder to 1.8B edges, and the rewritten GFQL docs (engines, indexing, benchmarks). Full changelog below.
Added
- GFQL
searchAny/search_nodes/search_edgesnow search datetime columns when the term is numeric, matching what the viz inspector displays rather thanrepr: a timestamp renders as the inspector's moment format (Jan 5 2024, 3:04:05 am UTC), so"2024"finds rows in that year. Datetimes previously declined. The zone is atemporal_tzparameter (default UTC) because the inspector renders in the viewer's zone, which a server cannot know — and the zone is not cosmetic, since it decides which day a timestamp lands on and therefore which rows match. Verified byte-identical to realmoment-timezoneon 3000 timestamps across 12 zones, including both DST directions, half- and quarter-hour offsets, and zones whose abbreviation changes seasonally.- Searching a datetime column means rendering it, which a numeric-term search now pays for on every datetime column present: on a 300k-row frame one datetime column takes a
searchAnyfrom 77ms to 629ms, about three times what a float column added when #2089 gated floats in the same way. A term that is not numeric does not gate datetime columns in and pays nothing, which is pinned by spying on the renderer rather than by timing. - pandas and polars render every zone and answer identical rows; polars matches pandas on 2968/3000 fuzz cases, the residue being
Africa/Casablanca, where polars' bundled timezone database and Python's disagree about its future DST. cuDF matches at UTC (0/5000 mismatches on a GB10) and declines any other zone rather than mislabel it: cudf 26.02.01'sstrftimeignorestz_convert, answeringUTCfor%Zwhatever the zone and reading the underlying UTC instant for%p, so anAsia/Kolkatacolumn would render the right hour beside the wrong meridiem.
- Searching a datetime column means rendering it, which a numeric-term search now pays for on every datetime column present: on a 300k-row frame one datetime column takes a
- GFQL
searchAny/search_nodes/search_edgesnow search float columns when the term is numeric, matching what the viz inspector displays rather thanrepr: fractional values render to a fixed number of decimals (so0.1 + 0.2is found by"0.3"and never by"0.30000000000000004"), whole values render without a trailing.0, and NaN plus the Int32 sentinel2147483647display nothing and never match. Precision is afloat_precisionparameter (default 4). This removes the previous cuDF float decline and the polars float exclusion (#1695).- Only pandas can reproduce the inspector's half-away-from-zero rounding; polars and cuDF round half-to-even, so a value whose
float_precision+1-th decimal is exactly 5 renders one unit lower there. Measured on 26,008 realistic column values: pandas 100%, cuDF 99.996%, polars 99.931% agreement with the UI. Pinned intest_known_cross_engine_divergences.py.
- Only pandas can reproduce the inspector's half-away-from-zero rounding; polars and cuDF round half-to-even, so a value whose
Changed
- GFQL polars:
prune_to_endpointson a single-hop edge is a typed decline (NotImplementedError, useengine='pandas'or'cudf'); the plain single-hop branch and the full polars traversal used to admit it and silently return both endpoints where pandas keeps the arrival side. Variable-length hops keep their native pruning (#2053). - GFQL: the general chain path keeps integer and boolean node attribute dtypes on a closed graph: the endpoint-closure backfill now appends only endpoints missing from the node frame instead of concatenating every endpoint id and deduplicating afterwards, which widened the attribute columns to float on every route that fell through the hot paths (#2058, chain seam; the row-pipeline pivot dtypes are a separate item).
- GFQL: the chain specializations move into
graphistry/compute/chain_specializations/{admission,hotpaths}.py(pandas/cuDF single-node lane, seeded typed single hop, seeded typed RETURN-destination) andgraphistry/compute/gfql/lazy/engine/polars/chain_specializations/{admission,hotpaths}.py(polars plain single-hop branches, seeded lane, RETURN-destination), each lane next to the admission predicate the dispatcher calls (native_fast_path_admits,polars_plain_single_hop_admits,polars_seeded_lane_admits);chain.pyand the polars chain only dispatch,chain_fast_paths.pykeeps the shared seed/index helpers. No route admits or declines anything it did not before. Tests mirror the new paths and filter one shared shape corpus per route with the route's own gate;GFQL_ROUTES_OFF=<route,...>(test conftest) makes named hot paths decline so every existing test replays through the other routes, andbin/test-routes-off.shreports the per-route divergences. - GFQL: the wavefront seed-rediscovery rule moved out of
hop.pyintographistry/compute/gfql/seed_rediscovery.py(pandas/cuDF) andgraphistry/compute/gfql/lazy/engine/polars/seed_rediscovery.py(polars);undirected_rediscovered_seed_ids(an internal helper) is gone.
Performance
- GFQL
searchAnyover a datetime column answers a live, as-you-type search on 30M rows in 412ms instead of 43.7s — 106x, measured on a GB10 with every arm checked equal to the rendered answer. A numeric search term cannot straddle two fields of the inspector's date format, because the separators between them are a space, a comma and a colon and a term may contain none of those. So the rendered text is never needed: the search becomes membership tests over narrow integer arrays for day, year, hour, minute, second and zone, and a term selects a handful of the at most sixty values a field can take. The components are built once per column and zone and held in a 512MB LRU keyed by a digest of the timestamps' own buffer — not by identity, which serves a stale answer once a frame is mutated in place, and order-sensitively, since a sorted column is a different index. Applies to pandas; polars still renders (399ms at 1M, 4.3s at 10M) and cuDF is unchanged. - GFQL: seeded fixed-hop chains on the indexed bindings path do less per-hop frame work — an array-side join estimate, the endpoint filter evaluated before the wide node gather, the endpoint semi-join skipped when it is the identity, and (polars) a searchsorted path expand join — and a bare
rows()immediately followed byselectattaches only the properties the select reads (rows(attach_prop_columns=...)) on pandas, cuDF, and polars. Same rows, order, and dtypes; LDBC SNB IC8recent-replieson polars drops from about 24 ms to 11 ms locally (#2084).
- GFQL: an undirected
hop()on pandas and cuDF hash-deduplicated the doubled edge frame (both orientations, 2E rows) on (from, to, edge id) before traversing, once per hop call and three times per chain; every consumer already dedups on the edge id, so the frame is now the plain concat the polars hop uses. Measured locally and not published: the block itself went from 7.1 s to 1.4 s on an 8M-edge frame on pandas and 50 ms to 17 ms on cuDF; the benchmark numbers are re-measured in pyg-bench. - GFQL latency: this release closes a set of point-lookup regressions found while re-measuring the LDBC SNB Interactive shapes, and adds the gates that stop them coming back. Regressions fixed: every Cypher string query on a wide pandas node table re-scanned the object columns to key the compile cache (#2029); an undirected multi-hop paid a per-edge Python loop in the seed-rediscovery rule (#2023) and a whole-frame dedup of the doubled edge frame on pandas and cuDF; the LDBC IS1 form (a seeded hop returning properties of both aliases) and the seeded single-node lookup ran the full chain with no index consulted; native op-list chains scanned the node table for a seed given on a non-binding property even with a node-property index resident, declined named patterns that the chain fast path could serve, and on polars never consulted the resident seed indexes at all (#2027, #2033; now served through the resident node-id, adjacency and node-property indexes on pandas, cuDF and polars, with the engagement visible in
gfql_explain). Gates added:test_gfql_latency_contract.py(fast path served plus a bounded ratio to plain frame ops for the basic shapes on pandas, polars and cuDF),test_hop_scaling_pin.py(undirected 2-hop cost bounded relative to two plain joins), and, in the private benchmark repository, a point-latency sentinel on the real SF0.1 fixture with absolute sub-10 ms thresholds that runs on every release candidate before the documentation numbers are re-vendored, plus an exporter gate that fails any unexplained slowdown or new loss in the published cells. The sentinel also times native op-list twins of the same shapes with untimed receipts of the route that served them and value parity with the full path, so a fast path that stops engaging is reported rather than hidden behind a still-correct answer.
Fixed
circle_layout()entry guards (#1968): an edge-only graph no longer crashes withTypeError: object of type 'NoneType' has no len()— nodes are materialized before they are counted, matching every other layout on the surface. Counting after materialization also fixes the stale count behind it: a graph carrying a node table with no node binding had its edge-derived replacement laid out at the old table's length, which surfaced asAssertionError: NaN values detected in x positions. The no-argument form on a graph withoutx/ypositions now raises aValueErrornaming the documented precondition (passbounding_box=or have positions) instead of a bareKeyError: 'x'. A partition-keyedbounding_boxframe with duplicatepartition_keyvalues now raises aValueErrornaming the duplicated keys instead of a pandasIntCastingNaNErrorfrom the row-multiplying merge — on cuDF the same frame surfaced asValueError: CuPy currently does not support masked arrays, and both engines now give the same typed message — and one missingpartition_key/cx/cy/w/hsays which columns are missing instead of raisingKeyErrorfrom inside the merge. A partition-keyed frame passed withoutpartition_by— the frame is then iterated as if it were the 4-tuple — says so instead ofValueError: too many values to unpack (expected 4). A null value in apartition_bycolumn raises aValueErrornaming that column, where the old message —Unexpected NaNs in node indices— sent the reader to node ids;groupby(...).cumcount()returns NaN for a null group key, so the node indices were only the messenger. Based on #2008 by @SAY-5.- The engine comparison that chose between the two angle formulas,
engine_concrete in [EngineAbstract.CUDF, 'cudf', 'Engine.CUDF'], was alwaysFalse—engine_concreteis anEngine, a different enum that is not a str-enum — so the arm was selected only by ahasattr(node_idx_relative, 'to_pandas')duck-type that happens to track cuDF. The arms differed in one thing: the CPU arm's.fillna(0.0)turned a NaN angle into angle 0 where the GPU raised. There is now one expression for both engines, and a NaN angle is unreachable — the node indices and the per-partition sizes are each already checked and a partition is never empty — so the existingUnexpected NaNs in node anglescheck is the single backstop. cuDF and pandas are pinned to identical positions on a GB10.
- The engine comparison that chose between the two angle formulas,
- Engine coercion can still name the engines when it degrades:
ensure_engine_matchreturns the original graph rather than crash a user's workflow, and its handler reports which engines it saw — but those names are resolved inside the sametry, so a failure early enough left them unset. The handler papered over this with'nodes_engine' in locals(); the names are now simply bound before thetry, and the degradation contract is pinned by tests for a failure both before and after they are resolved. adjacency_membership_keysno longer depends on its direction argument being one of three strings to have bound anything:oiandiiwere each assigned only under their own direction check, so an unrecognized direction reachedassert oi is not Nonewith nothing bound and raisedUnboundLocalErrorinstead of the assertion. Both are initialized up front; the existing asserts now say what they were written to say.from_igraphdetermines its node ID column without a bookkeeping flag: the branch that un-swizzles integer vertex IDs tracked whether it had found a matching column in a separatefoundboolean, so the column name was bound inside the loop and read outside it. Bindingnode_id_col = Noneup front and testing that instead removes the flag and the possibly-unbound read. Behaviour is unchanged; round-trips through igraph produce byte-identical edge frames for named vertices, integer vids and a bare attribute-less graph.- Two feature-pipeline functions raised
UnboundLocalErroron reachable inputs:transform()boundXonly inside itsnodes/edgesbranches, whileyandTbeside it were pre-initialized, so any otherkind— andkindis a plainstrthatFastEncoderdoes not validate — crashed at the firstX.empty; andget_numeric_transformers()returnsndf_unconditionally but assigned it only whenndf is not None, so passingNonecrashed, where the siblingy_ = yhad the same shape and worked. Both now bind on every route, matching the idiom already used beside them. - Three layout paths read a local that an earlier branch may never have bound, so the failure mode was
UnboundLocalErrorrather than the intended behaviour.GraphBase.__init__read the componentsin its connectivity check even when no edge ever assigned it (reachable when vertices arrive with a component already set); it now initializes toNone, which the existing check already treats as unconnected.tangents()appended the leaked loop variable after the loop to repeat the final tangent; it now appends by value.mercator_layoutboundcupyonly inside its import guard and gated on a separateuse_cupyflag; the flag is folded intocp is not None. No behaviour changes:tangentswas verified identical on every input, and the mercator cupy branch was verified equal to the numpy branch to 1e-9 on a GB10 (cudf 26.02.01, cupy 13.6.0) including equator and ±89° cases. PlotterBase's swallow-and-continue paths are exercised rather than assumed: the fivemaybe_*optional-import helpers (cudf, dask_cudf, dask.dataframe, pyspark, polars) now have tests pinning that each returnsNonewhen its library is absent and the module when present, which is the contract engine dispatch is built on; and twoexcept:guards that must never be able to fail an upload — a failure insideinfer_labels, and an edges table whoselen()raises — are pinned with tests that fail if the guard is removed. No behaviour change: a bare1used as a no-op statement becomespass, and two no-ops that merely followed alogger.debug(...)are deleted. Retires 9 findings from the pyright ratchet.graphistry.outliersis importable again without matplotlib, which is not a dependency, so this was the default install. The module guards its optional imports withtry/exceptand binds each name toNoneon failure, but two things defeated that:import matplotlib.font_managerbindsmatplotlib, and that name was missing from the fallback list (outliers.py:162would have raisedNameError); andget_outliers' signature annotatesUnion[np.ndarray, pd.DataFrame], evaluated atdeftime, so withnp = Nonethe module raisedAttributeError: 'NoneType' object has no attribute 'ndarray'before reaching any function. Annotations are now lazy (from __future__ import annotations) and the fallback list is complete, so the module imports and degrades as it was written to. Nothing insidegraphistry/imports it, so this only affected callers ofgraphistry.outliersdirectly.- An optional-import guard no longer leaves a name unbound on the path that skips it:
if polars: import polars as plincompute/gfql_unified.pymakespla function-local, so the pandas path left it unbound rather than falling back to any module-level binding. Both arms are guarded by the same flag, so no behaviour changes; the binding is made explicit instead. - GFQL Polars: id-set membership (
is_in) is spelled per installed polars version by one helper (membership.is_in_ids): the imploded RHS on polars >= 1.28, the bare Series RHS below it. FixesComputeError: shapes don't match ... 'is_in'on polars 1.21 (RAPIDS 25.02) for hop endpoint resolution, the seeded chain lanes, and EXISTS/OPTIONAL row ops, and removes the remaining bare-Series deprecation warnings on polars >= 1.28 (#2082, #1938). - GFQL: record whole-entity projection kind and presence independently of identity columns, so adapters distinguish unlabeled entities, property returns, and absent entities; preserve presence through OPTIONAL null-fill and reentry (tck-gfql#200).
- GFQL: explicit
polars-gpurow aggregation collects the aggregate plan on GPU; unsupported aggregation plans raise a structured operation decline (#2075). - GFQL: reject malformed aggregation sources during validation while preserving literal column names and valid expressions (#2076).
- GFQL Polars: property returns from multiple aliases gather existing columns directly around the endpoint joins, avoiding expression-plan overhead.
- GFQL Polars: single-node property returns gather columns directly, removing expression-plan collection from this fast path.
- GFQL: indexed scalar node and directed single-hop queries followed by
rows(source=...)and optionalselect(...)now produce row tables directly on pandas, cuDF, and Polars. Joined property projections preserve repeated edge matches without constructing full traversal results. - GFQL: indexed scalar equality filters no longer retain null-valued rows when the comparison produces a nullable boolean mask.
- GFQL: a native chain whose edge alias is named like the source, destination or edge-ID binding column silently overwrote that binding on pandas and cuDF (the seed's edges vanished, or the edge ids became
True) and raised a raw polarsSchemaError; it is now the same typed decline (E108) the node-ID collision already gets, on every engine, before execution — the rule the Cypher route already applied (#2050). - GFQL polars: a colliding alias no longer leaves the internal
__gfql_shadow_restore__<alias>__column on op-list results; the chain keeps the shadowed values under that name only while a compiled Cypher pipeline runs (its row pipeline reads them back), and ongfql([...])/chain([...])the marker simply shadows the column as on pandas and cuDF. Nothing is stripped from results: user-defined columns and pipelines composed from successivegfqlcalls are untouched. - GFQL: a multi-hop step (
hops=2,to_fixed_point) whose edge alias shares its name with the column its own filter uses raisedincompatible-column-typeon pandas and cuDF; the backward re-execution now reads the graph's edge columns for that step, so the eager engines serve it like the single-hop form and return the same rows as polars (#2049). - GFQL: one alias-shadowing contract on every engine for op-list chains: an alias named like a column of the frame it marks becomes the boolean marker under that name (pandas, cuDF and now polars, which used to keep the user values and leak a
_rightjoin suffix). The shadowed values ride under the internal restore column the Cypher row pipeline already resolves, so Cypher keeps reading the user value through the variable (MATCH (a)-[type:K]->(b) RETURN type.type) on both engines; the polars residual decline for that shape is gone. - GFQL polars: an unnamed, untyped single-hop chain kept duplicate node rows (a node table carrying the same id twice) where pandas, cuDF and every other polars shape collapse them; the plain single-hop branch now returns one row per node id (#2051).
- GFQL polars-gpu: the fused single-hop grouped-aggregate lane no longer takes the
value_countsformulation on the GPU target; itsunnestnode has no cudf-polars implementation, so every fusedcount(*)shape declined to the generic route onengine='polars-gpu'. Thegroup_byformulation is GPU-executable and answers the same rows (#2064). - GFQL schema validation tracks columns removed by row projections,
group_by, anddrop_cols, while retaining extended projections and prefix grouping keys. - GFQL: a
group_byaggregation source naming an existing column whose name contains a dot or a minus (analysis.cohorts.Activist,x.Pro-Vax) was rejected as a missing column (E301analysis, E301Vax): schema validation parsed every source as a row expression and split it on the operator, so uploaded column names both executors would serve never reached them. A source that is literally a column of the active row table is now required as that column — the precedence pandas and polars already use (the pandas row pipeline checks the column before parsing, and the polars aggregate accepts only columns) — and a source that is not a column is still parsed as an expression.
- cuDF 26.2 compatibility:
cudf.from_pandasreplaces the removedcudf.DataFrame.from_pandasat the five cuDF-only product sites (ai_utils,umap_utils,feature_utils) and in the test fixtures (#2043); the three cuDF chain differential cases that disagree with the full path on cuDF 26.2 are marked expected-failure on that line with the tracking issue, so the GPU lane reports them instead of crashing before them. - GFQL pandas/cuDF: several single-alias
IN(and other pushed-down) predicates across a hop no longer raiseUnalignable boolean Series(#2020): the predicate pushdown filtered an alias frame by label after an earlier pushdown had already narrowed it, while the mask it evaluated carried a fresh positional index. Rows are now kept by position, which is the contract of a mask computed on the same rows; results equal the polars engine and the scalar=form. - GFQL polars: a native chain whose edge alias shares its name with the column that step filters on is served instead of raising
incompatible-column-type(#2039): the backward pass and the pruned re-execution now run each step on the graph's original edge columns, and a node step whose alias names its own filtered column filters the graph's node values rather than the marker of an earlier pass, so a stamped alias marker is never re-filtered as that column; pandas and polars return the same rows. A cross-engine collision matrix pins the single-hop shapes and records the multi-hop (#2049) and binding-column (#2050) forms as expected failures. The Cypher rows-route projection of such an alias on polars still declines with a typed error (pinned) and stays tracked.
- GFQL: two basic Cypher shapes fell off the seeded fast paths on every engine. A seeded typed hop whose RETURN projects properties of more than the destination alias (
MATCH (m {id})-[r:T]->(p) RETURN m.id, r.since, p.name) went through the row pipeline's connected-bindings frame builder; the seeded fast path now projects any number of properties from any alias of the hop (seed node, edge, destination node) from the rows it already holds, one row per matched edge, and still declines anything beyond one hop or an absent property to the full path. A seeded single-node pattern (MATCH (p {id}) RETURN p/RETURN p.a, p.b) ran the full two-pass chain over the node table with no index consulted; a newseeded_node_lookupfast path resolves the seed through the resident node-id index, a resident node-property index, or one scalar scan and projects the matched rows directly. Seed resolution is shared with the seeded typed hop, so a seed predicate on an indexed property that is not the node binding now uses the property index there too. On the SNB SF0.1 fixture the IS1 text went from 19 ms to 4 ms and the node-only lookup from 14 ms to 1 ms on pandas; both shapes reportfast_path servedingfql_explainon pandas, polars and cuDF. - GFQL: native Polars single-node lookups resolve seeds through resident node-property indexes, and a directed scalar one-hop chain whose resident node-id and adjacency indexes cover it (typed edges, destination filters, named patterns) is served by the seeded reduction used by Cypher, with full-path table order and alias flags preserved and the engagement visible in
gfql_explain(#2033). - GFQL: the pandas and cuDF native op-list lanes (single-node lookup, seeded typed hop) record their resident-index engagement in
gfql_explain(seamsnative_seed_lookup/native_seeded_hop), matching the polars lane; a lane that stops engaging is now visible instead of silently correct. - GFQL: native op-list chains (
g.gfql([n(...), e_forward(...), n(...)])) resolve a seed predicate through the resident node-id or node-property index and serve named patterns on the chain fast path; before, a named single-node op always ran the full two-pass chain, a named seeded hop declined the fast path whenever the traversal indexes were resident (deferring to an indexed kernel that had already declined), and a seed on a property other than the node binding was a full node-table scan (#2027). The alias flag columns now sit where the full path places them. - GFQL: every Cypher string query re-read the node table's dtypes to build its compile-cache key, and that read scans the values of every
objectcolumn (the string-content gate for predicate pushdown). On a wide pandas node table this cost more than the query it keyed: an SNB SF0.1 seeded lookup on 327k nodes × 27 object columns spent 105 ms of 106 ms there. The read is now memoized per node frame (identity plus a length/columns fingerprint, the resident indexes' contract) and cleared bygfql_clear_caches(); the same lookup now takes 2 ms (#2029). - GFQL: undirected multi-hop traversals with a seeded wavefront were ~30x slower than before 0.58 because the wavefront seed-rediscovery rule (a seed is returned only when an edge-disjoint walk re-encounters it) ran as a per-edge Python loop on every undirected hop step. The rule now runs as joins and group-bys inside the caller's own frame engine (pandas, cuDF, polars), with the same results (#2023).
Documentation
- GFQL docs navigation is split into a short start path and two hubs:
gfql/indexnow lists four Start Here pages, a Performance and Benchmarks hub (gfql/perf/index), a Language Reference hub (gfql/reference/index), and the developer specs. No page moved, so existing URLs are unchanged; the previously unlistedgfql/loading_graph_datapage joins the reference hub. - The GFQL benchmark pages state the result first and consolidate caveats:
gfql/benchmark_filter_pagerank(now titled as a case study and listed under Start Here) andgfql/benchmark_graphframesopen with what the page compares and the measured outcome (GraphFrames: GFQL's best engine faster in all eight cells, CPU alone in six), keep every number unchanged, and move measurement rules into one "Method and limits" section per page. The GraphFrames page gains per-task bar charts rendered fromgfql/_static/graphframes/results.jsonby the same generator and sync test as the Neo4j charts. Thebench-provenancedirective now accepts several run ids and renders one Measurement block with caveats folded in; run provenance strings name the RAPIDS base image (pyg-bench #220). - The speedup case study renders GFQL arms re-measured at the 0.59.0 release commit: the vendored artifact now carries the 2026-09-04 filter/PageRank GFQL cells (Twitter CPU 2.62 s, GPU 0.23 s; GPlus CPU 34.11 s, which selects exactly the locked Neo4j lane's node set). The GPlus GPU arm selects a different node set than the CPU arm (Jaccard 0.91), so its time renders as a diagnostic and no GPlus GPU-vs-CPU ratio is published; the Twitter ratio is 11.3x. The pandas pipeline slowdown versus the July measurement is attributed by a same-harness A/B (#2021) and the cuGraph/igraph tail difference is tracked in #2022.
- GFQL performance page renders the relaned four-engine q1–q9 board and the aligned SNB matrix: the vendored pyg-bench artifact now carries the 2026-08-13 board (GFQL bound cold per timed run) with Kuzu, Memgraph, and Neo4j columns and the SNB-derived SF0.1/SF1 four-engine tables, where the databases win every point-lookup cell.
gfql/performancedrops its release-pinned heading and every hand-typed legacy figure (typed-hop fast path, index timings, the old Neo4j five-query table, the bulk engine sweep, LadybugDB) in favor of published cells, one Measurement block, and win counts derived from cells by a new:bench-tally:role. The bench extension now enforcespolicy.max_compute_commit_driftagainst the checkout (git history permitting) with per-run waivers carried in the artifact, so a board measured long before the current compute code fails the build instead of aging silently.gfql/overviewstates where GFQL wins and where the databases win, with links. - GFQL docs wording and structure pass: Start Here is now the 10-minute guide, the overview, and the speedup case study; the quick reference and Cypher syntax guide head the Language Reference hub.
gfql/indexingandgfql/index_adjacencycarry plain-language titles. The engines page folds its opening note boxes into prose and its "coming from" table names the concrete change and where the measurement lives (Memgraph added, PuppyGraph dropped).gfql/aboutexamples 3–7 gain tested Cypher twins, example 4 now returns rows on its own sample graph, and the sample-graph block runs in the doc-example lane so later examples are checked against it. Marketing filler ("leverage", "seamless", "honest") is removed across the GFQL pages. - GFQL overview states the positioning up front:
gfql/overviewsays GFQL is the only open-source in-process Cypher on dataframes and frames it against the columnar engines readers already know. An empty hidden toctree that rendered the page as a folder is gone.
Infrastructure
- The test extra now pins
tzdata, so named-timezone tests do not depend on the host shipping a complete system tzdata. Slim images and minimal distros ship a partial/usr/share/zoneinfo— one withAmerica/andEurope/but noUS/makestest_align_mixed_tz_converts_only_mixed_datetime_pairsfail withZoneInfoNotFoundError: 'US/Eastern', which reads like a pandas behaviour change and is not one. Toggling onlytzdatain an otherwise identical environment moves the five temporal test modules from 1 failed / 1402 passed to 1403 passed. - CI: the gfql change filter now includes the chain engine (
compute/chain*.py,hop.py,gfql_fast_paths.py,filter_by_dict.py,ast.py,predicates/, and the chain/hop test files), so tck-gfql, the Cypher-frontend gates and the gfql benchmark lane run on a change to the chain engine; they were skipped on #2055. - CI tooling moves into
bin/ci/:bin/had grown to 34 flat files. The 13 CI-only scripts and baselines (the fourci_*_guard.pywith their baselines,ci_docs_only_check.py,ci_gpu_gate_audit.py,ci-hf-cache-key.py,changed_line_coverage.py,coverage_audit.py) now live underbin/ci/, leaving 21 developer-facing entry points at the top level. Pure relocation: no behaviour, flags or output changed, andgit mvkeeps history. Thetest-*.shrunners deliberately stay put — they are referenced 32 times across 15 files includingconftest.pyand committed coverage baselines, which is fixture churn for a cosmetic gain. - CI: pyright now runs as a ratcheted gate (
python-pyright, py3.12), catching conditionally-unbound locals, unresolved names and no-effect statements that ruff and mypy miss.bin/pyright.shpins pyright 1.1.414;bin/ci/ci_pyright_guard.pyholds findings to per-file counts inbin/ci/ci_pyright_baseline.json, so existing debt is grandfathered and new or moved code is not. Only five rules gate — those decided by a file's own control flow, names and syntax; rules that read third-party stubs vary by up to 5.5x across environments and are reported but never gated. Unparseable files and a collapse in analysis scope also fail the gate, so widening apyrightconfig.jsonexclude cannot quietly disable it. See DEVELOP.md "Pyright ratchet". Builds on #1123 by @JithinBathula; closes #1075. - CI:
test-docsruns on docs-only pull requests (#2018): the job neededpython-lint-types, which a docs-only change skips, and GitHub skips a job whose prerequisite was skipped. The gate now accepts skipped prerequisites and refuses only failed or cancelled ones, so documentation changes are built and tested before merge. - Benchmark-number age rule moved to the docs lane, quotable runs only:
docs/test_bench_numbers.pynow runs in the docs build rather than every Python test lane, and a run no board can quote no longer fails CI by aging; a page that cites one of its cells is still age-checked at the point of use.
Tests
- GFQL:
test_gfql_latency_contract.pypins the low-latency contract for basic Cypher shapes on a wide 300k-node, 30-object-column table across pandas, polars and cuDF: a seeded typed hop with projections or a whole-entity return must be served by a fast path and cost at most 12x the plain frame ops for the same lookup (an id mask plus one join), measured interleaved; the node-only seeded lookup with projections is a recorded gap (strict xfail) so closing it flips the pin.