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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ It is columnar, vectorized, and factorized in the DuckDB and Kùzu mold, with on
- `sqlite` stores the graph in an ordinary SQLite database file, for interop and as the differential-testing oracle.
- `s3` is object-storage-native with immutable segments, compare-and-swap manifest commits, and a request accountant that keeps the monthly bill flat.

## Sixty seconds

```rust
use zudb::{Database, params};

fn main() -> zudb::Result<()> {
let db = Database::create("social.zu1")?;
let mut conn = db.connect()?;

conn.execute("INSERT (p:person {uid: 1, name: 'ada'})")?;
conn.execute("INSERT (p:person {uid: 2, name: 'grace'})")?;

let rows = conn.query_with(
"MATCH (p:person) WHERE p.uid >= $uid RETURN p.name AS name, p.uid AS uid",
&params! { "uid" => 1 },
)?;
for row in rows.iter() {
let (name, uid): (&str, i64) = row.get()?;
println!("{name} {uid}");
}
Ok(())
}
```

That is `cargo add zudb` and the whole program: no server, no schema step, no cluster. `create` makes the file and `open` is what you use the second time, because a create that found a database and opened it instead is the call that quietly writes into somebody else's data. The same sixty seconds in Python, `import zudb`, `zudb.connect`, `.to_pandas()`, is in [zu-python](https://github.com/tamnd/zu-python).

The snippet above is a program in this repository, `crates/zu-snippets/examples/sixty-seconds.rs`, and a test holds this README to it character for character and then runs it. A quickstart is the most read and least compiled code a project has, which is how it comes to be wrong.

## Status

Early. The specification is complete and lives in [docs/](docs/), starting with the [overview](docs/00-overview.md).
Expand Down Expand Up @@ -38,6 +66,7 @@ crates/zu-s3 object-storage engine
crates/zu-query parser, planner, factorized executor
crates/zu the public embedded API (published as zudb)
crates/zu-cli the zu binary
crates/zu-snippets the snippets this README prints, compiled and run
docs/ the specification, byte-level where it matters
```

Expand Down
26 changes: 26 additions & 0 deletions crates/zu-snippets/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "zu-snippets"
description = "The snippets this repository publishes, compiled and run as printed"
publish = false
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true

[lib]
path = "src/lib.rs"

[dependencies]
# Under the name it is published as, which is the name a reader types
# after `cargo add zudb` and therefore the name the printed snippet has
# to use. A crate cannot depend on itself under another name, which is
# the whole reason the snippets are a package of their own.
zudb = { package = "zu", path = "../zu" }

[dev-dependencies]
tempfile.workspace = true

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/zu-snippets/examples/sixty-seconds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use zudb::{Database, params};

fn main() -> zudb::Result<()> {
let db = Database::create("social.zu1")?;
let mut conn = db.connect()?;

conn.execute("INSERT (p:person {uid: 1, name: 'ada'})")?;
conn.execute("INSERT (p:person {uid: 2, name: 'grace'})")?;

let rows = conn.query_with(
"MATCH (p:person) WHERE p.uid >= $uid RETURN p.name AS name, p.uid AS uid",
&params! { "uid" => 1 },
)?;
for row in rows.iter() {
let (name, uid): (&str, i64) = row.get()?;
println!("{name} {uid}");
}
Ok(())
}
25 changes: 25 additions & 0 deletions crates/zu-snippets/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! The snippets this repository publishes, as programs.
//!
//! A quickstart snippet is read far more often than any other code the
//! project ships, and it is the one piece of code nothing compiles: it
//! lives in a README, it is copied by hand, and it goes wrong quietly,
//! a rename at a time, until a reader's first five minutes are spent
//! on an error message. So the snippets are programs here, one per
//! `examples/`, and `tests/readme.rs` holds the README to them
//! character for character and then runs them.
//!
//! Running is the half that matters. A snippet that compiles proves
//! the names still exist; a snippet that runs proves the statements in
//! it still parse, the file it writes still opens, and the numbers it
//! prints are the numbers the page claims. Each one runs in a
//! directory of its own, because it writes a database into the working
//! directory exactly as a reader's copy would.
//!
//! The package exists at all because a crate cannot depend on itself
//! under a second name. This one is published as `zudb`, which is what
//! a reader types, and no example inside it could say `zudb` while
//! being an example of the same package.

/// The examples this package holds, in the order the README prints
/// them, and the name of the fenced block each one has to match.
pub const SNIPPETS: &[&str] = &["sixty-seconds"];
113 changes: 113 additions & 0 deletions crates/zu-snippets/tests/readme.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! The README against the programs it prints.
//!
//! Two checks, and the second is the one with teeth. The text in the
//! fenced block has to be the text of the example, character for
//! character, so a snippet cannot be edited on the page into something
//! that was never compiled. Then the example runs, in a directory of
//! its own, and has to print what the page says it prints, so a
//! snippet cannot go on compiling after the statements in it stopped
//! meaning what they meant.

use std::path::{Path, PathBuf};
use std::process::Command;

/// The repository root, two levels above this package.
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.expect("the package sits two levels under the root")
.to_path_buf()
}

/// Every fenced block of one language in a markdown file, in order.
fn blocks(markdown: &str, language: &str) -> Vec<String> {
let mut found = Vec::new();
let mut lines = markdown.lines();
while let Some(line) = lines.next() {
if line.trim_end() != format!("```{language}") {
continue;
}
let mut block = String::new();
for line in lines.by_ref() {
if line.trim_end() == "```" {
break;
}
block.push_str(line);
block.push('\n');
}
found.push(block);
}
found
}

/// Where the example binary is, building it if this test was run in a
/// way that did not. `cargo test` builds examples and `cargo test
/// --test readme` does not, and a test that only passes under one of
/// them is a test somebody will disbelieve.
fn example(name: &str) -> PathBuf {
// The test binary is in target/<profile>/deps, and an example of
// the same build is in target/<profile>/examples.
let exe = std::env::current_exe().expect("this test has a path");
let built = exe
.parent()
.and_then(Path::parent)
.expect("target/<profile>/deps/<test>")
.join("examples")
.join(name);
let built = built.with_extension(std::env::consts::EXE_EXTENSION);
if built.exists() {
return built;
}
let status = Command::new(env!("CARGO"))
.args(["build", "-p", "zu-snippets", "--example", name])
.current_dir(root())
.status()
.expect("cargo runs");
assert!(status.success(), "building the {name} example");
assert!(built.exists(), "no {name} example at {}", built.display());
built
}

#[test]
fn the_readme_prints_the_program_this_repository_compiles() {
let readme = std::fs::read_to_string(root().join("README.md")).expect("a README");
let printed = blocks(&readme, "rust");
assert_eq!(
printed.len(),
zu_snippets::SNIPPETS.len(),
"the README prints {} Rust blocks and this package holds {} snippets",
printed.len(),
zu_snippets::SNIPPETS.len()
);
for (block, name) in printed.iter().zip(zu_snippets::SNIPPETS) {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join(format!("{name}.rs"));
let source = std::fs::read_to_string(&path).expect("the example is there");
assert_eq!(
block,
&source,
"the README block and {} have drifted apart",
path.display()
);
}
}

#[test]
fn the_sixty_second_program_runs_and_prints_what_the_readme_says() {
let dir = tempfile::tempdir().expect("a directory of its own");
let run = Command::new(example("sixty-seconds"))
.current_dir(dir.path())
.output()
.expect("the example runs");
assert!(
run.status.success(),
"the example failed: {}",
String::from_utf8_lossy(&run.stderr)
);
assert_eq!(String::from_utf8_lossy(&run.stdout), "ada 1\ngrace 2\n");
// The reader's copy writes into the directory they ran it from,
// which is the part of the story a compile check cannot see.
assert!(dir.path().join("social.zu1").is_file());
}
5 changes: 1 addition & 4 deletions crates/zu/benches/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,7 @@ fn stage(path: &std::path::Path) {
sq.insert_node_at(
"person",
row,
&[
SqlValue::Int(row),
SqlValue::Text(format!("person-{row}")),
],
&[SqlValue::Int(row), SqlValue::Text(format!("person-{row}"))],
)
.expect("node");
}
Expand Down
Loading