From 74c6756e1476d5d7920a7e1974454216b1784b83 Mon Sep 17 00:00:00 2001 From: prrao87 Date: Fri, 30 Oct 2026 09:53:20 -0400 Subject: [PATCH 1/2] refactor(config)!: remove deprecated with_node_label and with_relationship Both builder methods have been deprecated since 0.2.0 in favour of node_table and rel_table. Remove them from the Rust GraphConfigBuilder (with with_relationship_endpoints, which only completed with_relationship), the Python bindings and the Python facade, along with the tests that exercised them. The docs now list them as removed in 0.5.0, with their replacements. Relationships without endpoint labels can still be built in Rust with with_relationship_mapping; the Rust alternation test now does that. --- crates/lance-csr-python/src/graph.rs | 43 ------------ crates/lance-csr/src/config.rs | 68 ------------------- crates/lance-csr/src/pattern_alternatives.rs | 20 ++++-- docs/getting-started.md | 4 +- docs/graph-schema.md | 10 +-- python/README.md | 4 +- python/python/lance_csr/_internal/graph.pyi | 9 --- python/python/lance_csr/config.py | 30 -------- python/python/tests/test_config_builder.py | 18 +---- .../python/tests/test_engine_alternation.py | 25 +------ python/python/tests/test_engine_paths.py | 27 +------- python/python/tests/test_wrappers.py | 6 -- python/python/tests/typing/public_api.py | 2 - 13 files changed, 27 insertions(+), 239 deletions(-) diff --git a/crates/lance-csr-python/src/graph.rs b/crates/lance-csr-python/src/graph.rs index 4c9eb55..70fe26b 100644 --- a/crates/lance-csr-python/src/graph.rs +++ b/crates/lance-csr-python/src/graph.rs @@ -249,13 +249,6 @@ pub struct GraphConfigBuilder { inner: lance_csr::config::GraphConfigBuilder, } -/// Raise a ``DeprecationWarning`` pointing at the caller's line. -fn deprecated(py: Python, message: &std::ffi::CStr) -> PyResult<()> { - let category = py.get_type::(); - // Level 2 skips the Python wrapper method and points at its caller. - PyErr::warn(py, &category, message, 2) -} - #[pymethods] impl GraphConfigBuilder { #[new] @@ -287,42 +280,6 @@ impl GraphConfigBuilder { } } - fn with_node_label(&self, py: Python, label: &str, id_field: &str) -> PyResult { - deprecated(py, c"with_node_label() is deprecated; use node_table()")?; - Ok(self.node_table(label, id_field)) - } - - #[pyo3(signature = (rel_type, source_field, target_field, source_label=None, target_label=None))] - fn with_relationship( - &self, - py: Python, - rel_type: &str, - source_field: &str, - target_field: &str, - source_label: Option<&str>, - target_label: Option<&str>, - ) -> PyResult { - deprecated( - py, - c"with_relationship() is deprecated; use rel_table(), which also takes the node tables at both ends", - )?; - #[allow(deprecated)] - let inner = self - .inner - .clone() - .with_relationship(rel_type, source_field, target_field); - #[allow(deprecated)] - let inner = match (source_label, target_label) { - (Some(src), Some(dst)) => inner.with_relationship_endpoints(rel_type, src, dst), - (None, None) => inner, - _ => { - return Err(PyValueError::new_err( - "source_label and target_label must be given together", - )) - } - }; - Ok(Self { inner }) - } fn build(&self) -> PyResult { let config = self.inner.clone().build().map_err(graph_error_to_pyerr)?; diff --git a/crates/lance-csr/src/config.rs b/crates/lance-csr/src/config.rs index b28a63f..aa96599 100644 --- a/crates/lance-csr/src/config.rs +++ b/crates/lance-csr/src/config.rs @@ -249,12 +249,6 @@ impl GraphConfigBuilder { self } - /// Add a node label mapping. - #[deprecated(since = "0.2.0", note = "use `node_table`")] - pub fn with_node_label>(self, label: S, id_field: S) -> Self { - self.node_table(label, id_field) - } - /// Add a node mapping with additional configuration pub fn with_node_mapping(mut self, mapping: NodeMapping) -> Self { let normalized_key = mapping.label.to_lowercase(); @@ -262,54 +256,6 @@ impl GraphConfigBuilder { self } - /// Add a relationship type mapping, without endpoint node tables. - #[deprecated( - since = "0.2.0", - note = "use `rel_table`, which also takes the node tables at both ends" - )] - pub fn with_relationship>( - mut self, - rel_type: S, - source_field: S, - target_field: S, - ) -> Self { - let type_str = rel_type.into(); - let normalized_key = type_str.to_lowercase(); - self.relationship_mappings.insert( - normalized_key, - RelationshipMapping { - relationship_type: type_str, // Keep original case for display - source_id_field: source_field.into(), - target_id_field: target_field.into(), - type_field: None, - property_fields: Vec::new(), - filter_conditions: None, - source_label: None, - target_label: None, - }, - ); - self - } - - /// Record the node labels a configured relationship type connects (see - /// [`RelationshipMapping::source_label`]). No-op for an unknown type. - #[deprecated( - since = "0.2.0", - note = "use `rel_table`, which takes the endpoint node tables" - )] - pub fn with_relationship_endpoints>( - mut self, - rel_type: &str, - source_label: S, - target_label: S, - ) -> Self { - if let Some(mapping) = self.relationship_mappings.get_mut(&rel_type.to_lowercase()) { - mapping.source_label = Some(source_label.into()); - mapping.target_label = Some(target_label.into()); - } - self - } - /// Add a relationship mapping with additional configuration pub fn with_relationship_mapping(mut self, mapping: RelationshipMapping) -> Self { let normalized_key = mapping.relationship_type.to_lowercase(); @@ -479,20 +425,6 @@ mod tests { assert!(message.contains("Company"), "{message}"); } - #[test] - #[allow(deprecated)] - fn deprecated_builder_methods_still_configure_the_graph() { - let config = GraphConfig::builder() - .with_node_label("Person", "id") - .with_relationship("KNOWS", "src", "dst") - .with_relationship_endpoints("KNOWS", "Person", "Person") - .build() - .unwrap(); - let knows = config.get_relationship_mapping("knows").unwrap(); - assert_eq!(config.get_node_mapping("person").unwrap().id_field, "id"); - assert_eq!(knows.source_label.as_deref(), Some("Person")); - } - #[test] fn test_validation_empty_id_field() { let mut config = GraphConfig::default(); diff --git a/crates/lance-csr/src/pattern_alternatives.rs b/crates/lance-csr/src/pattern_alternatives.rs index 64b37df..4af644a 100644 --- a/crates/lance-csr/src/pattern_alternatives.rs +++ b/crates/lance-csr/src/pattern_alternatives.rs @@ -594,6 +594,7 @@ fn unsupported(feature: &str) -> GraphError { mod tests { use super::*; use crate::ast::ReadingClause; + use crate::config::RelationshipMapping; use crate::parser::parse_cypher_query; fn ldbc_config() -> GraphConfig { @@ -609,12 +610,19 @@ mod tests { ] { b = b.rel_table(t, s, d, "src", "dst"); } - // Without endpoint labels, which only the deprecated builder can still configure. - #[allow(deprecated)] - { - b = b.with_relationship("knows", "src", "dst"); - } - b.build().unwrap() + // Without endpoint labels, which only a hand-built mapping can leave out. + b.with_relationship_mapping(RelationshipMapping { + relationship_type: "knows".into(), + source_id_field: "src".into(), + target_id_field: "dst".into(), + type_field: None, + property_fields: Vec::new(), + filter_conditions: None, + source_label: None, + target_label: None, + }) + .build() + .unwrap() } fn branches(query: &str) -> Result> { diff --git a/docs/getting-started.md b/docs/getting-started.md index c94720d..981350d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -38,8 +38,8 @@ The package depends on `pyarrow>=14` and `pylance` 13.x, which uv installs for y !!! note "Docs track the `main` branch" These pages describe the code on `main`. The `node_table` and `rel_table` builder methods arrived in - 0.2.0; on 0.1.0 use `with_node_label` and `with_relationship` (see - [Graph schema](graph-schema.md#deprecated-builder-methods)). + 0.2.0; on 0.1.0 use `with_node_label` and `with_relationship`, which were removed in 0.5.0 (see + [Graph schema](graph-schema.md#removed-builder-methods)). Check the install: diff --git a/docs/graph-schema.md b/docs/graph-schema.md index c6ba2a9..68bfc1b 100644 --- a/docs/graph-schema.md +++ b/docs/graph-schema.md @@ -166,14 +166,14 @@ relationship endpoints (`int8` to `int64`, `uint8` to `uint32`), and its fused t integer** node keys. If your keys are strings or UUIDs, queries still work, using joins; consider adding a dense integer id column if traversal speed matters. -## Deprecated builder methods +## Removed builder methods -The 0.1 names still work but raise a `DeprecationWarning`, and will be removed in a later release: +The 0.1 names, deprecated since 0.2.0, were removed in 0.5.0. Replace them as follows: -| Deprecated | Replacement | +| Removed | Replacement | | --- | --- | | `with_node_label(label, id_field)` | `node_table(label, primary_key)` | | `with_relationship(rel_type, source_field, target_field, source_label=None, target_label=None)` | `rel_table(rel_type, from_table, to_table, src, dst)` | -`with_relationship` makes the endpoint labels optional; without them, label inference and alternation can't -pick the right tables. +`rel_table` needs the node tables at both ends, which `with_relationship` left optional. Add a `node_table` +for each end before you call `build()`. diff --git a/python/README.md b/python/README.md index 7a7ffc1..8db3c86 100644 --- a/python/README.md +++ b/python/README.md @@ -107,8 +107,8 @@ hold each end's key (`src` and `dst`). Knowing the ends lets queries leave nodes unlabeled where the relationship decides the label (`(p:Person)-[:lives_in]->(c)`), and lets relationship-type alternation (`[:likes_post|likes_comment]`) pick the right table. -`with_node_label` and `with_relationship`, the names in lance-csr 0.1, still work but are -deprecated and will be removed in a later release. +`with_node_label` and `with_relationship`, the names in lance-csr 0.1, were removed in +0.5.0; use `node_table` and `rel_table` instead. ### 3. Lance datasets: snapshots, refresh and streaming diff --git a/python/python/lance_csr/_internal/graph.pyi b/python/python/lance_csr/_internal/graph.pyi index 0c921a6..5244151 100644 --- a/python/python/lance_csr/_internal/graph.pyi +++ b/python/python/lance_csr/_internal/graph.pyi @@ -73,15 +73,6 @@ class GraphConfigBuilder: def rel_table( self, rel_type: str, from_table: str, to_table: str, src: str, dst: str ) -> Self: ... - def with_node_label(self, label: str, id_field: str) -> Self: ... - def with_relationship( - self, - rel_type: str, - source_field: str, - target_field: str, - source_label: str | None = None, - target_label: str | None = None, - ) -> Self: ... def build(self) -> GraphConfig: ... @final diff --git a/python/python/lance_csr/config.py b/python/python/lance_csr/config.py index 09e7712..1586139 100644 --- a/python/python/lance_csr/config.py +++ b/python/python/lance_csr/config.py @@ -137,33 +137,3 @@ def build(self) -> GraphConfig: label has no node table. """ return GraphConfig._from_inner(self._inner.build()) - - def with_node_label(self, label: str, id_field: str) -> Self: - """Deprecated: use [`node_table`][lance_csr.GraphConfigBuilder.node_table]. - - Emits `DeprecationWarning`. - """ - return self._from_inner(self._inner.with_node_label(label, id_field)) - - def with_relationship( - self, - rel_type: str, - source_field: str, - target_field: str, - source_label: str | None = None, - target_label: str | None = None, - ) -> Self: - """Deprecated: use [`rel_table`][lance_csr.GraphConfigBuilder.rel_table]. - - `rel_table` also takes the node tables at both ends. Emits `DeprecationWarning`. - - Raises - ------ - ValueError - If only one of `source_label` and `target_label` is given. - """ - return self._from_inner( - self._inner.with_relationship( - rel_type, source_field, target_field, source_label, target_label - ) - ) diff --git a/python/python/tests/test_config_builder.py b/python/python/tests/test_config_builder.py index 3ba984c..48148f4 100644 --- a/python/python/tests/test_config_builder.py +++ b/python/python/tests/test_config_builder.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright The lance-csr Authors -"""The graph config builder: node and relationship tables, and the deprecated names.""" +"""The graph config builder: node and relationship tables.""" import pyarrow as pa import pytest @@ -44,19 +44,3 @@ def test_rel_table_endpoints_must_be_node_tables(): ) with pytest.raises(Exception, match="Company"): builder.build() - - -def test_deprecated_names_warn_and_still_work(): - builder = GraphConfig.builder() - with pytest.deprecated_call(match="node_table"): - builder = builder.with_node_label("Person", "id").with_node_label( - "Company", "id" - ) - with pytest.deprecated_call(match="rel_table"): - builder = builder.with_relationship( - "WORKS_AT", "src", "dst", "Person", "Company" - ) - assert answer(builder.build()) == [ - {"p.name": "a", "c.name": "c"}, - {"p.name": "b", "c.name": "c"}, - ] diff --git a/python/python/tests/test_engine_alternation.py b/python/python/tests/test_engine_alternation.py index 09e632a..e85af9c 100644 --- a/python/python/tests/test_engine_alternation.py +++ b/python/python/tests/test_engine_alternation.py @@ -30,17 +30,12 @@ } -def config(endpoints=True): +def config(): builder = GraphConfig.builder() for label in ("Person", "Post", "Comment"): builder = builder.node_table(label, "id") for name, (src, dst, _, _) in RELS.items(): - if endpoints: - builder = builder.rel_table(name, src, dst, "src", "dst") - else: - # Only the deprecated method can still leave the endpoint tables out. - with pytest.deprecated_call(): - builder = builder.with_relationship(name, "src", "dst") + builder = builder.rel_table(name, src, dst, "src", "dst") return builder.build() @@ -101,22 +96,6 @@ def test_alternation_branches_use_csr(datasets): assert physical.count("CsrExpandExec") == 2 and "UnionExec" in physical -def test_label_choice_needs_endpoint_labels(datasets): - # Without endpoint labels, `m` could be a Post or a Comment for either type. - engine = CypherEngine(config(endpoints=False), datasets) - with pytest.raises(Exception, match="endpoint labels"): - engine.execute( - "MATCH (p:Person)<-[:postHasCreator|commentHasCreator]-(m:Post:Comment) " - "RETURN m.id" - ) - - -def test_endpoint_labels_must_be_given_together(): - builder = GraphConfig.builder().node_table("Post", "id") - with pytest.raises(ValueError, match="together"), pytest.deprecated_call(): - builder.with_relationship("r", "src", "dst", "Post") - - @pytest.mark.parametrize("use_csr", [False, True]) def test_variable_length_alternation_follows_endpoint_labels(datasets, use_csr): # Comment 21 replies to comment 20, which replies to post 10: one and two hops. diff --git a/python/python/tests/test_engine_paths.py b/python/python/tests/test_engine_paths.py index b548f91..bece713 100644 --- a/python/python/tests/test_engine_paths.py +++ b/python/python/tests/test_engine_paths.py @@ -7,11 +7,9 @@ (twice), 1->3, 2->3, 3->3 and 5->1; 4->99 points at a missing person. """ -import warnings - import pyarrow as pa import pytest -from lance_csr import CypherEngine, GraphConfig +from lance_csr import CypherEngine from test_engine_csr import CONFIG, KNOWS, PEOPLE CASES = [ @@ -168,26 +166,3 @@ def test_shortest_path_null_checks_keep_compact_plan( plan = engine.explain(query) assert "ShortestPathExec" in plan assert "PathMatchExec" not in plan - - -@pytest.mark.parametrize("use_csr", [False, True]) -def test_variable_length_target_inherits_source_label_without_endpoints(use_csr): - # The deprecated builder records no endpoint labels; an unlabeled - # variable-length target then takes the source's label, as before. - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - config = ( - GraphConfig.builder() - .with_node_label("Person", "id") - .with_relationship("KNOWS", "src", "dst") - .build() - ) - data = { - "Person": pa.table({"id": [1, 2, 3, 4], "name": ["A", "B", "C", "D"]}), - "KNOWS": pa.table({"src": [1, 2, 2], "dst": [2, 3, 4]}), - } - rows = CypherEngine(config, data, use_csr).execute( - "MATCH (a:Person)-[:KNOWS*1..2]->(b) WHERE a.id = 1 " - "RETURN b.name AS name ORDER BY name" - ) - assert rows.to_pylist() == [{"name": "B"}, {"name": "C"}, {"name": "D"}] diff --git a/python/python/tests/test_wrappers.py b/python/python/tests/test_wrappers.py index 9b1c1bf..2b2bd4d 100644 --- a/python/python/tests/test_wrappers.py +++ b/python/python/tests/test_wrappers.py @@ -84,9 +84,3 @@ def test_enums_reach_the_compiled_calls(): for metric in DistanceMetric: assert search.metric(metric).search(table)["id"].to_pylist() == [2] assert int(SqlDialect.Sqlite) == 4 - - -def test_deprecation_warning_points_at_the_caller(): - with pytest.warns(DeprecationWarning) as record: - GraphConfig.builder().with_node_label("Person", "id") - assert record[0].filename == __file__ diff --git a/python/python/tests/typing/public_api.py b/python/python/tests/typing/public_api.py index 27f2965..539614f 100644 --- a/python/python/tests/typing/public_api.py +++ b/python/python/tests/typing/public_api.py @@ -46,8 +46,6 @@ def public_api( assert_type(config.node_labels(), list[str]) assert_type(config.relationship_types(), list[str]) assert_type(GraphConfigBuilder().build(), GraphConfig) - assert_type(builder.with_node_label("City", "id"), GraphConfigBuilder) - assert_type(builder.with_relationship("R", "src", "dst"), GraphConfigBuilder) datasets = {"Person": table} engine = CypherEngine(config, datasets, use_csr=True) From eb43f529a15e2c4fb6f1131d762c4f10f4420e48 Mon Sep 17 00:00:00 2001 From: prrao87 Date: Fri, 30 Oct 2026 09:53:20 -0400 Subject: [PATCH 2/2] docs: add a Rust tab to the getting started CSR example Show the crate next to the Python example, reusing the compiled embedded_query example, and link to the Rust page for more. --- docs/getting-started.md | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 981350d..5975084 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -125,15 +125,31 @@ print(older.to_pydict()) # {'name': ['Bob', 'David']} Pass `use_csr=True` and the engine builds a CSR adjacency index for each relationship type. Multi-hop patterns then become array lookups instead of joins: -```python -engine = CypherEngine(config, datasets, use_csr=True) +=== "Python" -friends_of_friends = engine.execute(""" - MATCH (a:Person {name: 'Alice'})-[:KNOWS]->()-[:KNOWS]->(c:Person) - RETURN DISTINCT c.name AS friend_of_friend -""") -print(friends_of_friends.to_pylist()) # [{'friend_of_friend': 'David'}] -``` + ```python + engine = CypherEngine(config, datasets, use_csr=True) + + friends_of_friends = engine.execute(""" + MATCH (a:Person {name: 'Alice'})-[:KNOWS]->()-[:KNOWS]->(c:Person) + RETURN DISTINCT c.name AS friend_of_friend + """) + print(friends_of_friends.to_pylist()) # [{'friend_of_friend': 'David'}] + ``` + +=== "Rust" + + The same engine is a Rust crate (`cargo add lance-csr`). This example reads `Person` and `KNOWS` from + Lance datasets on disk, where Alice knows only Bob, so the answer differs from the Python tab. The third + argument of `CypherEngine::new` is `use_csr`. + + ```rust + --8<-- "crates/lance-csr/examples/embedded_query.rs:engine" + + --8<-- "crates/lance-csr/examples/embedded_query.rs:query" + ``` + + [Using from Rust](rust.md) has the full, runnable example, along with refresh, errors and timeouts. !!! tip "When should I turn the CSR on?"