The cheapest route, and the route itself
In SQLA recursive CTE, with the path glued into a string you parse back apart.
TRACE origin, dest VIA cost MINIMIZE
AS route (Flights)
The row for LHR → JFK
| cost | route |
|---|---|
| 350 | [LHR, DUB, JFK] |
An embedded, federated query engine for Java
Relix is a library that runs inside your application. Give it your existing DataSource, a CSV file and a JSON endpoint, and query them together as if they were tables in one database.
It sends the work your database can do to your database, and does the rest itself.
DataSource shop = ...; // the database you already have
Relix relix = Relix.builder().jdbc("shop", shop).build();
relix.define("""
source Regions from csv("regions.csv") {
header: true,
schema: { customer_id: NUMBER, region: STRING }
};
""");
List<Tuple> rows = relix.relation("""
GROUP region, SUM(amount) -> revenue (
SELECT status = 'OPEN' (shop.orders) JOIN Regions
)
""").toList();
region revenue North 175
The status = 'OPEN' filter ran in the database. The join with the CSV ran in your process.
One jar in your application. Nothing to install, deploy or keep running.
It stores nothing. It reads from the systems you already have.
No entities or mappings. You ask questions and get rows, or records if you prefer.
A query can't change your data. Relix reads; it never writes.
Federated queries
Real questions cross boundaries. Customers are in one database, invoices in another, support tickets behind an API, and account managers in a spreadsheet. The usual answer is a pipeline that copies everything into a warehouse first. Relix queries each system where it lives, when you ask.
“Which customers have an overdue invoice and an urgent support ticket, and who manages them?”
sent as SQL
SELECT "customer_id", "name" FROM "customers"
sent as SQL, filter included
SELECT `invoice_id`, `customer_id`,
`status`, `amount`
FROM `invoices`
WHERE (CONVERT(`status` USING utf8mb4)
COLLATE utf8mb4_0900_bin
= 'OVERDUE')
fetched, then filtered in Relix
GET https://support.example.com/api/tickets
read from disk
managers.csv
Relix, in your process
One result
| name | manager | overdue |
|---|---|---|
| Acme Ltd | Priya | 1500 |
Relix relix = Relix.builder()
.jdbc("shop", postgres) // DataSources you already have
.jdbc("billing", mysql)
.build();
relix.define("""
source Tickets from http {
url: "https://support.example.com/api/tickets",
extract: json("$.tickets"),
schema: { customer_id: NUMBER at "$.customer.id",
priority: STRING }
};
source Managers from csv("managers.csv") {
header: true,
schema: { customer_id: NUMBER, manager: STRING }
};
""");
GROUP name, manager, SUM(amount) -> overdue (
(shop.customers
JOIN SELECT status = 'OVERDUE' (billing.invoices)
JOIN Managers)
SEMI customer_id = Tickets.customer_id
SELECT priority = 'urgent' (Tickets)
)
γ name, manager, SUM(amount) → overdue (
(shop.customers
⋈ σ status = 'OVERDUE' (billing.invoices)
⋈ Managers)
⋉ customer_id = Tickets.customer_id
σ priority = 'urgent' (Tickets)
)
Names from a database connection are written connection.table. Everything else is a name you declared. The query neither knows nor cares which system is which.
Aggregate
└─ Join SEMI/NESTED_LOOP build=RIGHT
├─ Join NATURAL/HASH build=RIGHT
│ ├─ Join NATURAL/HASH build=RIGHT
│ │ ├─ PushedScan [jdbc/shop]
│ │ │ SELECT "customer_id", "name" FROM "customers"
│ │ └─ PushedScan [jdbc/billing]
│ │ SELECT `invoice_id`, `customer_id`, `status`, `amount`
│ │ FROM `invoices`
│ │ WHERE (CONVERT(`status` USING utf8mb4)
│ │ COLLATE utf8mb4_0900_bin = 'OVERDUE')
│ └─ Scan Managers
└─ Select
└─ Scan Tickets
Each PushedScan is a database computing its own share; everything above them runs in this process. The API and the file have no query engine to fold into, and a join across two connections has no single system to run in — so what crosses the network here is whatever those scans return.
Nothing is loaded into a warehouse first, so an answer is as current as the systems it came from.
A database gets the filters, grouping and sorting it can run as SQL, written for its dialect, and Relix computes the rest. How much crosses the network is a property of the query, which is why the plan is on this page rather than a promise about it.
A federated source is just a table, so the routes, budgets and rankings below work across systems too.
One operator a backend can’t express ends the fold there, and a join across two connections is always computed here. explain() says where the line fell, before a row is read.
Reads from PostgreSQL · MySQL · any JDBC database · CSV · JSON files · HTTP / JSON APIs · MongoDB (plugin) · rows your program already holds
Beyond SQL
Every Relix operation takes tables and returns a table, so each of these works on federated data as readily as on a single table.
In SQLA recursive CTE, with the path glued into a string you parse back apart.
TRACE origin, dest VIA cost MINIMIZE
AS route (Flights)
The row for LHR → JFK
| cost | route |
|---|---|
| 350 | [LHR, DUB, JFK] |
In SQLNo query for it. Export the data, run a solver somewhere else, paste the answer back.
OPTIMIZE MAXIMIZE SUM(value)
SUBJECT TO SUM(cost) <= 50000
PER region (Backlog)
OPTIMIZE MAXIMIZE SUM(value)
SUBJECT TO SUM(cost) ≤ 50000
PER region (Backlog)
Result: the repairs to fund
| repair | value | cost |
|---|---|---|
| road | 60 | 20000 |
| school | 50 | 25000 |
In SQLA window function in a subquery, filtered outside it.
TOP 1 amount DESC
PER customer (Orders)
Result: whole rows
| customer | order_id | amount |
|---|---|---|
| Alice | 3 | 200 |
| Bob | 5 | 75 |
Sessions, as-of joins, lineage, graph clusters and 16 more →
Made for Java
Both produce the same query. Building one in Java means a value from outside goes in as a value rather than as text spliced into a query, so there is no string for it to break out of.
Relation open = relix.relation("""
PROJECT order_id, amount (
SELECT status = 'OPEN' (shop.orders)
)
""");
import static com.darkcollective.relix.ast.Expr.*;
String status = request.getParameter("status");
Relation open = relix.relation("shop.orders")
.select(eq(attr("status"), str(status)))
.project("order_id", "amount");
str(status) binds a value, so the request may put anything in it. The names around it are still names: a column or relation chosen at run time is an identifier, and belongs checked against the schema first — as a sort column would in any API.
No black box
Before a row is read, a query can show its plan: what was sent to each system as SQL and what Relix does itself.
System.out.println(relix.relation("""
GROUP region, SUM(amount) -> revenue (
SELECT status = 'OPEN' (shop.orders) JOIN Regions
)
""").explain());
Aggregate └─ Join NATURAL/HASH build=RIGHT ├─ PushedScan [jdbc/shop] │ SELECT order_id, customer_id, status, amount │ FROM orders WHERE (status = 'OPEN') └─ Scan Regions
Everything under PushedScan ran in the database. When a query stays inside one database, its grouping, sorting and conditional joins are sent there too.
Before you add a dependency
Any database with a JDBC driver, with SQL tuned for PostgreSQL and MySQL. Also CSV and JSON files, HTTP endpoints that return JSON, rows your program already holds, and MongoDB through a plugin. One query can use any mix of them.
No. Rows stream through the query. Filters, projections, grouping, sorting and joins on a condition within one database are sent to that database as SQL, so only what's left crosses the wire. Operations that must hold rows, such as a sort in Relix itself, can be capped (maxMaterializedRows) so a runaway query fails with a clear error instead of an OutOfMemoryError.
No. Every operator has a plain keyword (SELECT, PROJECT, JOIN, GROUP) and that's what this page uses. The symbols from relational algebra (σ, π, ⋈, γ) are an optional shorthand that means exactly the same thing, and every example in the manuals can be switched between the two.
Neither, really. Keep SQL for what lives inside one database. Relix is for the questions that cross systems, or that SQL turns into a project: routes through a graph, choices under constraints, lineage, sessions and time-aligned joins. If you think in SQL, Coming from SQL shows the everyday statements side by side with their Relix.
The Java API keeps backward compatibility within a major version, and every example in the manuals is compiled and run against the release, so the documentation matches what you install. Relix is developed by one person; About has the background.
Java 21 is the current long-term-support release, so it is the version most projects can actually adopt. The jar needs Java 21 or newer at runtime — a newer JDK is fine, and nothing in it is a preview feature.
Get started
1Add Relix to your build. The engine, functions, file, HTTP and JDBC readers, and the solver are all in the one jar. For a database, add its JDBC driver too.
dependencies {
implementation("com.darkcollective.relix:relix:1.0.0-rc6")
}
dependencies {
implementation 'com.darkcollective.relix:relix:1.0.0-rc6'
}
<dependency>
<groupId>com.darkcollective.relix</groupId>
<artifactId>relix</artifactId>
<version>1.0.0-rc6</version>
</dependency>
2Point it at a CSV file and ask a question. No database needed:
try (Relix relix = Relix.open()) {
relix.define("""
source Orders from csv("orders.csv") {
header: true,
schema: { order_id: NUMBER, customer: STRING, amount: NUMBER }
};
""");
for (Tuple t : relix.relation("TOP 1 amount DESC PER customer (Orders)").toList()) {
System.out.println(t.string("customer") + " " + t.decimal("amount"));
}
}
3Connect your own database with Relix.builder().jdbc("name", dataSource), and name its tables as name.table. Querying a database walks through it.
Going further
The Java API step by step: sessions, getting data in, building and running queries, and reading the plan. Every example is compiled and run.
Read the guide →A page for every operator and function, with a worked example, its real output, and what the engine did with it. The grammar has the whole language on one page.
What kind of problem is this, and which parts of the language solve it — a recipe per class of question, each built around a worked example with its real output.
Find a recipe →The statements you already write, each beside its Relix. Every pair is run on a real database and returns the same rows.
See them side by side →Twenty-three questions SQL makes hard or impossible, each shown with its Relix version.
See all 23 →A Game of Life spaceship, a Turing machine, a royal family tree and a Pokémon battle, each in a handful of lines.
Play →