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: 5 additions & 6 deletions crates/lance-csr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,11 @@ let config = GraphConfig::builder()
.build()?;
```

For Rust callers migrating away from direct mapping-field access, use `get_node_mapping(label)` and
`get_relationship_mapping(type_name)` for case-insensitive lookups. `iter_node_mappings()` and
`iter_relationship_mappings()` yield read-only `(normalized_key, mapping)` pairs; iteration order is
unspecified. Build mappings through `GraphConfigBuilder` so keys are normalized. Public fields remain
available for compatibility until a breaking release; replacing direct writes with builder calls avoids
that future migration. The serialized configuration and Python API are unchanged.
`GraphConfig`'s mapping fields are private, so mappings are built only through `GraphConfigBuilder`,
which normalizes keys. Use `get_node_mapping(label)` and `get_relationship_mapping(type_name)` for
case-insensitive lookups. `iter_node_mappings()` and `iter_relationship_mappings()` yield read-only
`(normalized_key, mapping)` pairs; iteration order is unspecified. The serialized format is unchanged;
deserializing a configuration validates it as `build()` does. The Python API is unchanged.

## Executing Cypher Queries

Expand Down
67 changes: 38 additions & 29 deletions crates/lance-csr/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,18 @@ use std::collections::HashMap;

/// Configuration for mapping Lance datasets to property graphs
///
/// # Important: Case-Insensitive Keys
///
/// **WARNING**: `node_mappings` and `relationship_mappings` store keys as **lowercase** for
/// case-insensitive behavior. If you directly insert into these maps, you **must** normalize
/// keys to lowercase to maintain invariants.
///
/// **Recommended**: Use `GraphConfigBuilder` instead of direct field access. The builder
/// automatically normalizes keys.
///
/// # Future API Changes
///
/// These fields may become private in a future major version to enforce invariants.
/// Use the lookup and iterator accessors for reads, and [`GraphConfigBuilder`] for writes.
/// The mapping fields remain public for source compatibility until a breaking release.
/// Node labels and relationship types are case-insensitive: the mappings are keyed by their
/// lowercase form. Build a config with [`GraphConfigBuilder`], which normalizes keys, and read
/// it through the lookup and iterator accessors. Deserialization goes through the builder too,
/// so a deserialized config is validated like a built one.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "GraphConfigBuilder")]
pub struct GraphConfig {
/// Mapping of node labels to their field configurations
///
/// **Keys are stored as lowercase** for case-insensitive lookups.
/// Use `get_node_mapping()` for lookups instead of direct access.
///
/// TODO: Make this private to enforce key normalization invariants
pub node_mappings: HashMap<String, NodeMapping>,
/// Node labels (lowercase) to their field configurations
node_mappings: HashMap<String, NodeMapping>,

/// Mapping of relationship types to their field configurations
///
/// **Keys are stored as lowercase** for case-insensitive lookups.
/// Use `get_relationship_mapping()` for lookups instead of direct access.
///
/// TODO: Make this private to enforce key normalization invariants
pub relationship_mappings: HashMap<String, RelationshipMapping>,
/// Relationship types (lowercase) to their field configurations
relationship_mappings: HashMap<String, RelationshipMapping>,

/// Default node ID field if not specified in mappings
pub default_node_id_field: String,
Expand Down Expand Up @@ -204,7 +185,8 @@ impl GraphConfig {
}

/// Builder for GraphConfig
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct GraphConfigBuilder {
node_mappings: HashMap<String, NodeMapping>,
relationship_mappings: HashMap<String, RelationshipMapping>,
Expand Down Expand Up @@ -365,6 +347,14 @@ impl GraphConfigBuilder {
}
}

impl TryFrom<GraphConfigBuilder> for GraphConfig {
type Error = GraphError;

fn try_from(builder: GraphConfigBuilder) -> Result<Self> {
builder.build()
}
}

impl NodeMapping {
/// Create a new node mapping
pub fn new<S: Into<String>>(label: S, id_field: S) -> Self {
Expand Down Expand Up @@ -519,6 +509,25 @@ mod tests {
assert!(config.validate().is_err());
}

#[test]
fn deserialization_validates_through_the_builder() {
let config = GraphConfig::builder()
.node_table("Person", "id")
.rel_table("KNOWS", "Person", "Person", "src", "dst")
.build()
.unwrap();
let json = serde_json::to_string(&config).unwrap();
let restored: GraphConfig = serde_json::from_str(&json).unwrap();
assert_eq!(restored.get_node_mapping("PERSON").unwrap().label, "Person");
assert!(restored.get_relationship_mapping("knows").is_some());

let unnormalized = json.replace("\"person\":", "\"Person\":");
let message = serde_json::from_str::<GraphConfig>(&unnormalized)
.unwrap_err()
.to_string();
assert!(message.contains("not normalized"), "{message}");
}

#[test]
fn test_node_mapping_with_properties() {
let mapping = NodeMapping::new("Person", "id")
Expand Down
Loading