Skip to content
Open
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
8 changes: 4 additions & 4 deletions ci/check_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

Run with ``python -I`` from a clean virtual environment. Covers both table placements
(Arrow tables in memory, Lance datasets on disk), CSR traversal, shortestPath, and
``refresh()`` picking up a new Lance version.
``checkout_latest()`` picking up a new Lance version.
"""

import importlib.metadata
Expand Down Expand Up @@ -57,17 +57,17 @@ def main():
engine = CypherEngine(config, datasets, use_csr=True)
assert engine.execute(TWO_HOPS).to_pylist() == [{"name": "Carol"}]
assert engine.execute(SHORTEST).to_pylist() == [{"hops": 3}]
# The last engine reads Lance datasets: refresh() follows an appended version.
# The last engine reads Lance datasets: checkout_latest() follows an appended version.
appended = pa.table({"src": [4], "dst": [1]})
lance.write_dataset(appended, Path(root) / "knows.lance", mode="append")
engine.refresh()
engine.checkout_latest()
got = engine.execute(
"MATCH (a:Person {id: 4})-[:KNOWS]->(b:Person) RETURN b.id AS id"
)
assert got.to_pylist() == [{"id": 1}]
print(
f"Installed lance-csr {importlib.metadata.version('lance-csr')} wheel passed "
f"in-memory, on-disk, CSR, shortestPath and refresh checks on Python "
f"in-memory, on-disk, CSR, shortestPath and checkout_latest checks on Python "
f"{sys.version.split()[0]} without Rust"
)

Expand Down
5 changes: 0 additions & 5 deletions crates/lance-csr-python/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1124,11 +1124,6 @@ impl CypherEngine {
.map_err(graph_error_to_pyerr)
}

/// Deprecated alias; the Python wrapper warns before calling it.
fn refresh(&self, py: Python) -> PyResult<()> {
self.checkout_latest(py)
}

fn optimize_csr(&self, py: Python) -> PyResult<()> {
RT.block_on(Some(py), self.inner.optimize_csr())?
.map_err(graph_error_to_pyerr)
Expand Down
14 changes: 7 additions & 7 deletions crates/lance-csr/src/csr_expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ use crate::error::{GraphError, Result};
///
/// # Design note for future changes (read before "optimizing" the delta)
///
/// `with_changes` copies the whole delta (not the base) on every refresh, so refresh
/// cost grows with the delta accumulated since the last full build, not just with
/// `with_changes` copies the whole delta (not the base) on every `checkout_latest()`,
/// so its cost grows with the delta accumulated since the last full build, not just with
/// the new commit. This is deliberate. Snapshots must stay immutable because
/// in-flight queries keep using the previous one, so the delta cannot be mutated in
/// place. The copy is bounded: the engine rebuilds once the delta exceeds
Expand All @@ -117,16 +117,16 @@ use crate::error::{GraphError, Result};
/// occurrence offsets; queries can then resume without allocating deletion maps.
///
/// Measured at 10M edges (`python/benchmarks/csr_delta_accumulated.py`, 20 × 1%
/// appends): refresh grows from about 11 ms to about 80 ms as the delta reaches 20%,
/// appends): `checkout_latest()` grows from about 11 ms to about 80 ms as the delta reaches 20%,
/// against 1.2–1.6 s for a full rebuild. Two alternatives were tried or assessed
/// and rejected:
/// - shared, immutable delta layers merged like a binary counter: median refresh
/// 74 → 32 ms, but merge cascades spiked to 298 ms, lookups got 5–25% slower, and
/// - shared, immutable delta layers merged like a binary counter: median
/// `checkout_latest()` 74 → 32 ms, but merge cascades spiked to 298 ms, lookups got 5–25% slower, and
/// the code was about 60 lines larger;
/// - a persistent hash map (`imbl`): no spikes and one lookup, but about five new
/// crates and `unsafe` code to trust.
///
/// Revisit if refreshes become frequent on graphs far larger than 10M edges, or if
/// Revisit if calls to `checkout_latest()` become frequent on graphs far larger than 10M edges, or if
/// the rebuild threshold is raised.
pub struct Adjacency {
base: Arc<CsrBase>,
Expand Down Expand Up @@ -200,7 +200,7 @@ impl Adjacency {
/// Removals apply to the edges present before this call, so a rewritten fragment
/// can drop its old rows and add new ones under the same fragment id. `self` is
/// unchanged, so snapshots still using it keep their answers. The base is never copied.
// shortcut: copies the accumulated delta on refresh; bounded by the engine's 25% rebuild threshold. Upgrade to a persistent map if refreshes get frequent on much larger graphs.
// shortcut: copies the accumulated delta on each `checkout_latest()`; bounded by the engine's 25% rebuild threshold. Upgrade to a persistent map if `checkout_latest()` gets frequent on much larger graphs.
pub fn with_changes(&self, added: &[(i64, i64, u64)], removed: &RemovedRows) -> Self {
let mut next = Self {
base: self.base.clone(),
Expand Down
25 changes: 10 additions & 15 deletions crates/lance-csr/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,17 +318,18 @@ struct CsrEntry {
base_version: Option<u64>,
/// Wall time of the last build or delta application.
build_ms: f64,
/// How the last refresh or optimize produced this entry: build, rebuild, loaded, delta or reused.
/// How the last build, `checkout_latest()` or optimize produced this entry: build, rebuild, loaded, delta or reused.
update: &'static str,
}

/// Rebuild instead of applying a delta once pending changes (appended edges +
/// tombstones, accumulated since the last full build) would exceed this fraction
/// of the base. Lookups read the delta on every query, and a rebuild clears it.
///
/// This threshold also bounds refresh cost: each delta refresh copies the whole
/// accumulated delta (see the design note on `crate::csr_expand::Adjacency`).
/// Raising it makes refreshes slower as well as lookups.
/// This threshold also bounds the cost of `checkout_latest()`: each delta it applies
/// copies the whole accumulated delta (see the design note on
/// `crate::csr_expand::Adjacency`).
/// Raising it makes `checkout_latest()` slower as well as lookups.
// shortcut: fixed threshold, chosen from python/benchmarks/csr_delta_accumulated.py; tune per workload if deltas grow large.
const CSR_REBUILD_FRACTION: f64 = 0.25;

Expand Down Expand Up @@ -741,7 +742,7 @@ fn memtable_partitions(batch: &RecordBatch) -> Vec<Vec<RecordBatch>> {
/// source and target counts when the CSR is built, and otherwise one scan of the two
/// endpoint columns; only integer endpoints are counted. Reused while a dataset is
/// the same snapshot, so an unchanged `checkout_latest()` costs nothing.
// shortcut: the fallback scan reads every edge endpoint at construction and on each changed refresh (about one CSR build's scan); switch to an approximate counter (HyperLogLog) or Lance column statistics if that shows up in startup time.
// shortcut: the fallback scan reads every edge endpoint at construction and on each `checkout_latest()` that finds changes (about one CSR build's scan); switch to an approximate counter (HyperLogLog) or Lance column statistics if that shows up in startup time.
async fn build_key_statistics(
config: &GraphConfig,
sources: &HashMap<String, EngineSource>,
Expand Down Expand Up @@ -793,7 +794,7 @@ async fn build_key_statistics(
dst.name().clone(),
));
match csr.get(&mapping.relationship_type) {
// Built from this same snapshot in this refresh, so its counts are current.
// Built from this same snapshot in this `checkout_latest()`, so its counts are current.
Some(entry) => {
distinct.insert(src.name().clone(), entry.outgoing.num_sources());
distinct.insert(dst.name().clone(), entry.incoming.num_sources());
Expand Down Expand Up @@ -923,7 +924,7 @@ async fn build_csr_entries(
}
}

// The one full build: the first, a refresh whose delta would pass the threshold,
// The one full build: the first, a `checkout_latest()` whose delta would pass the threshold,
// and optimize_csr() (`rebuild_deltas`) all end here.
let started = std::time::Instant::now();
// Two projected scans of the endpoint columns, one per direction.
Expand Down Expand Up @@ -1339,12 +1340,6 @@ impl CypherEngine {
Ok(())
}

/// Former name of [`checkout_latest`](Self::checkout_latest).
#[deprecated(note = "renamed to `checkout_latest`, after Lance's `Dataset::checkout_latest`")]
pub async fn refresh(&self) -> Result<()> {
self.checkout_latest().await
}

/// Rebuild each CSR that carries a delta, at the snapshot the engine is pinned to: the
/// CSR's counterpart of Lance's `optimize_indices()`.
///
Expand Down Expand Up @@ -1650,7 +1645,7 @@ mod tests {
assert_eq!(first, again);
assert_eq!(first.num_rows(), 1);

// A refresh publishes a new snapshot with an empty cache; the old one is untouched.
// `checkout_latest()` publishes a new snapshot with an empty cache; the old one is untouched.
engine.checkout_latest().await.unwrap();
assert!(engine.snapshot().plans.lock().unwrap().is_empty());
assert_eq!(cached(), PLAN_CACHE_ENTRIES);
Expand Down Expand Up @@ -1717,7 +1712,7 @@ mod tests {
let sources = HashMap::from([("Person".to_string(), source)]);
let engine = Arc::new(CypherEngine::new(config, sources, true).await.unwrap());

// Stand in for refresh A, still building its replacement.
// Stand in for `checkout_latest()` A, still building its replacement.
let in_flight = engine.update_lock.lock().await;
let b = tokio::spawn({
let engine = engine.clone();
Expand Down
2 changes: 1 addition & 1 deletion docs/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ uv run --no-sync python benchmarks/csr_expand.py --data /tmp/csr-bench --edges 1
| --- | --- |
| `csr_expand.py` | CSR build cost, single-hop latency against the join path, `checkout_latest()` cost and peak RSS, per edge count |
| `csr_delta.py` | `checkout_latest()` with a delta against a full rebuild, after one append or delete commit (uses `csr_expand.py`'s data) |
| `csr_delta_accumulated.py` | Refresh cost as deltas accumulate over many small commits (the basis of the 25% rebuild threshold) |
| `csr_delta_accumulated.py` | `checkout_latest()` cost as deltas accumulate over many small commits (the basis of the 25% rebuild threshold) |
| `engine_cache.py` | Engine construction, first-query, warm-query and `checkout_latest()` timings; can compare against upstream `lance_graph` |
| `bounded_memory.py` | Peak memory of streamed scans over Lance data larger than a memory cap (`generate`, then `run`) |

Expand Down
2 changes: 1 addition & 1 deletion docs/internals/execution-plumbing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ partition per core. Lance datasets are scanned by Lance's own parallel reader.
Each [snapshot](../execution.md#snapshots-and-concurrency) keeps up to 128 unoptimized DataFusion plan templates,
least recently used first out, keyed by the query text and the types of its parameters. Running a query again with
new scalar `$parameters` skips parsing and graph planning, and only binds the new values. Some operators need literal
values while planning; when a template can't be built, the query is planned in full as before. A refresh starts the
values while planning; when a template can't be built, the query is planned in full as before. `checkout_latest()` starts the
new snapshot with an empty cache. CALL queries skip the cache and are planned in full.

## Cooperative deadlines
Expand Down
8 changes: 4 additions & 4 deletions docs/internals/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ the data structure those pieces read.
| --- | --- |
| [Custom operators](operators.md) | `CsrExpand`, `NodeLookup`, `PathExists`, `ShortestPath`, `PathMatch`, `Apply` and `KeyStatistics`, with their plans |
| [Optimizer rules and rewrites](optimizer.md) | The two logical optimizer rules, graph rewrites before DataFusion sees the plan, and join statistics |
| [Indexes and refresh](indexes.md) | How the CSR adjacency and node indexes are laid out, and how a refresh diffs Lance versions |
| [Indexes and updates](indexes.md) | How the CSR adjacency and node indexes are laid out, and how `checkout_latest()` diffs Lance versions |
| [Execution plumbing](execution-plumbing.md) | Partitioning, the plan template cache, deadlines and Cypher functions |

## What lance-csr added to lance-graph
Expand All @@ -34,7 +34,7 @@ Added in lance-csr
: Everything this section describes as a DataFusion extension: the extension nodes and their operators, the
extension planner, both optimizer rules, the deadline rule and the join statistics. Also the graph rewrites
(re-rooting, alternation and multi-label branching), the `OptionalMatch`, `ShortestPath`, `PathMatch` and `Apply`
operators, the CSR adjacency and node indexes with incremental refresh, engine snapshots, the plan template cache
operators, the CSR adjacency and node indexes with incremental updates, engine snapshots, the plan template cache
and the Cypher-specific functions.

[lance-graph](https://github.com/lance-format/lance-graph) plans every hop as joins, and lance-csr keeps that planning
Expand Down Expand Up @@ -109,7 +109,7 @@ All paths are under

| File | What it holds | Origin |
| --- | --- | --- |
| `csr_expand.rs` | `Adjacency`, `NodeRows`, `EdgeRows`, `CsrExpandNode` and `CsrExpandExec`, `CsrExtensionPlanner`, `CsrQueryPlanner`, both optimizer rules, fragment diffing for refresh | lance-csr |
| `csr_expand.rs` | `Adjacency`, `NodeRows`, `EdgeRows`, `CsrExpandNode` and `CsrExpandExec`, `CsrExtensionPlanner`, `CsrQueryPlanner`, both optimizer rules, fragment diffing for `checkout_latest()` | lance-csr |
| `csr_expand/node_lookup.rs` | `NodeLookupNode` and `NodeLookupExec` | lance-csr |
| `csr_index.rs` | The compact CSR neighbour store | [lance-graph](https://github.com/lance-format/lance-graph); edge rows and the delta added in lance-csr, unused traversal helpers removed |
| `csr_traversal.rs` | Resumable, typed variable-length walks | lance-csr |
Expand All @@ -120,6 +120,6 @@ All paths are under
| `pattern_order.rs`, `pattern_alternatives.rs` | Re-rooting, predicates implied by an `OR`, alternation and multi-label branching | lance-csr |
| `logical_plan.rs` | Graph logical operators | [lance-graph](https://github.com/lance-format/lance-graph), extended in lance-csr |
| `datafusion_planner/` | Graph plan to DataFusion plan; `builder/expand_ops.rs` chooses CSR or join for each hop | [lance-graph](https://github.com/lance-format/lance-graph), extended in lance-csr |
| `engine.rs` | `CypherEngine`: snapshots, session setup, refresh, plan template cache, SQL entry points | lance-csr |
| `engine.rs` | `CypherEngine`: snapshots, session setup, `checkout_latest()`, plan template cache, SQL entry points | lance-csr |
| `graph_table.rs` | The `GRAPH_TABLE` rewrite and `GraphTableFunction` | lance-csr |
| `query/deadline.rs` | `DeadlineJoins` and `with_execution_deadline` | lance-csr |
Loading
Loading