- All Implemented Interfaces:
AutoCloseable
Two things live here and nowhere else: the declarations a script
would contain (sources, connections, views, functions, relationships) and the
bindings that connect them to the outside world (a live
DataSource, a clock, a catalog). Everything else is a Relation, which
is a value.
try (Relix relix = Relix.builder().jdbc("warehouse", dataSource).build()) {
relix.define("""
source Orders from warehouse { table: "orders" };
Open := { σ status = 'OPEN' (Orders) };
""");
Relation open = relix.relation("Open");
}
Declarations accumulate; relations are pinned
define(java.lang.String) adds statements to the session, and the next relation minted from it
is analysed against everything declared so far. A Relation, however,
captures the model it was created against: redefining Orders
afterwards does not change a relation already built over it.
That rule is what makes a relation a value rather than a view onto mutable state. A
relation whose meaning shifted under its holder could not be composed with confidence,
cached, or handed to another thread — and the alternative costs nothing, because
SemanticModel is already immutable.
It works with no database — deliberately, not by default
A source that declares its own schema needs nothing reachable, so a session over one composes, renders and optimises offline with no special mode.
A reference that can only be resolved by asking a database — a dotted
warehouse.orders, whose columns live in the catalog — is different. The analyser
reports it as an unresolved name and still returns a model, and this session
rejects that by default: the overwhelmingly common cause is a typo,
and a relation built over a name that resolves to nothing is a failure deferred to a
worse moment. Relix.Builder.allowUnresolved() takes the other reading, for the case
where composing and rendering against an unreachable database is the actual goal.
Thread safety
A session is not thread-safe: define(java.lang.String) mutates it, and analysis is cached.
The relations it hands out are safe to share, which is the point of pinning.
- Since:
- 1.0
-
Nested Class Summary
Nested Classes -
Method Summary
Modifier and TypeMethodDescriptionstatic Relix.Builderbuilder()Starts building a session.Introspects every connection-backed table this session names, and returns what comes back as a snapshot that can be written to a file and replayed later.voidclose()Releases what the session holds.connector(RelixConnector connector) Installs a connector of the caller's own, dispatched to by the type tokens ithandles.Adds declarations to the session, given as statements already built — withScriptBuilders, say — rather than as text.Adds declarations to the session, given as.relixtext.The session's declarations, rendered back as.relixtext.functions(FunctionLibrary library) Installs a library of the caller's own functions, discoverable to every relation the session analyses afterwards.ir()The session's intermediate representation — every symbol, its heading, and the expression tree behind each view.materialize(String name, Relation relation) Declares a relation from the rows another relation produces, computed now.model()The analysis of everything declared so far.static Relixopen()A session with no bindings.intHow many row streams this session has started and not seen closed.static ScriptParses.relixtext into aScript, without analysing it.static ScriptParses.relixtext into aScript, naming where it came from.referencePage(String path) The markdown of one reference page.static List<ReferencePage> Every page of the language reference: operators, joins, set operations, aggregates, predicates, literals, statements, and the guide pages.A relation over an already-built expression tree, for a caller composing withAstBuilders/Exprrather than with text.The relation named by, or written as,expression.Declares a relation whose rows the program produces on demand.The statements declared so far, in declaration order.Declares a relation from rows the program already holds, taking the heading from the first row.Declares a relation from rows the program already holds, under a stated heading.Splits.relixtext into classified tokens, for syntax highlighting.AnalysesrelixTextagainst the session without installing it, and reports what is wrong as data.static Stringversion()This engine's version, as the build stamped it:1.0.0, say.
-
Method Details
-
builder
Starts building a session.- Returns:
- a new builder
- Since:
- 1.0
-
version
This engine's version, as the build stamped it:1.0.0, say.The same value
relix.versionreports for the engine, for a program that wants it without running a query.- Returns:
- the version, or
unknownwhen the packaging carries none - Since:
- 1.0
-
referencePages
Every page of the language reference: operators, joins, set operations, aggregates, predicates, literals, statements, and the guide pages.Each entry says what the page is about and the words it can be looked up by; the markdown is
referencePage(String)of its path. The reference ships in this artifact, so a tool reads it with nothing else installed. A function's page is not here: it belongs to the library that offers the function, andFunctionCatalog.documentationserves it.- Returns:
- the pages, in the reference's own order; never null
- Since:
- 1.0
-
referencePage
The markdown of one reference page.- Parameters:
path- the page's path, asReferencePage.path()gives it, such asoperators/select.md; must not be null- Returns:
- the page, or empty if the reference has none at that path
- Since:
- 1.0
-
open
A session with no bindings.- Returns:
- a session with default settings
- Since:
- 1.0
-
define
Adds declarations to the session, given as.relixtext.Takes any statements the grammar accepts —
source,connection, a view assignment,def,relate, an inline table — and anamespace. Aquerystatement is a declaration of nothing, so it is rejected here; usescript(java.lang.String)for text that contains one.- Parameters:
relixText- the declarations; must not be null- Returns:
- this session, for chaining
- Throws:
RelixException- if the text does not parse, contains a query, or does not analyse against what is already declared- Since:
- 1.0
-
define
Adds declarations to the session, given as statements already built — withScriptBuilders, say — rather than as text.The typed counterpart of
define(String): a header value or a password is a plain Java string here, so there is nothing to escape. It does put the value in the session's declarations, wheredefinitions()andir()will show it. For a secret, prefer a${NAME}placeholder andRelix.Builder.placeholders, which keep the value out of the model altogether.- Parameters:
declarations- the statements; must not be null, and none may be a query- Returns:
- this session, for chaining
- Throws:
RelixException- if a statement is a query, or the declarations do not analyse against what is already declared- Since:
- 1.0
-
script
Adds every declaration inrelixTextand returns oneRelationperquerystatement, in source order.The whole-script counterpart of
define(java.lang.String)— what a caller reaches for when the text is a.relixfile rather than a fragment.- Parameters:
relixText- the script; must not be null- Returns:
- one relation per query statement, in order; empty when there are none
- Throws:
RelixException- if the text does not parse or does not analyse- Since:
- 1.0
-
relation
The relation named by, or written as,expression.Takes a relational expression in either spelling the language accepts —
"Open","σ status = 'OPEN' (Orders)","SELECT status = 'OPEN' (Orders)"— and resolves it against everything declared so far.- Parameters:
expression- the relational expression; must not be null- Returns:
- the relation, pinned to the session's current analysis
- Throws:
RelixException- if the expression does not parse or does not analyse- Since:
- 1.0
-
relation
A relation over an already-built expression tree, for a caller composing withAstBuilders/Exprrather than with text.- Parameters:
node- the expression; must not be null- Returns:
- the relation, pinned to the session's current analysis
- Throws:
RelixException- if the expression does not analyse- Since:
- 1.0
-
table
Declares a relation from rows the program already holds, under a stated heading."Here is my in-memory data, joined against my database table" is one of the reasons to embed a query engine at all, and this is the small end of it: reference data, a lookup table, fixtures, a result computed elsewhere.
The columns are the heading, in the order given. Each row is read by name, so the maps need no order of their own and
Map.ofis safe here. A column a row does not mention is NULL for that row — which is how an optional column is expressed — and a key naming no column is refused rather than silently dropped.Types are inferred, not declared. This becomes an inline table in the session, exactly as the same data written in a script would, so a column of numbers types as
NUMBERand everything else asSTRING. Where the types matter — a realTIMESTAMP, a large or lazily-produced relation — declare a source instead.- Parameters:
name- the relation name; must not be blankcolumns- the heading, in order; must not be null or empty, and must not name a column twicerows- the rows, each a column-name to value map; must not be null, and may be empty since the heading is stated- Returns:
- this session, for chaining
- Throws:
RelixException- if the columns are empty or repeat a name, if a row names a column the heading does not, or if the result does not analyse- Since:
- 1.0
-
table
Declares a relation from rows the program already holds, taking the heading from the first row.table(String, List, List)for a caller whose rows already carry their order — the short form, and the one to reach for when the maps are built rather than written out.The first row's map has to define an order for its keys, since that order becomes the relation's heading. A
LinkedHashMapdoes;Map.ofandHashMapdo not, andMap.of's iteration order is randomised per JVM, so a heading taken from one would differ between runs of the same program. A row of more than one entry that cannot say what its order is, is therefore refused rather than accepted arbitrarily — state the columns instead.Every row must carry the same columns: a relation has one heading, so a row that disagrees is a mistake rather than a sparse row. Values are converted with
Expr.lit, so aLong, anInstantor aBigDecimalmay be passed directly.Types are inferred, not declared — see
table(String, List, List).- Parameters:
name- the relation name; must not be blankrows- the rows, each a column-name to value map; must not be null or empty- Returns:
- this session, for chaining
- Throws:
RelixException- if the first row cannot say what its column order is, if the rows disagree about their columns, or if the result does not analyse- Since:
- 1.0
-
functions
Installs a library of the caller's own functions, discoverable to every relation the session analyses afterwards.The function SPI's argument is that anything the shipped library can do a third party can do; needing a
META-INF/servicesentry to exercise that inside one's own program would be a gap in the claim rather than a feature of it.Installed libraries are consulted before the discovered ones, so a name declared here wins.
- Parameters:
library- the library; must not be null- Returns:
- this session, for chaining
- Since:
- 1.0
-
source
Declares a relation whose rows the program produces on demand.The lazy counterpart to
table(java.lang.String, java.util.List<java.lang.String>, java.util.List<? extends java.util.Map<java.lang.String, ?>>): where that one takes rows already in memory and infers their types, this takes a declared heading and a supplier called once per scan — which is what anything large, expensive or genuinely streaming needs. A column typedTIMESTAMPhere is aTIMESTAMP, where an inline table could only have made it a string.Build the rows with
ArrayRow.of(schema, values), in the heading's column order. The stream is closed by the engine, so a supplier holding a resource should release it throughStream.onClose.The relation is finite as far as the engine is concerned, so the collecting terminals will read it to the end; a supplier whose stream never terminates should be consumed with
Relation.stream().- Parameters:
name- the relation name; must not be nullschema- the heading every produced row carries; must not be nullrows- called once per scan, producing that scan's rows; must not be null- Returns:
- this session, for chaining
- Throws:
RelixException- if the name is already taken, or the result does not analyse- Since:
- 1.0
-
materialize
Declares a relation from the rows another relation produces, computed now.The step a multi-stage program otherwise writes by hand: compute something expensive once, give it a name, and go on querying it like anything else the session declares. What is registered carries the relation's own heading, so a
TIMESTAMPcolumn is still aTIMESTAMP— which is the difference between this and draining the rows into maps and handing them totable(java.lang.String, java.util.List<java.lang.String>, java.util.List<? extends java.util.Map<java.lang.String, ?>>), where the types would be inferred back toNUMBERorSTRING.This is the one method on a session that executes. Everywhere else, building a relation runs nothing and a declaration is a declaration; this one drains its argument before it returns, because computing once and reusing the result is the whole point of it. A relation that provably never ends is refused for the reason
Relation.toList()refuses one.Nothing is written anywhere. The rows land in this session's memory and nowhere else, and a session that has gone out of scope has taken them with it.
- Parameters:
name- the relation name; must not be null, and must not already be takenrelation- the relation to compute; must not be null- Returns:
- this session, for chaining
- Throws:
RelixException- if the name is already taken, if the relation cannot be executed, or if the result does not analyseUnboundedRelationException- if the relation is provably unbounded, since collecting one would never return- Since:
- 1.0
-
connector
Installs a connector of the caller's own, dispatched to by the type tokens ithandles.A backend the program already knows how to read, reached without a
META-INF/servicesdeclaration — the same argumentfunctions(com.darkcollective.relix.function.FunctionLibrary)makes: the connector SPI's claim is that anything a shipped connector can do a third party can do, so needing a service declaration to exercise it inside one's own program would be a gap in the claim rather than a feature of it.Declare a
connectionof the handled type and the session's sources over it route here. An installed connector is consulted before the discovered ones, so it wins a token a shipped connector also claims.It is not closed with the session. The connector is the caller's, exactly as a bound
DataSourceis.- Parameters:
connector- the connector; must not be null- Returns:
- this session, for chaining
- Since:
- 1.0
-
model
The analysis of everything declared so far.Cached until the next
define(java.lang.String), so repeated calls on an unchanged session return the same model. ARelation's own model is not this one: it additionally carries the schema annotations for that relation's expression, so every relation has its own.- Returns:
- the semantic model; never null
- Throws:
RelixException- if what is declared does not analyse- Since:
- 1.0
-
ir
The session's intermediate representation — every symbol, its heading, and the expression tree behind each view.The counterpart to
definitions()for reading rather than round-tripping: that one renders what was declared, this one renders what the analyser made of it, which is where an inferred heading or a resolved reference becomes visible.- Returns:
- the IR report
- Throws:
RelixException- if what is declared does not analyse- Since:
- 1.0
-
captureCatalog
Introspects every connection-backed table this session names, and returns what comes back as a snapshot that can be written to a file and replayed later.The one call in this class that reaches a database on purpose. Everything else either runs a query or touches nothing; this asks each declared connection to describe its tables, so that a later session with nothing reachable can still cost a query rather than merely compose one.
Replay it by handing it back through
Relix.Builder.catalog: a snapshot is aCatalogProvider, so it goes exactly where the live one went.Files.writeString(path, live.captureCatalog().toJson()); // ... elsewhere, with no database in reach: Relix offline = Relix.builder() .catalog(CatalogSnapshot.parse(Files.readString(path))) .build();- Returns:
- the snapshot, empty when nothing could be introspected
- Throws:
RelixException- if the session is closed, or what is declared does not analyse- Since:
- 1.0
-
definitions
The session's declarations, rendered back as.relixtext.Round-trips: the text parses back to an equal script, which is what makes a session something a program can save, diff or hand to someone else. Formatting is normalised rather than preserved — comments are not in the AST, so no printer can return them.
- Returns:
- the declarations as text; empty when nothing is declared
- Since:
- 1.0
-
statements
The statements declared so far, in declaration order.- Returns:
- an immutable snapshot
- Since:
- 1.0
-
validate
AnalysesrelixTextagainst the session without installing it, and reports what is wrong as data.The counterpart to
define(java.lang.String)for a caller assembling text or trees from user input, where an error is an expected outcome to render rather than a bug to raise. Everything else in this class throws.- Parameters:
relixText- the text to check; must not be null- Returns:
- the diagnostics, empty when it analyses cleanly
- Since:
- 1.0
-
openStreams
public int openStreams()How many row streams this session has started and not seen closed.Zero between executions is the healthy state. A lazy stream owns the connector it reads through — and with it any pooled JDBC connection — so a caller who abandons one holds that connection until the session closes; this is the number that says so, and the only thing that can, since nothing else in a running program can see a stream nobody kept.
It counts executions rather than connections, so a stream over an inline table counts too: leaving one unclosed is the same mistake and shows up here before a database is added to the query.
Relation.toList()-shaped terminals never contribute — they close what they drain — so a non-zero reading is always aRelation.stream()result that got away.try (Relix relix = Relix.builder().build()) { // … the program's work … assert relix.openStreams() == 0; // no stream got away }- Returns:
- the number of unclosed row streams; never negative
- Since:
- 1.0
-
close
public void close()Releases what the session holds. BoundDataSources are the caller's, and are not closed.A row stream the caller never closed is closed here rather than left: its connector holds a borrowed connection, and a borrowed connection is exactly what closing the pool does not reach. How many there were is logged as a warning —
openStreams()is the same fact while the session is still open.- Specified by:
closein interfaceAutoCloseable- Since:
- 1.0
-
parse
Parses.relixtext into aScript, without analysing it.The tree is what
define(Statement...)takes and what aScriptLoaderreturns, so this is the method for a program that reads scripts itself: a loader serving files, or a tool that inspects a script's statements before deciding what to do with them. Nothing is resolved, so a script naming a relation nobody declared parses.- Parameters:
text- the script; must not be null- Returns:
- the parsed script; never null
- Throws:
ScriptParseException- if the text does not parse, carrying the line and column of the failure- Since:
- 1.0
-
parse
Parses.relixtext into aScript, naming where it came from.sourceis the name a failure and every node's location carry, such as the file's path, so an error in an imported file points at that file.- Parameters:
text- the script; must not be nullsource- the name of the text's origin; must not be null- Returns:
- the parsed script; never null
- Throws:
ScriptParseException- if the text does not parse, carrying the line and column of the failure- Since:
- 1.0
-
tokens
Splits.relixtext into classified tokens, for syntax highlighting.Lenient, because an editor asks while the user is still typing: a well-formed prefix is tokenized as usual, and whatever follows the point where the text stops making sense is one
TokenKind.INCOMPLETEtoken running to the end. Comments are tokens; whitespace is not. The tokens are in order and do not overlap, and offsets countchars, asString.charAt(int)does.This is lexical only. A token's kind says what it is on its own (
σis an operator,COUNTa keyword), not whether the text around it parses.- Parameters:
text- the text to tokenize; null is read as empty- Returns:
- the tokens; never null, possibly empty
- Since:
- 1.0
-