diff --git a/CHANGELOG.md b/CHANGELOG.md index c586e14a03..a72604ed4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/source/10min.rst b/docs/source/10min.rst index 1108d988f4..97317b1c44 100644 --- a/docs/source/10min.rst +++ b/docs/source/10min.rst @@ -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(), diff --git a/docs/source/cheatsheet.md b/docs/source/cheatsheet.md index 98b105ca91..80325aa286 100644 --- a/docs/source/cheatsheet.md +++ b/docs/source/cheatsheet.md @@ -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'), @@ -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() ``` @@ -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 @@ -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 @@ -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 ])) ``` @@ -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: @@ -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'`. diff --git a/docs/source/gfql/about.rst b/docs/source/gfql/about.rst index b75d8076ec..e10a9f55a2 100644 --- a/docs/source/gfql/about.rst +++ b/docs/source/gfql/about.rst @@ -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 @@ -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() @@ -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 ] @@ -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"), @@ -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"), @@ -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:** @@ -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:** @@ -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') ]) @@ -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** @@ -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 @@ -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** diff --git a/docs/source/gfql/combo.rst b/docs/source/gfql/combo.rst index 72e37cd300..916cd3051a 100644 --- a/docs/source/gfql/combo.rst +++ b/docs/source/gfql/combo.rst @@ -25,7 +25,7 @@ Simple Plotting .. code-block:: python - g2 = g1.chain(gfql_query) + g2 = g1.gfql(gfql_query) g2.plot() **Explanation**: diff --git a/docs/source/gfql/datetime_filtering.md b/docs/source/gfql/datetime_filtering.md index 78990da77c..4467620d47 100644 --- a/docs/source/gfql/datetime_filtering.md +++ b/docs/source/gfql/datetime_filtering.md @@ -80,12 +80,12 @@ from graphistry import n from graphistry.compute import gt, lt, between # Filter nodes created after a specific datetime -recent_nodes = g.chain([ +recent_nodes = g.gfql([ n(filter_dict={"created_at": gt(pd.Timestamp("2023-01-01 12:00:00"))}) ]) # Filter edges within a date range -date_range_edges = g.chain([ +date_range_edges = g.gfql([ n(edge_match={"timestamp": between( datetime(2023, 1, 1), datetime(2023, 12, 31) @@ -102,12 +102,12 @@ from datetime import date from graphistry.compute import eq, ge # Filter nodes by exact date -specific_date = g.chain([ +specific_date = g.gfql([ n(filter_dict={"event_date": eq(date(2023, 6, 15))}) ]) # Filter nodes on or after a date -after_date = g.chain([ +after_date = g.gfql([ n(filter_dict={"start_date": ge(date(2023, 1, 1))}) ]) ``` @@ -121,7 +121,7 @@ from datetime import time from graphistry.compute import is_in, between # Filter events at specific times -morning_events = g.chain([ +morning_events = g.gfql([ n(filter_dict={"event_time": is_in([ time(9, 0, 0), time(9, 30, 0), @@ -130,7 +130,7 @@ morning_events = g.chain([ ]) # Filter events in time range -business_hours = g.chain([ +business_hours = g.gfql([ n(filter_dict={"timestamp": between( time(9, 0, 0), time(17, 0, 0) @@ -145,7 +145,7 @@ import pytz # Timezone-aware filtering eastern = pytz.timezone('US/Eastern') -tz_aware_filter = g.chain([ +tz_aware_filter = g.gfql([ n(filter_dict={ "timestamp": gt(pd.Timestamp("2023-01-01 12:00:00", tz=eastern)) }) @@ -164,7 +164,7 @@ Combine temporal predicates with other filters: from graphistry.compute import gt, lt, eq # Complex filter with multiple conditions -complex_filter = g.chain([ +complex_filter = g.gfql([ n(filter_dict={ "created_at": gt(datetime(2023, 1, 1)), "status": eq("active"), @@ -179,7 +179,7 @@ You can pass wire protocol dictionaries directly to predicates, which is useful ```python # Pass wire protocol dictionaries directly -filter_with_dict = g.chain([ +filter_with_dict = g.gfql([ n(filter_dict={"timestamp": gt({ "type": "datetime", "value": "2023-01-01T12:00:00", @@ -188,7 +188,7 @@ filter_with_dict = g.chain([ ]) # Works with all temporal predicates -date_range_filter = g.chain([ +date_range_filter = g.gfql([ n(filter_dict={"event_date": between( {"type": "date", "value": "2023-01-01"}, {"type": "date", "value": "2023-12-31"} @@ -196,7 +196,7 @@ date_range_filter = g.chain([ ]) # And with is_in for multiple values -time_filter = g.chain([ +time_filter = g.gfql([ n(filter_dict={"event_time": is_in([ {"type": "time", "value": "09:00:00"}, {"type": "time", "value": "12:00:00"}, @@ -216,7 +216,7 @@ Use temporal filters in complex graph traversals: ```python # Find all transactions after a date, then their related accounts -recent_transactions = g.chain([ +recent_transactions = g.gfql([ n(filter_dict={"type": eq("transaction"), "date": gt(date(2023, 6, 1))}), n(edge_match={"relationship": eq("involves")}), @@ -239,7 +239,7 @@ from graphistry.compute import DateTimeValue, gt dt_value = DateTimeValue("2023-01-01T12:00:00", "US/Eastern") # Use in predicate -filter_dt = g.chain([ +filter_dt = g.gfql([ n(filter_dict={"timestamp": gt(dt_value)}) ]) ``` @@ -256,7 +256,7 @@ start = DateValue("2023-01-01") end = DateValue("2023-12-31") # Use in between predicate -year_filter = g.chain([ +year_filter = g.gfql([ n(filter_dict={"event_date": between(start, end)}) ]) ``` @@ -273,7 +273,7 @@ morning = TimeValue("09:00:00") noon = TimeValue("12:00:00") # Filter by specific times -time_filter = g.chain([ +time_filter = g.gfql([ n(filter_dict={"daily_event": is_in([morning, noon])}) ]) ``` @@ -300,12 +300,12 @@ from datetime import datetime, timedelta # Find events within last 7 days now = datetime.now() week_ago = now - timedelta(days=7) -recent_events = g.chain([ +recent_events = g.gfql([ n(filter_dict={"timestamp": gt(pd.Timestamp(week_ago))}) ]) # For recurring intervals, use multiple conditions -business_days = g.chain([ +business_days = g.gfql([ n(filter_dict={ "timestamp": between( pd.Timestamp("2023-01-01"), @@ -324,7 +324,7 @@ from datetime import datetime, timedelta # Get data from last 30 days thirty_days_ago = datetime.now() - timedelta(days=30) -recent_data = g.chain([ +recent_data = g.gfql([ n(filter_dict={"timestamp": gt(pd.Timestamp(thirty_days_ago))}) ]) ``` @@ -333,7 +333,7 @@ recent_data = g.chain([ ```python # Filter events during business hours -business_hours = g.chain([ +business_hours = g.gfql([ n(filter_dict={ "timestamp": between(time(9, 0, 0), time(17, 0, 0)) }) @@ -344,7 +344,7 @@ business_hours = g.chain([ ```python # Q1 2023 data -q1_2023 = g.chain([ +q1_2023 = g.gfql([ n(filter_dict={ "date": between( date(2023, 1, 1), diff --git a/docs/source/gfql/overview.rst b/docs/source/gfql/overview.rst index bbedaf9414..fe92118519 100644 --- a/docs/source/gfql/overview.rst +++ b/docs/source/gfql/overview.rst @@ -64,7 +64,7 @@ Example: Find all nodes where the `type` is `"person"`. 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)) **Visualize 2-Hop Edge Sequences with an Attribute** @@ -75,7 +75,7 @@ Example: Find 2-hop paths where edges have `"interesting": True`. from graphistry import n, e_forward - g_2_hops = g.chain([n(), e_forward({"interesting": True}, hops=2) ]) + g_2_hops = g.gfql([n(), e_forward({"interesting": True}, hops=2) ]) g_2_hops.plot() **Find Nodes 1-2 Hops Away and Label Each Hop** @@ -86,7 +86,7 @@ Example: Find nodes up to 2 hops away from node `"a"` and label each hop. from graphistry import n, e_undirected - g_2_hops = g.chain([ + g_2_hops = g.gfql([ n({g._node: "a"}), e_undirected(name="hop1"), e_undirected(name="hop2") @@ -106,12 +106,12 @@ Example: Find recent transactions using temporal predicates. import pandas as pd # Find transactions after a specific date - recent = g.chain([ + recent = g.gfql([ n(edge_match={"timestamp": gt(pd.Timestamp("2023-01-01"))}) ]) # Find transactions in a date range during business hours - business_hours_txns = g.chain([ + business_hours_txns = g.gfql([ n(edge_match={ "date": between(date(2023, 6, 1), date(2023, 6, 30)), "time": between(time(9, 0), time(17, 0)) @@ -126,7 +126,7 @@ Example: Find transaction nodes between two kinds of risky 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"), @@ -144,7 +144,7 @@ Example: Filter nodes and edges by multiple types. 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"), @@ -176,7 +176,7 @@ Example: Run GFQL queries with GPU dataframes. g_gpu = graphistry.edges(e_gdf, 'src', 'dst').nodes(n_gdf, 'id') # Run GFQL query (executes on GPU) - g_result = g_gpu.chain([ ... ]) # Your GFQL query here + g_result = g_gpu.gfql([ ... ]) # Your GFQL query here print('Number of resulting edges:', len(g_result._edges)) **Forcing GPU Mode** @@ -185,7 +185,7 @@ Example: Explicitly set the engine to ensure GPU execution. .. code-block:: python - g_result = g_gpu.chain([ ... ], engine='cudf') + g_result = g_gpu.gfql([ ... ], engine='cudf') Run Remotely ~~~~~~~~~~~~~ @@ -203,7 +203,7 @@ Example: Bind to remote data and run queries on remote GPU resources. g = graphistry.bind(dataset_id='my-dataset-id') - nodes_df = g.chain_remote([ n() ])._nodes + nodes_df = g.gfql_remote([ n() ])._nodes **Upload Data and Run GPU Python Remotely** @@ -248,7 +248,7 @@ Example: Visualize high PageRank nodes. g_enriched = g_result.compute_cugraph('pagerank') # Filter nodes with high PageRank - g_high_pagerank = g_enriched.chain([ + g_high_pagerank = g_enriched.gfql([ n(query='pagerank > 0.1'), e(), n(query='pagerank > 0.1') ]) diff --git a/docs/source/gfql/quick.rst b/docs/source/gfql/quick.rst index 3800518bc0..915934be59 100644 --- a/docs/source/gfql/quick.rst +++ b/docs/source/gfql/quick.rst @@ -12,9 +12,9 @@ Basic Usage .. code-block:: python - g.chain(ops=[...], engine=EngineAbstract.AUTO) + g.gfql(ops=[...], engine=EngineAbstract.AUTO) -:meth:`chain ` sequences multiple matchers for more complex patterns of paths and subgraphs +:meth:`gfql ` sequences multiple matchers for more complex patterns of paths and subgraphs - **ops**: Sequence of graph node and edge matchers (:class:`ASTObject ` instances). - **engine**: Optional execution engine. Engine is typically not set, defaulting to `'auto'`. Use `'cudf'` for GPU acceleration and `'pandas'` for CPU. @@ -153,7 +153,7 @@ Combined Examples .. code-block:: python - g.chain([ + g.gfql([ n({"type": "person"}), e_forward({"status": "active"}), n({"type": "transaction"}) @@ -163,7 +163,7 @@ Combined Examples .. code-block:: python - g.chain([ + g.gfql([ n({"id": "start_node"}, name="start"), e_forward(name="edge1"), n({"level": 2}, name="middle"), @@ -175,7 +175,7 @@ Combined Examples .. code-block:: python - g.chain([ + g.gfql([ n({"status": "infected"}), e_forward(to_fixed_point=True), n(name="reachable") @@ -185,7 +185,7 @@ Combined Examples .. code-block:: python - g.chain([ + g.gfql([ n({"type": is_in(["server", "database"])}), e_undirected({"protocol": "TCP"}, hops=3), n(query="risk_level >= 8") @@ -195,7 +195,7 @@ Combined Examples .. code-block:: python - g.chain([ + g.gfql([ n(query="age > 30 and country == 'USA'"), e_forward(edge_query="weight > 5"), n(query="status == 'active'") @@ -208,7 +208,7 @@ GPU Acceleration .. code-block:: python - g.chain([...], engine='cudf') + g.gfql([...], engine='cudf') - **Example with cuDF DataFrames:** @@ -220,7 +220,7 @@ GPU Acceleration n_gdf = cudf.from_pandas(node_df) g = graphistry.nodes(n_gdf, 'node_id').edges(e_gdf, 'src', 'dst') - g.chain([...], engine='cudf') + g.gfql([...], engine='cudf') Remote Mode ----------- @@ -231,7 +231,7 @@ Remote Mode g = graphistry.bind(dataset_id='ds-abc-123') - nodes_df = g.chain_remote([n()])._nodes + nodes_df = g.gfql_remote([n()])._nodes - **Upload graph and run GFQL** @@ -239,28 +239,28 @@ Remote Mode g2 = g1.upload() - g3 = g2.chain_remote([n(), e(), n()]) + g3 = g2.gfql_remote([n(), e(), n()]) - **Enforce CPU and GPU mode on remote GFQL** .. code-block:: python - g3a = g2.chain_remote([n(), e(), n()], engine='pandas') - g3b = g2.chain_remote([n(), e(), n()], engine='cudf') + g3a = g2.gfql_remote([n(), e(), n()], engine='pandas') + g3b = g2.gfql_remote([n(), e(), n()], engine='cudf') - **Return only nodes and certain columns** .. code-block:: python cols = ['id', 'name'] - g2b = g1.chain_remote([n(), e(), n()], output_type="edges", edge_col_subset=cols) + g2b = g1.gfql_remote([n(), e(), n()], output_type="edges", edge_col_subset=cols) - **Return only edges and certain columns** .. code-block:: python cols = ['src', 'dst'] - g2b = g1.chain_remote([n(), e(), n()], output_type="edges", edge_col_subset=cols) + g2b = g1.gfql_remote([n(), e(), n()], output_type="edges", edge_col_subset=cols) - **Return only shape metadata** @@ -397,7 +397,7 @@ Examples at a Glance .. code-block:: python - g.chain([ + g.gfql([ n({g._node: "Alice"}), e_undirected(hops=3), n({g._node: "Bob"}) @@ -419,7 +419,7 @@ Examples at a Glance .. code-block:: python - g.chain([ + g.gfql([ n({"community": "A"}), e_undirected(hops=2), n({"community": "B"}, name="bridge_nodes") @@ -429,7 +429,7 @@ Examples at a Glance .. code-block:: python - g.chain([ + g.gfql([ n(query="age >= 18"), e_forward(edge_query="interaction == 'message'"), n(query="location == 'NYC'") diff --git a/docs/source/gfql/remote.rst b/docs/source/gfql/remote.rst index 868158feeb..4983608614 100644 --- a/docs/source/gfql/remote.rst +++ b/docs/source/gfql/remote.rst @@ -14,7 +14,7 @@ Run chain remotely and fetch results .. code-block:: python from graphistry import n, e - g2 = g1.chain_remote([n(), e(), n()]) + g2 = g1.gfql_remote([n(), e(), n()]) assert len(g2._nodes) <= len(g1._nodes) Method :meth:`chain_remote ` runs chain remotely and fetched the computed graph @@ -40,7 +40,7 @@ Run on GPU remotely and fetch results .. code-block:: python from graphistry import n, e - g2 = g1.chain_remote([n(), e(), n()], engine='cudf') + g2 = g1.gfql_remote([n(), e(), n()], engine='cudf') assert len(g2._nodes) <= len(g1._nodes) CPU @@ -51,7 +51,7 @@ Run on CPU remotely and fetch results .. code-block:: python from graphistry import n, e - g2 = g1.chain_remote([n(), e(), n()], engine='pandas') + g2 = g1.gfql_remote([n(), e(), n()], engine='pandas') @@ -68,8 +68,8 @@ Explicit uploads via :meth:`upload ` g2 = g1.upload() assert g2._dataset_id is not None, "Uploading sets `dataset_id` for subsequent calls" - g3a = g2.chain_remote([n()]) - g3b = g2.chain_remote([n(), e(), n()]) + g3a = g2.gfql_remote([n()]) + g3b = g2.gfql_remote([n(), e(), n()]) assert len(g3a._nodes) >= len(g3b._nodes) @@ -86,7 +86,7 @@ If data is already uploaded and your user has access to it, such as from a previ g1 = graphistry.bind(dataset_id='abc123') assert g1._nodes is None, "Binding does not fetch data" - connected_graph_g = g1.chain_remote([n(), e()]) + connected_graph_g = g1.gfql_remote([n(), e()]) connected_nodes_df = connected_graph_g._nodes print(connected_nodes_df.shape) @@ -102,7 +102,7 @@ Return only nodes .. code-block:: python - g1.chain_remote([n(), e(), n()], output_type="nodes") + g1.gfql_remote([n(), e(), n()], output_type="nodes") Return only nodes and specific columns ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -110,7 +110,7 @@ Return only nodes and specific columns .. code-block:: python cols = [g1._node, 'time'] - g2b = g1.chain_remote( + g2b = g1.gfql_remote( [n(), e(), n()], output_type="nodes", node_col_subset=cols) @@ -122,7 +122,7 @@ Return only edges .. code-block:: python - g2a = g1.chain_remote([n(), e(), n()], output_type="edges") + g2a = g1.gfql_remote([n(), e(), n()], output_type="edges") Return only edges and specific columns ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -130,7 +130,7 @@ Return only edges and specific columns .. code-block:: python cols = [g1._source, g1._destination, 'time'] - g2b = g1.chain_remote([n(), e(), n()], + g2b = g1.gfql_remote([n(), e(), n()], output_type="edges", edge_col_subset=cols) assert len(g2b._edges.columns) == len(cols) diff --git a/docs/source/gfql/spec/cypher_mapping.md b/docs/source/gfql/spec/cypher_mapping.md index f936358348..80e873537f 100644 --- a/docs/source/gfql/spec/cypher_mapping.md +++ b/docs/source/gfql/spec/cypher_mapping.md @@ -2,41 +2,39 @@ # Cypher to GFQL Python & Wire Protocol Mapping -## Introduction +Translate existing Cypher workloads to GPU-accelerated GFQL with minimal code changes. -This specification shows how to translate Cypher queries to both GFQL Python code and Wire Protocol JSON, enabling: -- Migration from Cypher-based systems -- Two-stage LLM synthesis: Text → Cypher → GFQL -- Language-agnostic API integration -- Secure query generation without code execution +## Introduction -## Conceptual Framework +This specification shows how to translate Cypher queries to both GFQL Python code and :ref:`Wire Protocol ` JSON, enabling migration from Cypher-based systems, LLM pipelines (text → Cypher → GFQL), language-agnostic API integration, and secure query generation without code execution. -### Translation Scenarios +## What Maps 1-to-1 When translating from Cypher, you'll encounter three scenarios: -**1. Direct Translation** - Most pattern matching maps cleanly to pure GFQL -**2. Hybrid Approach** - Post-processing operations (RETURN clauses) use dataframes +**1. Direct Translation** - Most pattern matching maps cleanly to pure GFQL +**2. Hybrid Approach** - Post-processing operations (RETURN clauses with aggregations) use df.groupby/agg **3. GFQL Advantages** - Some capabilities go beyond what Cypher offers -### What Translates Directly +### Direct Translations - Graph patterns: `(a)-[r]->(b)` → chain operations - Property filters: WHERE clauses embed into operations - Path traversals: Variable-length paths use `hops` parameter - Pattern composition: Multiple patterns become sequential operations -### What Requires DataFrames +## When You Need DataFrames - Aggregations: COUNT, SUM, AVG → pandas operations - Projections: RETURN specific columns → DataFrame selection - Sorting/limiting: ORDER BY, LIMIT → DataFrame methods - Joins: Multiple disconnected patterns → pandas merge -### GFQL Advantages Beyond Cypher -- **Rich edge properties**: Query edges as first-class entities +## GFQL-Only Super-Powers +- **Edge properties**: Query edges as first-class entities - **Dataframe-native**: Zero-cost transitions between graph and tabular operations -- **GPU acceleration**: Massively parallel execution on NVIDIA hardware +- **GPU acceleration**: Parallel execution on NVIDIA hardware - **Heterogeneous graphs**: No schema constraints on types or properties +- **Integrated visualization**: Layouts like `group_in_a_box_layout` for community visualization +- **Algorithm chaining**: Combine community detection with layout algorithms ## Quick Example @@ -48,7 +46,7 @@ WHERE p.age > 30 **Python:** ```python -g.chain([ +g.gfql([ n({"type": "Person", "age": gt(30)}, name="p"), e_forward({"type": "FOLLOWS"}, name="r"), n({"type": "Person"}, name="q") @@ -64,7 +62,7 @@ g.chain([ ]} ``` -## Pattern Translations +## Translation Tables ### Node Patterns @@ -84,7 +82,7 @@ g.chain([ | `<-[r]-` | `e_reverse(name="r")` | `{"type": "Edge", "direction": "reverse", "name": "r"}` | | `-[r]-` | `e(name="r")` | `{"type": "Edge", "direction": "undirected", "name": "r"}` | | `-[*2]->` | `e_forward(hops=2)` | `{"type": "Edge", "direction": "forward", "hops": 2}` | -| `-[*1..3]->` | `e_forward(hops=3)` | `{"type": "Edge", "direction": "forward", "hops": 3}` | +| `-[*1..3]->` | `e_forward(hops=3)` | `{"type": "Edge", "direction": "forward", "hops": 3}` | # upper-bound only; lower bound = 1 | | `-[*]->` | `e_forward(to_fixed_point=True)` | `{"type": "Edge", "direction": "forward", "to_fixed_point": true}` | | `-[r:BOUGHT {amount: gt(100)}]->` | `e_forward({"type": "BOUGHT", "amount": gt(100)}, name="r")` | `{"type": "Edge", "direction": "forward", "edge_match": {"type": "BOUGHT", "amount": {"type": "GT", "val": 100}}, "name": "r"}` | @@ -92,11 +90,11 @@ g.chain([ | Cypher | Python | Wire Protocol | |--------|--------|---------------| +| `n.status = 'active'` | `"active"` | `"active"` | # literal | `n.age > 30` | `gt(30)` | `{"type": "GT", "val": 30}` | | `n.age >= 50` | `ge(50)` | `{"type": "GE", "val": 50}` | | `n.age < 100` | `lt(100)` | `{"type": "LT", "val": 100}` | | `n.age <= 50` | `le(50)` | `{"type": "LE", "val": 50}` | -| `n.status = 'active'` | `"active"` | `"active"` | | `n.status <> 'deleted'` | `ne("deleted")` | `{"type": "NE", "val": "deleted"}` | | `n.id IN [1,2,3]` | `is_in([1,2,3])` | `{"type": "IsIn", "options": [1,2,3]}` | | `n.score BETWEEN 0 AND 100` | `between(0, 100)` | `{"type": "Between", "lower": 0, "upper": 100}` | @@ -119,7 +117,7 @@ WHERE fof.active = true **Python:** ```python -g.chain([ +g.gfql([ n({"type": "User", "name": "Alice"}), e_forward({"type": "FRIEND"}, hops=2), n({"type": "User", "active": True}, name="fof") @@ -145,7 +143,7 @@ WHERE t.amount > 10000 AND t.date > date('2024-01-01') **Python:** ```python -g.chain([ +g.gfql([ n({"type": "Account"}), e_forward({ "type": "TRANSFER", @@ -183,7 +181,7 @@ LIMIT 10 **Python:** ```python # Step 1: Graph pattern -result = g.chain([ +result = g.gfql([ n({"type": "User"}), e_forward({"type": "TRANSACTION", "date": gt(date(2024,1,1))}, name="trans"), n({"type": "Merchant"}) diff --git a/docs/source/gfql/spec/language.md b/docs/source/gfql/spec/language.md index 65fae87b4b..d506b887ea 100644 --- a/docs/source/gfql/spec/language.md +++ b/docs/source/gfql/spec/language.md @@ -330,7 +330,7 @@ GFQL follows a declarative execution model similar to Neo4j's Cypher: Query execution returns filtered node and edge datasets. In the Python embedding: ```python -result = g.chain([...]) +result = g.gfql([...]) nodes_df = result._nodes # Filtered nodes edges_df = result._edges # Filtered edges ``` @@ -340,7 +340,7 @@ edges_df = result._edges # Filtered edges Operations with `name` parameter add boolean columns to mark matched entities: ```python -result = g.chain([ +result = g.gfql([ n({"type": "person"}, name="people"), e_forward(name="connections"), n({"active": True}, name="active_targets") diff --git a/docs/source/gfql/spec/python_embedding.md b/docs/source/gfql/spec/python_embedding.md index cf20db1741..1b455d98c5 100644 --- a/docs/source/gfql/spec/python_embedding.md +++ b/docs/source/gfql/spec/python_embedding.md @@ -42,7 +42,7 @@ Graph edges can be accessed similarly: from graphistry import n, e_forward # Execute a chain -result = g.chain([ +result = g.gfql([ n({"type": "person"}), e_forward(), n() @@ -63,9 +63,9 @@ GFQL supports multiple execution engines: ```python # Force specific engine -g.chain([...], engine='cudf') # GPU execution -g.chain([...], engine='pandas') # CPU execution -g.chain([...], engine='auto') # Auto-select +g.gfql([...], engine='cudf') # GPU execution +g.gfql([...], engine='pandas') # CPU execution +g.gfql([...], engine='auto') # Auto-select ``` ## Python-Specific Values @@ -123,7 +123,7 @@ chain = Chain([ You have two options for validating queries against your data schema: 1. **Validate-only** (no execution): Use `validate_chain_schema()` to check compatibility without running the query -2. **Validate-and-run**: Use `g.chain(..., validate_schema=True)` to validate before execution +2. **Validate-and-run**: Use `g.gfql(..., validate_schema=True)` to validate before execution ```python # Method 1: Validate-only (no execution) @@ -139,7 +139,7 @@ except GFQLSchemaError as e: # Method 2: Runtime validation (automatic) try: - result = g.chain([ + result = g.gfql([ n({'missing_column': 'value'}) ]) # Validates during execution, raises GFQLSchemaError except GFQLSchemaError as e: @@ -147,7 +147,7 @@ except GFQLSchemaError as e: # Method 3: Validate-and-run (pre-execution validation) try: - result = g.chain([ + result = g.gfql([ n({'missing_column': 'value'}) ], validate_schema=True) # Validates first, only executes if valid except GFQLSchemaError as e: @@ -200,7 +200,7 @@ errors = validate_chain_schema(g, chain, collect_all=True) from graphistry.compute.exceptions import GFQLValidationError, GFQLSchemaError try: - result = g.chain([ + result = g.gfql([ n({'age': 'twenty-five'}) # Type mismatch ]) except GFQLSchemaError as e: @@ -238,27 +238,27 @@ n({"created": gt(pd.Timestamp("2024-01-01"))}) print(g._nodes.columns) # ['id', 'type', 'name'] # Wrong - Column doesn't exist -g.chain([n({"username": "Alice"})]) # KeyError +g.gfql([n({"username": "Alice"})]) # KeyError # Correct - Use existing column -g.chain([n({"name": "Alice"})]) +g.gfql([n({"name": "Alice"})]) ``` ### Unsupported Operations ```python # Wrong - Can't aggregate in chain -# g.chain([n(), e(), count()]) +# g.gfql([n(), e(), count()]) # Correct - Aggregate after chain -result = g.chain([n(), e()]) +result = g.gfql([n(), e()]) count = len(result._edges) # Wrong - OPTIONAL MATCH not supported # No direct GFQL equivalent # Correct - Handle optionality in post-processing -result = g.chain([n(), e_forward()]) +result = g.gfql([n(), e_forward()]) # Check for nodes without edges nodes_with_edges = result._nodes[result._nodes[g._node].isin(result._edges[g._source])] ``` @@ -271,16 +271,16 @@ nodes_with_edges = result._nodes[result._nodes[g._node].isin(result._edges[g._so node_filters = {"type": "User"} if min_age: node_filters["age"] = gt(min_age) -g.chain([n(node_filters)]) +g.gfql([n(node_filters)]) # Avoid: Hardcoded query strings -g.chain([n(query=f"type == 'User' and age > {min_age}")]) # SQL injection risk +g.gfql([n(query=f"type == 'User' and age > {min_age}")]) # SQL injection risk ``` ### Memory Efficiency ```python # Good: Filter early and use named results -result = g.chain([ +result = g.gfql([ n({"active": True}, name="active_users"), # Filter first e_forward({"recent": True}) ]) diff --git a/docs/source/gfql/spec/wire_protocol.md b/docs/source/gfql/spec/wire_protocol.md index beacfdf20f..27456de5ea 100644 --- a/docs/source/gfql/spec/wire_protocol.md +++ b/docs/source/gfql/spec/wire_protocol.md @@ -241,7 +241,7 @@ null // null **Python**: ```python -g.chain([ +g.gfql([ n({"customer_id": "C123"}), e_forward({ "type": "purchase", @@ -284,7 +284,7 @@ g.chain([ **Python**: ```python -g.chain([ +g.gfql([ n({"ip": is_in(["192.168.1.100", "192.168.1.101"])}), e_forward( edge_query="port IN [22, 23, 3389]", diff --git a/docs/source/gfql/translate.rst b/docs/source/gfql/translate.rst index 86bedd4b16..9baf34bce4 100644 --- a/docs/source/gfql/translate.rst +++ b/docs/source/gfql/translate.rst @@ -10,6 +10,8 @@ Introduction GFQL (GraphFrame Query Language) is designed to be intuitive for users familiar with SQL, Cypher, or dataframe like Pandas and Spark. By comparing equivalent queries across these languages, you can quickly grasp GFQL's syntax, benefits, and start utilizing its powerful graph querying capabilities within your workflows. +GFQL operates on graph DataFrames - graphs represented as node and edge DataFrames. This DataFrame-native approach enables seamless integration with the PyData ecosystem and natural vectorization for both CPU and GPU processing. + Who Is This Guide For? ---------------------- @@ -59,11 +61,11 @@ Finding Nodes with Specific Properties from graphistry import n # df[['id', 'type', ...]] - g.chain([ n({"type": "person"}) ])._nodes + g.gfql([ n({"type": "person"}) ])._nodes **Explanation**: -- **GFQL**: `n({"type": "person"})` filters nodes where `type` is `"person"`. `g.chain([...])` applies this filter to the graph `g`, and `._nodes` retrieves the resulting nodes. The performance is similar to that of Pandas (CPU) or cuDF (GPU). +- **GFQL**: `n({"type": "person"})` filters nodes where `type` is `"person"`. `g.gfql([...])` applies this filter to the graph `g`, and `._nodes` retrieves the resulting nodes. The performance is similar to that of Pandas (CPU) or cuDF (GPU). --- @@ -165,7 +167,7 @@ Performing Multi-Hop Traversals from graphistry import n, e_forward # df[['id', ...]] - g.chain([ + g.gfql([ n({g._node: "Alice"}), e_forward(), e_forward(), n(name='m') ])._nodes.query('m') @@ -208,7 +210,7 @@ Filtering Edges and Nodes with Conditions from graphistry import e_forward # df[['src', 'dst', 'weight', ...]] - g.chain([ e_forward(edge_query='weight > 0.5') ])._edges + g.gfql([ e_forward(edge_query='weight > 0.5') ])._edges **Explanation**: @@ -349,7 +351,7 @@ All Paths and Connectivity # g._edges: df[['src', 'dst', ...]] # g._nodes: df[['id', ...]] - g.chain([ + g.gfql([ n({"id": "Alice"}), e_forward( source_node_query='type == "person"', @@ -437,7 +439,7 @@ Time-Windowed Graph Analytics .. code-block:: python past_week = pd.Timestamp.now() - pd.Timedelta(7) - g.chain([ + g.gfql([ n({"id": {"$in": ["Alice", "Bob"]}}), e_forward(edge_query=f'timestamp >= "{past_week}"'), n({"id": {"$in": ["Alice", "Bob"]}}) @@ -488,7 +490,7 @@ Parallel Pathfinding from graphistry import n, e_forward # g._nodes: cudf.DataFrame[['src', 'dst', ...]] - g.chain([ + g.gfql([ n({"id": "Alice"}), e_forward(to_fixed_point=False), n({"id": is_in(["Bob", "Charlie"])}) @@ -527,7 +529,7 @@ GPU Execution from graphistry import n, e_forward # Executing pathfinding queries in parallel - g.chain([ + g.gfql([ n({"id": "Alice"}), e_forward(to_fixed_point=False), n({"id": is_in(["Bob", "Charlie"])}) diff --git a/docs/source/gfql/wire_protocol_examples.md b/docs/source/gfql/wire_protocol_examples.md index 40f507b097..3e1903fe54 100644 --- a/docs/source/gfql/wire_protocol_examples.md +++ b/docs/source/gfql/wire_protocol_examples.md @@ -259,7 +259,7 @@ from graphistry.compute import gt, eq, between from datetime import datetime, timedelta # Multi-hop query with temporal filters -chain = g.chain([ +chain = g.gfql([ # Recent transactions n(edge_match={ "timestamp": gt(datetime.now() - timedelta(days=7)), @@ -429,7 +429,7 @@ from graphistry.compute.ast import Chain reconstructed_query = Chain.from_json(received_data) # 5. Apply to graph data -result = g.chain(reconstructed_query.queries) +result = g.gfql(reconstructed_query.queries) ``` ## Wire Protocol Structure