README.MD

June 12, 2026 · View on GitHub

Build & test

Brackit

A powerful JSONiq engine for querying JSON and XML

Use it standalone like jq, or embed it in your data store


Why Brackit?

Two ways to use it:

  1. Command-line tool (bjq) - Like jq, but with FLWOR expressions, joins, and user-defined functions
  2. Embeddable query engine - Add JSONiq queries to your data store with automatic optimizations
# Query JSON from the command line
echo '{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]}' | \
  bjq 'for $u in $$.users[] where $u.age > 26 return $u.name'

"Alice"

Performance

bjq native binary (Oracle GraalVM, PGO, G1 GC, -O3) vs jq 1.7 — wall-clock, median of 3 runs:

1M records (173 MB flat array):

QuerybjqjqSpeedup
Filter (age > 40 and active)2.4s3.7s1.6x
Group by dept + 3 aggregates1.4s7.0s4.9x
Group by 2 keys + sort1.5s6.0s3.9x
Hash-join (1M customers × 5M orders)44.4s— (O(n·m), skipped)
Join + group + aggregate + sort14.7s— (skipped)
5-way aggregation1.7s4.9s2.9x
String equality filter2.0s3.6s1.8x
Top-N (order + slice)1.2s5.8s4.8x
Compound AND filter2.8s4.0s1.4x
Count distinct1.5s5.9s3.9x
Multi-key group + top-N5.8s39.6s6.8x

100k records (18 MB):

QuerybjqjqSpeedup
Filter314ms376ms1.2x
Group by dept + 3 aggregates249ms634ms2.5x
Group by 2 keys + sort249ms543ms2.2x
5-way aggregation205ms466ms2.3x
Top-N240ms517ms2.2x
Count distinct184ms496ms2.7x
Multi-key group + top-N968ms3.2s3.3x

Reproducible: ./examples/benchmark.sh --sizes "10000 100000 1000000" (or benchmark-3way.sh for native vs JAR vs jq).

Scaling: 1M → 100M → 1B records — 9 query shapes × 3 engines

Compact 48-byte schema ({"age":N,"dept":"X","city":"X","active":T}), Linux, 20 cores, NVMe SSD. Native binary built with PGO trained on all 9 query shapes, -H:+VectorAPISupport, G1 GC, -O3. JAR run with HotSpot JIT.

1M records (50 MB):

Querynative + PGOJAR (JVM)jq
filter-count503ms603ms1.5s
group-by dept718ms657ms3.8s
group-by 2 keys828ms711ms5.4s
filter + group-by727ms649ms2.7s
count distinct638ms584ms3.6s
sum(age)475ms561ms1.3s
avg(age)473ms577ms1.3s
min(age)469ms550ms1.2s
max(age)467ms563ms1.2s

100M records (4.9 GB):

Querynative + PGOJAR (JVM)jq
filter-count1.5s1.5sOOM
group-by dept1.7s2.9sOOM
group-by 2 keys1.6s2.6sOOM
filter + group-by1.6s2.7sOOM
count distinct1.6s2.8sOOM
sum(age)1.5s1.7sOOM
avg(age)1.4s1.7sOOM
min(age)1.5s1.6sOOM
max(age)1.5s1.6sOOM

1B records (49 GB):

Querynative + PGOJAR (JVM)jq
filter-count13.6s13.4sOOM
group-by dept14.7s14.8sOOM
group-by 2 keys14.5s14.7sOOM
filter + group-by15.1s15.2sOOM
count distinct14.5s15.3sOOM
sum(age)13.1s13.4sOOM
avg(age)13.0s13.5sOOM
min(age)13.4s13.7sOOM
max(age)13.4s13.4sOOM

jq OOMs at 100M records (exit 137, OOM-killed by the kernel). Both Brackit paths scale linearly: 1M to 100M is ~3x time for 100x data (cache warm-up + per-thread mmap), 100M to 1B is ~9x time for 10x data — at 1B we're at ~3.4 GB/s, approximately raw NVMe sequential read speed. Pick native for CLI/startup latency, JVM when embedding Brackit as a library.

Reproducible: examples/Gen1B.java (compact 48-byte generator, ~250 MB/s) + examples/benchmark-1b.sh.

Quick Start

Option 1: Native Binary (fastest)

Download the pre-built binary for your platform:

# Linux (x86-64)
curl -L https://github.com/sirixdb/brackit/releases/latest/download/bjq-linux-amd64 -o bjq
chmod +x bjq
sudo mv bjq /usr/local/bin/

# Linux (ARM64)
curl -L https://github.com/sirixdb/brackit/releases/latest/download/bjq-linux-arm64 -o bjq
chmod +x bjq
sudo mv bjq /usr/local/bin/

# macOS (Apple Silicon)
curl -L https://github.com/sirixdb/brackit/releases/latest/download/bjq-macos-arm64 -o bjq
chmod +x bjq
sudo mv bjq /usr/local/bin/

# macOS (Intel)
curl -L https://github.com/sirixdb/brackit/releases/latest/download/bjq-macos-amd64 -o bjq
chmod +x bjq
sudo mv bjq /usr/local/bin/

# Windows (x86-64) - download bjq-windows-amd64.exe from GitHub Releases

Then use it:

echo '{"name": "Alice"}' | bjq '$$.name'

# FLWOR expressions - the killer feature!
bjq 'for $u in $$.users[] where $u.age > 21 order by $u.name return $u' data.json

Option 2: Java Jar

Requires Java 25 or later. Download the jar from GitHub Releases, then:

alias bjq='java --enable-preview --add-modules=jdk.incubator.vector -jar /path/to/bjq-jar-with-dependencies.jar'
bjq 'for $u in $$.users[] where $u.age > 21 return $u' data.json

Option 3: Build from Source

Requires Java 25 or later.

git clone https://github.com/sirixdb/brackit.git
cd brackit
mvn package

# Set up bjq alias
alias bjq='java --enable-preview --add-modules=jdk.incubator.vector -jar '$(pwd)'/target/bjq-jar-with-dependencies.jar'

# Try it out - FLWOR with grouping!
echo '[{"cat":"A","v":1},{"cat":"B","v":2},{"cat":"A","v":3}]' | \
  bjq 'for $x in $$[] group by $c := $x.cat return {$c: sum($x.v)}'

Features at a Glance

FeatureExample
Field access$$.users[0].name
Array iteration$$.items[].price
Python-style slices$$[0:5], $$[-1], $$[::2]
Object projection$${name, email}
Predicates$$.users[][?$$.active]
FLWOR expressionsfor $x in $$ where $x.age > 21 return $x
User-defined functionsdeclare function local:double($x) { $x * 2 }
Automatic join optimizationHash-joins for FLWOR with multiple for clauses
JSON updatesinsert, delete, replace, rename

A [? ... ] predicate that references the current item $$ is a truthiness filter: it keeps the items for which the expression has a true effective boolean value (null, false, 0, "", and a missing member are falsy). Predicates that do not reference $$ — such as [?1] or [?last()] — select by position, as in XQuery.

Mutable JSON with Update Expressions

Brackit supports the full JSONiq Update Facility - modify JSON data with declarative expressions:

(: Insert fields into an object :)
insert json {"status": "active", "updated": current-dateTime()} into $user

(: Append to an array :)
append json $newItem into $order.items

(: Update a value :)
replace json value of $product.price with $product.price * 0.9

(: Remove a field :)
delete json $user.temporaryToken

(: Rename a field :)
rename json $record.oldFieldName as "newFieldName"

This makes Brackit ideal for data stores that need to expose update capabilities through a query language.

The Power of FLWOR

Unlike simple path-based query languages, Brackit supports full FLWOR expressions (for, let, where, order by, return) - the SQL of JSON:

(: Group sales by category and compute totals :)
for $sale in $$.sales[]
let $cat := $sale.category
group by $cat
order by sum($sale.amount) descending
return {
  "category": $cat,
  "total": sum($sale.amount),
  "count": count($sale)
}
(: Join orders with customers - automatically optimized! :)
for $order in $$.orders[], $customer in $$.customers[]
where $order.customer_id eq $customer.id
return {
  "order": $order.id,
  "customer": $customer.name,
  "total": $order.total
}

bjq: The jq Alternative

bjq provides a familiar jq-like interface with JSONiq power:

# Basic field access
bjq '$$.name' data.json

# Array operations
bjq '$$.users[].email' data.json
bjq '$$[0:5]' data.json              # First 5 elements
bjq '$$[-1]' data.json               # Last element

# Filtering
bjq 'for $u in $$.users[] where $u.active return $u' data.json

# Aggregation
bjq 'sum($$.prices[])' data.json

# Raw output (no quotes)
bjq -r '$$.name' data.json

# Compact output
bjq -c '$$' data.json

Embed in Your Data Store

Brackit is designed as a retargetable query compiler. Data stores can plug in their own:

  • Physical optimizations (index scans, specialized operators)
  • Storage backends (your custom Node/Item implementations)
  • Rewrite rules (index matching, predicate pushdown)
// Minimal example: run a query in Java
QueryContext ctx = new BrackitQueryContext();
Query query = new Query("for $i in 1 to 10 return $i * $i");
query.serialize(ctx, System.out);

The optimizer automatically applies:

  • Hash-joins for multi-variable FLWOR expressions
  • Predicate pushdown
  • Constant folding
  • And more...

Installation

Maven

<dependency>
  <groupId>io.sirix</groupId>
  <artifactId>brackit</artifactId>
  <version>0.7</version>
</dependency>

Gradle

dependencies {
    implementation 'io.sirix:brackit:0.7'
}

JSONiq Syntax

Arrays

[ 1, 2, 3 ]                          (: literal array :)
[ =(1 to 5) ]                        (: spread: [1, 2, 3, 4, 5] :)
$arr[0]                              (: index access (0-based!) :)
$arr[-1]                             (: last element :)
$arr[1:3]                            (: slice :)
$arr[]                               (: unbox to sequence :)

Objects

{ "name": "Alice", "age": 30 }       (: literal object :)
$obj.name                            (: field access :)
$obj{name, age}                      (: projection :)
{ $obj1, $obj2 }                     (: merge objects :)

Updates (for mutable stores)

insert json {"new": "field"} into $obj
delete json $obj.field
replace json value of $obj.name with "Bob"
rename json $obj.old as "new"

Differences from Standard JSONiq

  • Array indexes start at 0 (not 1)
  • Object projection: $obj{field1, field2} instead of jn:project()
  • Python-style array slices: $arr[start:end:step]
  • Statement syntax with semicolons (syntactic sugar for let-bindings)

Community

Join us on Discord to ask questions, share ideas, or contribute!

Used By

  • SirixDB - A bitemporal, append-only database storing JSON and XML with full version history at the node level

Origins & Publications

Brackit was created by Sebastian Bächle during his PhD at TU Kaiserslautern, researching query processing for semi-structured data. It's now maintained as part of the SirixDB project.

License

New BSD License