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
62 changes: 21 additions & 41 deletions crates/lance-csr/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1271,11 +1271,16 @@ impl CypherQuery {
);
rows.push(("physical_plan", df_physical_str));

// Calculate column widths
// Calculate column widths. Lines wider than MAX_PLAN_WIDTH (long parameter
// values, such as a large `IN` list or a query vector) run past the right border
// instead of widening the column: padding every line to them made the output
// grow with lines × width.
const MAX_PLAN_WIDTH: usize = 1_000;
let plan_type_width = rows.iter().map(|(t, _)| t.len()).max().unwrap_or(10);
let plan_width = rows
.iter()
.map(|(_, p)| p.lines().map(|l| l.len()).max().unwrap_or(0))
.flat_map(|(_, p)| p.lines().map(|l| l.chars().count()))
.filter(|&w| w <= MAX_PLAN_WIDTH)
.max()
.unwrap_or(50);

Expand All @@ -1285,52 +1290,27 @@ impl CypherQuery {
"-".repeat(plan_type_width + 2),
"-".repeat(plan_width + 2)
);
// Pad by hand: `{:<width$}` panics on widths above u16::MAX.
let row = |plan_type: &str, plan: &str| {
format!(
"| {plan_type}{} | {plan}{} |\n",
" ".repeat(plan_type_width.saturating_sub(plan_type.chars().count())),
" ".repeat(plan_width.saturating_sub(plan.chars().count())),
)
};

output.push_str(&separator);
output.push('\n');

// Header
output.push_str(&format!(
"| {:<width$} | {:<plan_width$} |\n",
"plan_type",
"plan",
width = plan_type_width,
plan_width = plan_width
));
output.push_str(&row("plan_type", "plan"));
output.push_str(&separator);
output.push('\n');

// Data rows
// Data rows: the plan type labels the first line of each plan
for (plan_type, plan_content) in rows {
let lines: Vec<&str> = plan_content.lines().collect();
if lines.is_empty() {
output.push_str(&format!(
"| {:<width$} | {:<plan_width$} |\n",
plan_type,
"",
width = plan_type_width,
plan_width = plan_width
));
} else {
// First line with plan_type
output.push_str(&format!(
"| {:<width$} | {:<plan_width$} |\n",
plan_type,
lines[0],
width = plan_type_width,
plan_width = plan_width
));

// Remaining lines with empty plan_type
for line in &lines[1..] {
output.push_str(&format!(
"| {:<width$} | {:<plan_width$} |\n",
"",
line,
width = plan_type_width,
plan_width = plan_width
));
}
let mut lines = plan_content.lines();
output.push_str(&row(plan_type, lines.next().unwrap_or("")));
for line in lines {
output.push_str(&row("", line));
}
}

Expand Down
5 changes: 3 additions & 2 deletions docs/cypher.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,9 @@ From Rust, use `with_execution_deadline`; see [Timeouts](rust.md#timeouts).

### `explain(query, params=None)`

Returns the plan as text: the graph logical plan, DataFusion's logical plan and the physical plan.
It's the quickest way to check whether a hop used the CSR (`CsrExpandExec`) or a join (`HashJoinExec`), and
Returns the plan as text: the graph logical plan, DataFusion's logical plan and the physical plan,
laid out as a table. Lines longer than 1,000 characters, such as one holding a long list or vector
parameter, run past the table's right border rather than widening every row. It's the quickest way to check whether a hop used the CSR (`CsrExpandExec`) or a join (`HashJoinExec`), and
whether filters reached the Lance scan:

```python
Expand Down
16 changes: 16 additions & 0 deletions python/python/tests/test_explain.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,19 @@ def test_explain_error_handling(person_data):
)
with pytest.raises(ValueError, match="No input datasets provided"):
query_with_config.explain({})


def test_explain_with_large_list_parameter(person_data):
"""Explain handles plan lines wider than Rust's 65,535-character format width."""
config, people = person_data
ids = list(range(6_000))
query = (
CypherQuery("MATCH (p:Person) WHERE p.person_id IN $ids RETURN p.name")
.with_config(config)
.with_parameter("ids", ids)
)
plan = query.explain({"Person": people})

assert "5999" in plan
# Lines holding the list overflow the table instead of widening every row.
assert len(plan) < 10_000_000