Skip to content

Commit eda3a4b

Browse files
committed
Use jetro 0.5.9
1 parent 640f5df commit eda3a4b

5 files changed

Lines changed: 158 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ homepage = "https://github.com/mitghi/jetrocli"
88
repository = "https://github.com/mitghi/jetrocli"
99

1010
[dependencies]
11-
jetro-core = "0.5.8"
11+
jetro-core = "0.5.9"
1212
serde_json = "1"
1313
indexmap = "2"
1414
ratatui = "0.29"

README.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
# jetrocli
22

3+
New to jetro? [**jetro-book**](https://mitghi.github.io/jetro-book/) is the best place to start — guide, tour, and documentation for learning the expression language.
4+
35
<p align="center">
46
<img src="media/jetrocli.png" alt="jetrocli">
57
</p>
68

7-
Interactive split-pane TUI for [jetro](https://github.com/mitghi/jetro) — paste JSON, write expressions, see results live. Built on `ratatui` + `crossterm`.
89

9-
New to jetro? [**jetro-book**](https://mitghi.github.io/jetro-book/) is the best place to start — guide, tour, and documentation for learning the expression language.
10+
`jetrocli` is a terminal companion for [jetro](https://github.com/mitghi/jetro), a JSON expression language. It gives you two ways to work with JSON: an interactive TUI for exploring data and a command-line program for processing JSON in scripts, pipes, files, and NDJSON datasets.
11+
12+
Run it directly and it opens a split-pane JSON workbench: paste or load JSON, write a jetro expression, and see the result update live as you type. Pipe or redirect input and it skips the TUI, evaluates once, and prints the result like a regular Unix command.
13+
14+
For large files, `jetrocli` can memory map regular-file input. In `--ndjson` mode it scans one JSON document per line, supports reverse reads from the end of log-style files, and can stop early with `--limit` for bounded queries over very large datasets.
1015

1116
## Features
1217

@@ -15,6 +20,11 @@ New to jetro? [**jetro-book**](https://mitghi.github.io/jetro-book/) is the best
1520
- **Structural folding** in JSON editor. Fold any `{…}` / `[…]` block, with gutter triangles (`` / ``) and inline `⋯ N lines` markers.
1621
- **Schema-aware completion** — suggests fields at the current path, auto-unwraps element fields inside array chains, filters builtins by receiver type.
1722
- **Inline docs pane** next to completions — every jetro builtin ships with signature, summary, and example.
23+
- **Pipe / batch mode without TUI** — when stdin is piped or redirected, jetrocli evaluates once and prints the result directly for shell workflows.
24+
- **File-backed large JSON reads** — regular-file stdin is memory mapped for zero-copy loading instead of forcing the interactive path.
25+
- **Fast NDJSON scans**`--ndjson` evaluates one JSON document per line from `-i <FILE>` and emits one compact result per row.
26+
- **Reverse NDJSON reads**`--ndjson -r` scans from tail to head, useful for log-style files where the newest rows matter first.
27+
- **Bounded NDJSON filters** — combine `--ndjson` with `--limit <N>` to stop after the first `N` emitted rows, including reverse scans for "latest matching rows" queries.
1828
- **Emacs-style bindings** throughout (`C-a/C-e`, `C-f/C-b`, `M-f/M-b`, `C-n/C-p`, `C-g`, `C-c` prefix chord).
1929
- **Expression formatter** — breaks long jetro chains onto indented lines (`C-c C-f`).
2030

@@ -92,6 +102,41 @@ jetrocli --ndjson -i app.log -r --limit 50 '$.msg' # last 50 matches
92102
| `--max-line-bytes <BYTES>` | Per-line byte cap. Default 64 MiB. |
93103
| `--reverse-chunk <BYTES>` | Reverse reader chunk size. Tune for very wide rows. |
94104

105+
## Performance
106+
107+
NDJSON mode is built for file-backed batch scans. It memory maps the input file, evaluates the expression independently for each line, and emits compact JSON results without starting the interactive TUI.
108+
109+
The repository includes a reproducible benchmark in `benchmark/`:
110+
111+
```sh
112+
rustc -O benchmark/gen_ndjson.rs -o /tmp/gen_ndjson
113+
/tmp/gen_ndjson /tmp/big.ndjson 1000000000
114+
benchmark/bench.sh
115+
```
116+
117+
`benchmark/bench.sh` compares only the `jetrocli` and [`jaq`](https://github.com/01mf02/jaq) program. The generator writes roughly 1 GB of NDJSON shaped like:
118+
119+
```json
120+
{"id":1,"name":"user_1","attributes":[{"key":"k1","value":"v_1_1"}]}
121+
```
122+
123+
One run on an Apple M1 laptop over 4,764,404 rows produced:
124+
125+
| Query | jetro expression | jaq expression | jetrocli | jaq | Speedup |
126+
| --- | --- | --- | ---: | ---: | ---: |
127+
| Project id | `$.id` | `.id` | 0.72s | 27.89s | **38.7x** |
128+
| Project name | `$.name` | `.name` | 0.30s | 28.99s | **96.6x** |
129+
| Attributes count | `$.attributes.len()` | `.attributes \| length` | 1.52s | 28.19s | **18.5x** |
130+
| Attribute keys list | `$.attributes.map(@.key)` | `.attributes \| map(.key)` | 2.06s | 39.98s | **19.4x** |
131+
| First attr value | `$.attributes.first().value` | `.attributes[0].value` | 0.80s | 29.22s | **36.5x** |
132+
| Last attr value | `$.attributes.last().value` | `.attributes[-1].value` | 1.61s | 29.25s | **18.2x** |
133+
| Uppercase name | `$.name.upper()` | `.name \| ascii_upcase` | 0.41s | 28.42s | **69.3x** |
134+
| `[key,value]` pairs | `$.attributes.map([@.key, @.value])` | `.attributes \| map([.key, .value])` | 2.99s | 54.26s | **18.1x** |
135+
| Count attrs matching `_3` | `$.attributes.filter(@.value.contains("_3")).len()` | `[.attributes[] \| select(.value \| contains("_3"))] \| length` | 1.52s | 48.15s | **31.7x** |
136+
| Object keys | `$.keys()` | `keys` | 1.05s | 28.48s | **27.1x** |
137+
138+
In practice, expect NDJSON mode to be especially strong for field projection, string transforms, row-local indexing, and shallow array operations over large files. Queries that allocate larger derived arrays or inspect more nested values naturally move more bytes and take longer.
139+
95140
## License
96141

97142
MIT

benchmark/bench.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
# jetrocli vs jaq, 10 per-row queries over 1 GB NDJSON.
3+
# Output: per-tool wall time + emitted row count, written to /tmp/bench_results.txt.
4+
5+
set -u
6+
FILE=/tmp/big.ndjson
7+
OUT=/tmp/bench_results.txt
8+
> "$OUT"
9+
10+
run() {
11+
local idx=$1 jq=$2 jeq=$3 desc=$4
12+
echo "=== Q$idx: $desc ===" | tee -a "$OUT"
13+
echo " jetro: $jeq" | tee -a "$OUT"
14+
echo " jaq : $jq" | tee -a "$OUT"
15+
16+
local out_j=/tmp/bench_q${idx}_jetro.out
17+
local out_a=/tmp/bench_q${idx}_jaq.out
18+
19+
/usr/bin/time -p jetrocli --ndjson -i "$FILE" "$jeq" > "$out_j" 2>/tmp/jc_t
20+
local jc_real=$(awk '/real/ {print $2}' /tmp/jc_t)
21+
local jc_rows=$(wc -l < "$out_j")
22+
23+
/usr/bin/time -p jaq -c "$jq" < "$FILE" > "$out_a" 2>/tmp/ja_t
24+
local ja_real=$(awk '/real/ {print $2}' /tmp/ja_t)
25+
local ja_rows=$(wc -l < "$out_a")
26+
27+
printf " jetrocli %ss rows=%s\n" "$jc_real" "$jc_rows" | tee -a "$OUT"
28+
printf " jaq %ss rows=%s\n" "$ja_real" "$ja_rows" | tee -a "$OUT"
29+
echo | tee -a "$OUT"
30+
}
31+
32+
run 1 '.id' '$.id' "project id"
33+
run 2 '.name' '$.name' "project name"
34+
run 3 '.attributes | length' '$.attributes.len()' "attributes count"
35+
run 4 '.attributes | map(.key)' '$.attributes.map(@.key)' "attribute keys list"
36+
run 5 '.attributes[0].value' '$.attributes.first().value' "first attr value"
37+
run 6 '.attributes[-1].value' '$.attributes.last().value' "last attr value"
38+
run 7 '.name | ascii_upcase' '$.name.upper()' "uppercase name"
39+
run 8 '.attributes | map([.key, .value])' '$.attributes.map([@.key, @.value])' "[key,value] pairs"
40+
run 9 '[.attributes[] | select(.value | contains("_3"))] | length' '$.attributes.filter(@.value.contains("_3")).len()' "count attrs matching _3"
41+
run 10 'keys' '$.keys()' "object keys"

benchmark/gen_ndjson.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Standalone NDJSON generator. Writes ~target_bytes worth of rows.
2+
// Row shape: {"id":N,"name":"user_N","attributes":[{"key":"kI","value":"v_N_I"}, ...]}
3+
//
4+
// Build: rustc -O /tmp/gen_ndjson.rs -o /tmp/gen_ndjson
5+
// Run: /tmp/gen_ndjson <out_path> <target_bytes>
6+
7+
use std::env;
8+
use std::fs::File;
9+
use std::io::{BufWriter, Write};
10+
11+
const ATTRS_PER_ROW: usize = 5;
12+
13+
fn main() {
14+
let mut args = env::args().skip(1);
15+
let path = args.next().expect("usage: gen_ndjson <path> <bytes>");
16+
let target: u64 = args
17+
.next()
18+
.expect("usage: gen_ndjson <path> <bytes>")
19+
.parse()
20+
.expect("bytes must be integer");
21+
22+
let file = File::create(&path).expect("create");
23+
let mut w = BufWriter::with_capacity(1 << 20, file);
24+
let mut written: u64 = 0;
25+
let mut id: u64 = 0;
26+
let mut buf: Vec<u8> = Vec::with_capacity(512);
27+
28+
while written < target {
29+
id += 1;
30+
buf.clear();
31+
buf.extend_from_slice(b"{\"id\":");
32+
write_u64(&mut buf, id);
33+
buf.extend_from_slice(b",\"name\":\"user_");
34+
write_u64(&mut buf, id);
35+
buf.extend_from_slice(b"\",\"attributes\":[");
36+
for i in 1..=ATTRS_PER_ROW {
37+
if i > 1 {
38+
buf.push(b',');
39+
}
40+
buf.extend_from_slice(b"{\"key\":\"k");
41+
write_u64(&mut buf, i as u64);
42+
buf.extend_from_slice(b"\",\"value\":\"v_");
43+
write_u64(&mut buf, id);
44+
buf.push(b'_');
45+
write_u64(&mut buf, i as u64);
46+
buf.extend_from_slice(b"\"}");
47+
}
48+
buf.extend_from_slice(b"]}\n");
49+
w.write_all(&buf).expect("write");
50+
written += buf.len() as u64;
51+
}
52+
w.flush().expect("flush");
53+
eprintln!("wrote {} bytes, {} rows", written, id);
54+
}
55+
56+
fn write_u64(buf: &mut Vec<u8>, mut n: u64) {
57+
if n == 0 {
58+
buf.push(b'0');
59+
return;
60+
}
61+
let start = buf.len();
62+
while n > 0 {
63+
buf.push(b'0' + (n % 10) as u8);
64+
n /= 10;
65+
}
66+
buf[start..].reverse();
67+
}

0 commit comments

Comments
 (0)