Skip to content
Merged
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
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/e
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and all PyGraphistry-specific breaking changes are explictly noted here.

## Dev
### Breaking 🔥
* GFQL: Renamed `chain()` methods to `gfql()` for clarity
* **Migration required**: Update your code as follows:
* `g.chain([...])` → `g.gfql([...])`
* `g.chain_remote([...])` → `g.gfql_remote([...])`
* `g.chain_remote_shape([...])` → `g.gfql_remote_shape([...])`
* The old `chain*` methods are deprecated and will be removed in a future version
* All functionality remains the same, only the method names have changed

### Added
* GFQL: Add comprehensive validation framework with detailed error reporting
* Built-in validation: `Chain()` constructor validates syntax automatically
* Schema validation: `validate_chain_schema()` validates queries against DataFrame schemas
* Pre-execution validation: `g.chain(ops, validate_schema=True)` catches errors before execution
* Pre-execution validation: `g.gfql(ops, validate_schema=True)` catches errors before execution
* Structured error types: `GFQLValidationError`, `GFQLSyntaxError`, `GFQLTypeError`, `GFQLSchemaError`
* Error codes (E1xx syntax, E2xx type, E3xx schema) for programmatic error handling
* Collect-all mode: `validate(collect_all=True)` returns all errors instead of fail-fast
Expand Down
2 changes: 1 addition & 1 deletion docs/source/10min.rst
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ Suppose you want to focus on attacks that started with the "MS08067 (NetAPI)" vu

.. code-block:: python

g2 = g1.chain([
g2 = g1.gfql([
n(),
e(edge_query="vulnName == 'MS08067 (NetAPI)' & `time(max)` > 1421430000"),
n(),
Expand Down
30 changes: 15 additions & 15 deletions docs/source/cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ It is easy to turn arbitrary data into insightful graphs. PyGraphistry comes wit
```python
from graphistry import n, e_undirected, is_in

g2 = g1.chain([
g2 = g1.gfql([
n({'user': 'Biden'}),
e_undirected(),
n(name='bridge'),
Expand All @@ -186,7 +186,7 @@ It is easy to turn arbitrary data into insightful graphs. PyGraphistry comes wit
import cudf
g2 = g1.edges(lambda g: cudf.DataFrame(g._edges))
# GFQL will automaticallly run on a GPU
g3 = g2.chain([n(), e(hops=3), n()])
g3 = g2.gfql([n(), e(hops=3), n()])
g3.plot()
```

Expand Down Expand Up @@ -914,7 +914,7 @@ g2.plot() # nodes are values from cols s, d, k1
destination_node_match={"k2": 2},
destination_node_query='k2 == 2 or k2 == 4',
)
.chain([ # filter to subgraph with Cypher-style GFQL
.gfql([ # filter to subgraph with Cypher-style GFQL
n(),
n({'k2': 0, "m": 'ok'}), #specific values
n({'type': is_in(["type1", "type2"])}), #multiple valid values
Expand All @@ -937,14 +937,14 @@ g2.plot() # nodes are values from cols s, d, k1
.collapse(node='some_id', column='some_col', attribute='some val')
```

Both `hop()` and `chain()` (GFQL) match dictionary expressions support dataframe series *predicates*. The above examples show `is_in([x, y, z, ...])`. Additional predicates include:
Both `hop()` and `gfql()` (GFQL) match dictionary expressions support dataframe series *predicates*. The above examples show `is_in([x, y, z, ...])`. Additional predicates include:

* categorical: is_in, duplicated
* temporal: is_month_start, is_month_end, is_quarter_start, is_quarter_end, is_year_start, is_year_end
* numeric: gt, lt, ge, le, eq, ne, between, isna, notna
* string: contains, startswith, endswith, match, isnumeric, isalpha, isdigit, islower, isupper, isspace, isalnum, isdecimal, istitle, isnull, notnull

Both `hop()` and `chain()` will run on GPUs when passing in RAPIDS dataframes. Specify parameter `engine='cudf'` to be sure.
Both `hop()` and `gfql()` will run on GPUs when passing in RAPIDS dataframes. Specify parameter `engine='cudf'` to be sure.

### Table to graph

Expand Down Expand Up @@ -1065,23 +1065,23 @@ g5 = g2.hop(pd.DataFrame({g4._node: g4[g4._node]}), hops=1, direction='undirecte
g5.plot()
```

Rich compound patterns are enabled via `.chain()`:
Rich compound patterns are enabled via `.gfql()`:

```python
from graphistry import n, e_forward, e_reverse, e_undirected, is_in

g2.chain([ n() ])
g2.chain([ n({"x": 1, "y": True}) ]),
g2.chain([ n(query='x == 1 and y == True') ]),
g2.chain([ n({"z": is_in([1,2,4,'z'])}) ]), # multiple valid values
g2.chain([ e_forward({"type": "x"}, hops=2) ]) # simple multi-hop
g3 = g2.chain([
g2.gfql([ n() ])
g2.gfql([ n({"x": 1, "y": True}) ]),
g2.gfql([ n(query='x == 1 and y == True') ]),
g2.gfql([ n({"z": is_in([1,2,4,'z'])}) ]), # multiple valid values
g2.gfql([ e_forward({"type": "x"}, hops=2) ]) # simple multi-hop
g3 = g2.gfql([
n(name="start"), # tag node matches
e_forward(hops=3),
e_forward(name="final_edge"), # tag edge matches
n(name="end")
])
g2.chain(n(), e_forward(), n(), e_reverse(), n()]) # rich shapes
g2.gfql(n(), e_forward(), n(), e_reverse(), n()]) # rich shapes
print('# end nodes: ', len(g3._nodes[ g3._nodes.end ]))
print('# end edges: ', len(g3._edges[ g3._edges.final_edge ]))
```
Expand All @@ -1096,7 +1096,7 @@ from graphistry.compute.chain import Chain
pattern = Chain([n(), e(), n()])
pattern_json = pattern.to_json()
pattern2 = Chain.from_json(pattern_json)
g.chain(pattern2).plot()
g.gfql(pattern2).plot()
```

Benefit from automatic GPU acceleration by passing in GPU dataframes:
Expand All @@ -1105,7 +1105,7 @@ Benefit from automatic GPU acceleration by passing in GPU dataframes:
import cudf

g1 = graphistry.edges(cudf.read_csv('data.csv'), 's', 'd')
g2 = g1.chain(..., engine='cudf')
g2 = g1.gfql(..., engine='cudf')
```

The parameter `engine` is optional, defaulting to `'auto'`.
Expand Down
34 changes: 17 additions & 17 deletions docs/source/gfql/about.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ You can filter nodes based on their properties using the `n()` function.

from graphistry import n

people_nodes_df = g.chain([ n({"type": "person"}) ])._nodes
people_nodes_df = g.gfql([ n({"type": "person"}) ])._nodes
print('Number of person nodes:', len(people_nodes_df))

**Explanation:**

- `n({"type": "person"})` filters nodes where the `type` property is `"person"`.
- `g.chain([...])` applies the chain of operations to the graph `g`.
- `g.gfql([...])` applies the chain of operations to the graph `g`.
- `._nodes` retrieves the resulting nodes dataframe.

2. Find 2-Hop Edge Sequences with an Attribute
Expand All @@ -81,7 +81,7 @@ Traverse multiple hops and filter edges based on attributes using `e_forward()`.

from graphistry import e_forward

g_2_hops = g.chain([ e_forward({"interesting": True}, hops=2) ])
g_2_hops = g.gfql([ e_forward({"interesting": True}, hops=2) ])
print('Number of edges in 2-hop paths:', len(g_2_hops._edges))
g_2_hops.plot()

Expand All @@ -101,9 +101,9 @@ Label hops in your traversal to analyze specific relationships.

from graphistry import n, e_undirected

g_2_hops = g.chain([
n({g._node: "a"}),
e_undirected(name="hop1"),
g_2_hops = g.gfql([
n({g._node: "a"}),
e_undirected(name="hop1"),
e_undirected(name="hop2")
])
first_hop_edges = g_2_hops._edges[ g_2_hops._edges.hop1 == True ]
Expand All @@ -127,7 +127,7 @@ Chain multiple traversals to find patterns between nodes.

from graphistry import n, e_forward, e_reverse

g_risky = g.chain([
g_risky = g.gfql([
n({"risk1": True}),
e_forward(to_fixed_point=True),
n({"type": "transaction"}, name="hit"),
Expand Down Expand Up @@ -155,7 +155,7 @@ Use the `is_in` predicate to filter nodes or edges by multiple values.

from graphistry import n, e_forward, e_reverse, is_in

g_filtered = g.chain([
g_filtered = g.gfql([
n({"type": is_in(["person", "company"])}),
e_forward({"e_type": is_in(["owns", "reviews"])}, to_fixed_point=True),
n({"type": is_in(["transaction", "account"])}, name="hit"),
Expand Down Expand Up @@ -195,7 +195,7 @@ GFQL is optimized for GPU acceleration using `cudf` and `rapids`. When using GPU
g_gpu = graphistry.edges(e_gdf, 'src', 'dst').nodes(n_gdf, 'id')

# Run GFQL query (executes on GPU)
g_result = g_gpu.chain([ ... ])
g_result = g_gpu.gfql([ ... ])
print('Number of resulting edges:', len(g_result._edges))

**Explanation:**
Expand All @@ -213,7 +213,7 @@ You can explicitly set the engine to ensure GPU execution.

::

g_result = g_gpu.chain([ ... ], engine='cudf')
g_result = g_gpu.gfql([ ... ], engine='cudf')

**Explanation:**

Expand Down Expand Up @@ -259,9 +259,9 @@ Use PyGraphistry's visualization capabilities to explore your graph.
from graphistry import n, e

# Filter nodes with high PageRank
g_high_pagerank = g_enriched.chain([
n(query='pagerank > 0.1'),
e(),
g_high_pagerank = g_enriched.gfql([
n(query='pagerank > 0.1'),
e(),
n(query='pagerank > 0.1')
])

Expand All @@ -284,7 +284,7 @@ You may want to run GFQL remotely because the data is remote or a GPU is availab

from graphistry import n, e

g2 = g1.chain_remote([n(), e(), n()])
g2 = g1.gfql_remote([n(), e(), n()])

**Example: Run GFQL remotely, and decouple the upload step**

Expand All @@ -294,7 +294,7 @@ You may want to run GFQL remotely because the data is remote or a GPU is availab

g2 = g1.upload()
assert g2._dataset_id is not None, "Uploading sets `dataset_id` for subsequent calls"
g3 = g2.chain_remote([n(), e(), n()])
g3 = g2.gfql_remote([n(), e(), n()])

Additional parameters enable controlling options such as the execution `engine` and what is returned

Expand All @@ -307,8 +307,8 @@ Additional parameters enable controlling options such as the execution `engine`

g2 = graphistry.bind(dataset_id='my-dataset-id')

nodes_df = g2.chain_remote([n()])._nodes
edges_df = g2.chain_remote([e()])._edges
nodes_df = g2.gfql_remote([n()])._nodes
edges_df = g2.gfql_remote([e()])._edges

**Example: Run Python on remote GPUs over remote data**

Expand Down
2 changes: 1 addition & 1 deletion docs/source/gfql/combo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Simple Plotting

.. code-block:: python

g2 = g1.chain(gfql_query)
g2 = g1.gfql(gfql_query)
g2.plot()

**Explanation**:
Expand Down
Loading
Loading