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..5975084 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: @@ -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?" 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)