Skip to content

feat: Properly support parameter placeholders in Python/Rust #103

@prrao87

Description

@prrao87

Summary

$param is parsed as a field name; other syntaxes (@param:param{param}, ?) that users coming from other databases may be familiar with, also fail. with_parameter also doesn’t help in Python, even though the Rust docs state that it exists.

Currently in lance-graph, it's necessary do custom string substitution before the parameterized Cypher query ever reaches the parser.

For example, in query.py, execute_query() calls apply_params(), which replaces $city, $country, etc. with literal values (e.g., 'London') in the query string. So Lance Graph never sees $city; it only sees a fully inlined literal, which is supported.

So the engine itself still doesn’t support placeholders — we’re just emulating parameters upstream in Python. Can we do better?

Example

import pyarrow as pa
from lance_graph import GraphConfig, CypherQuery

people = pa.table({"id": [1, 2], "age": [30, 40]})
cfg = GraphConfig.builder().with_node_label("Person", "id").build()
datasets = {"Person": people}

query = "MATCH (p:Person) WHERE p.age > $age RETURN p.id"
CypherQuery(query).with_parameter("age", 35).with_config(cfg).execute(datasets)
Traceback (most recent call last):
  File "/Users/prrao/code/graph-benchmark/lance_graph/tt.py", line 9, in <module>
    CypherQuery(query).with_parameter("age", 35).with_config(cfg).execute(datasets)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
ValueError: Query planning error: Failed to build filter: Schema error: No field named "$age". Did you mean 'p__age'?.

Expected

Returns id=2, when the age value is passed as a Cypher parameter.

Actual

When we attempt the example above, we get the following:
ValueError: ... No field named "$age".

A lot of custom boilerplate was needed to be written (see below) to correctly parse the parameters into a syntax that naturally maps to other systems:

def format_cypher_value(value: Any) -> str:
    if isinstance(value, str):
        escaped = value.replace("'", "''")
        return f"'{escaped}'"
    if isinstance(value, bool):
        return "true" if value else "false"
    if value is None:
        return "null"
    return str(value)


def apply_params(query: str, params: dict[str, Any]) -> str:
    # Lance Graph currently doesn't parse $param placeholders; inline values here
    # so the query string is fully concrete before parsing.
    for key, value in params.items():
        query = query.replace(f"${key}", format_cypher_value(value))
    return query


def execute_query(
    query: str,
    cfg: GraphConfig,
    datasets: dict[str, pa.Table],
    params: dict[str, Any] | None = None,
) -> pl.DataFrame:
    if params:
        # Inline params instead of using CypherQuery.with_parameter, which isn't
        # respected by the current parser.
        query = apply_params(query, params)
    cypher = CypherQuery(query)
    result = cypher.with_config(cfg).execute(datasets)
    return to_polars(result)


# use in query
result = execute_query(query, cfg, datasets, params=params)

Proposal

Currently the user has to inline parameters manually (string substitution) to proceed. We should support parameterized query execution in Cypher more naturally, like other graph DBs, to make writing queries more natural for users coming from those systems, if possible.

Environment

The following environment was used to test this:

lance-graph 0.4.0
Python 3.13
macOS Tahoe 26.2

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions