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
43 changes: 0 additions & 43 deletions crates/lance-csr-python/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<pyo3::exceptions::PyDeprecationWarning>();
// Level 2 skips the Python wrapper method and points at its caller.
PyErr::warn(py, &category, message, 2)
}

#[pymethods]
impl GraphConfigBuilder {
#[new]
Expand Down Expand Up @@ -287,42 +280,6 @@ impl GraphConfigBuilder {
}
}

fn with_node_label(&self, py: Python, label: &str, id_field: &str) -> PyResult<Self> {
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<Self> {
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<GraphConfig> {
let config = self.inner.clone().build().map_err(graph_error_to_pyerr)?;
Expand Down
68 changes: 0 additions & 68 deletions crates/lance-csr/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,67 +249,13 @@ impl GraphConfigBuilder {
self
}

/// Add a node label mapping.
#[deprecated(since = "0.2.0", note = "use `node_table`")]
pub fn with_node_label<S: Into<String>>(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();
self.node_mappings.insert(normalized_key, mapping);
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<S: Into<String>>(
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<S: Into<String>>(
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();
Expand Down Expand Up @@ -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();
Expand Down
20 changes: 14 additions & 6 deletions crates/lance-csr/src/pattern_alternatives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Vec<String>> {
Expand Down
36 changes: 26 additions & 10 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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?"

Expand Down
10 changes: 5 additions & 5 deletions docs/graph-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
4 changes: 2 additions & 2 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 0 additions & 9 deletions python/python/lance_csr/_internal/graph.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 0 additions & 30 deletions python/python/lance_csr/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)
18 changes: 1 addition & 17 deletions python/python/tests/test_config_builder.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"},
]
25 changes: 2 additions & 23 deletions python/python/tests/test_engine_alternation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading