java.lang.Object
com.darkcollective.relix.embed.Relix
All Implemented Interfaces:
AutoCloseable

public final class Relix extends Object implements AutoCloseable
A Relix session — the environment relations resolve against, and the entry point to the embedding API.

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
  • Method Details

    • builder

      public static Relix.Builder builder()
      Starts building a session.
      Returns:
      a new builder
      Since:
      1.0
    • version

      public static String version()
      This engine's version, as the build stamped it: 1.0.0, say.

      The same value relix.version reports for the engine, for a program that wants it without running a query.

      Returns:
      the version, or unknown when the packaging carries none
      Since:
      1.0
    • referencePages

      public static List<ReferencePage> 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, and FunctionCatalog.documentation serves it.

      Returns:
      the pages, in the reference's own order; never null
      Since:
      1.0
    • referencePage

      public static Optional<String> referencePage(String path)
      The markdown of one reference page.
      Parameters:
      path - the page's path, as ReferencePage.path() gives it, such as operators/select.md; must not be null
      Returns:
      the page, or empty if the reference has none at that path
      Since:
      1.0
    • open

      public static Relix open()
      A session with no bindings.
      Returns:
      a session with default settings
      Since:
      1.0
    • define

      public Relix define(String relixText)
      Adds declarations to the session, given as .relix text.

      Takes any statements the grammar accepts — source, connection, a view assignment, def, relate, an inline table — and a namespace. A query statement is a declaration of nothing, so it is rejected here; use script(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

      public Relix define(Statement... declarations)
      Adds declarations to the session, given as statements already built — with ScriptBuilders, 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, where definitions() and ir() will show it. For a secret, prefer a ${NAME} placeholder and Relix.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

      public List<Relation> script(String relixText)
      Adds every declaration in relixText and returns one Relation per query statement, in source order.

      The whole-script counterpart of define(java.lang.String) — what a caller reaches for when the text is a .relix file 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

      public Relation relation(String expression)
      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

      public Relation relation(RelNode node)
      A relation over an already-built expression tree, for a caller composing with AstBuilders/Expr rather 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

      public Relix table(String name, List<String> columns, List<? extends Map<String,?>> rows)
      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.of is 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 NUMBER and everything else as STRING. Where the types matter — a real TIMESTAMP, a large or lazily-produced relation — declare a source instead.

      Parameters:
      name - the relation name; must not be blank
      columns - the heading, in order; must not be null or empty, and must not name a column twice
      rows - 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

      public Relix table(String name, List<? extends Map<String,?>> rows)
      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 LinkedHashMap does; Map.of and HashMap do not, and Map.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 a Long, an Instant or a BigDecimal may be passed directly.

      Types are inferred, not declared — see table(String, List, List).

      Parameters:
      name - the relation name; must not be blank
      rows - 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

      public Relix functions(FunctionLibrary library)
      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/services entry 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

      public Relix source(String name, Schema schema, Supplier<Stream<Row>> rows)
      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 typed TIMESTAMP here is a TIMESTAMP, 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 through Stream.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 null
      schema - the heading every produced row carries; must not be null
      rows - 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

      public Relix materialize(String name, Relation relation)
      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 TIMESTAMP column is still a TIMESTAMP — which is the difference between this and draining the rows into maps and handing them to table(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 to NUMBER or STRING.

      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 taken
      relation - 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 analyse
      UnboundedRelationException - if the relation is provably unbounded, since collecting one would never return
      Since:
      1.0
    • connector

      public Relix connector(RelixConnector connector)
      Installs a connector of the caller's own, dispatched to by the type tokens it handles.

      A backend the program already knows how to read, reached without a META-INF/services declaration — the same argument functions(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 connection of 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 DataSource is.

      Parameters:
      connector - the connector; must not be null
      Returns:
      this session, for chaining
      Since:
      1.0
    • model

      public SemanticModel 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. A Relation'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

      public String 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

      public CatalogSnapshot 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 a CatalogProvider, 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

      public String definitions()
      The session's declarations, rendered back as .relix text.

      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

      public List<Statement> statements()
      The statements declared so far, in declaration order.
      Returns:
      an immutable snapshot
      Since:
      1.0
    • validate

      public List<Diagnostic> validate(String relixText)
      Analyses relixText against 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 a Relation.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. Bound DataSources 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:
      close in interface AutoCloseable
      Since:
      1.0
    • parse

      public static Script parse(String text)
      Parses .relix text into a Script, without analysing it.

      The tree is what define(Statement...) takes and what a ScriptLoader returns, 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

      public static Script parse(String text, String source)
      Parses .relix text into a Script, naming where it came from.

      source is 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 null
      source - 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

      public static List<Token> tokens(String text)
      Splits .relix text 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.INCOMPLETE token running to the end. Comments are tokens; whitespace is not. The tokens are in order and do not overlap, and offsets count chars, as String.charAt(int) does.

      This is lexical only. A token's kind says what it is on its own (σ is an operator, COUNT a 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