Class Relix.Builder

java.lang.Object
com.darkcollective.relix.embed.Relix.Builder
Enclosing class:
Relix

public static final class Relix.Builder extends Object
Assembles a Relix session.

Deliberately short. A knob here is one every embedder has to read past, so the bar is that it cannot be expressed as a declaration or a relation.

Since:
1.0
  • Method Details

    • placeholders

      public Relix.Builder placeholders(Function<String,Optional<String>> resolver)
      Supplies the values of the ${NAME} placeholders in source and connection declarations: a bearer token, an API key, a database password or URL.
      Relix relix = Relix.builder()
              .placeholders(name -> Optional.ofNullable(System.getenv(name)))
              .build();
      relix.define("""
              source Orders from http {
                  url:     "https://api.example.com/orders",
                  headers: { "Authorization": "Bearer ${ORDERS_TOKEN}" }
              };
              """);
      

      The placeholder stays in the declaration. It is resolved each time a query runs, for the declarations that query reaches, and the value goes to the connector for that run alone. So the value is never written into the text a script is parsed from, and nothing that prints the session — Relix.definitions(), Relix.ir(), an analysis error — shows it. Because it is asked for on each run, a rotated credential is picked up without redefining anything.

      Only string values are resolved: URLs, paths, table names, header values, request bodies, credentials, column defaults and connection properties. Each name is asked for at most once per run, and only for the declarations the query reaches. Analysis asks too, when it introspects a connection's tables. When the resolver returns empty for a name a query needs, the query fails with an error naming the placeholder and the source or connection holding it, rather than sending the text ${NAME}. A session built without a resolver leaves placeholders as written.

      Parameters:
      resolver - the value of each placeholder name, or empty when it has none; must not be null
      Returns:
      this builder, for chaining
      Since:
      1.0
    • remoteFiles

      public Relix.Builder remoteFiles(boolean allow)
      Says whether a file connection may name its file by an https URL.

      Allowed by default. A url: is something the script asked for, like an HTTP source's request, so reaching for it is not a surprise. It does put bytes on disk inside an embedded process: a fetched file is cached under ~/.relix/cache/files and reused by later sessions when the server says it has not changed. A session that must not do that says so here, and a connection naming a URL is then an error rather than a download.

      Within one session a URL is fetched at most once, and every scan reads that copy, so a file that changes on the server does not change under a running query.

      Parameters:
      allow - whether an https file may be fetched
      Returns:
      this builder, for chaining
      Since:
      1.0
    • jdbc

      public Relix.Builder jdbc(String name, DataSource dataSource)
      Binds a connection name to a live handle, so a script may reference its tables by dotted name without declaring a URL.
      Parameters:
      name - the connection name; must not be blank
      dataSource - the live handle; must not be null
      Returns:
      this builder
      Since:
      1.0
    • jdbc

      public Relix.Builder jdbc(String name, DataSource dataSource, String dialect)
      Binds a connection name to a live handle with an explicit dialect, skipping the metadata probe.
      Parameters:
      name - the connection name; must not be blank
      dataSource - the live handle; must not be null
      dialect - the dialect token (e.g. "postgres"); must not be null
      Returns:
      this builder
      Since:
      1.0
    • catalog

      public Relix.Builder catalog(CatalogProvider catalog)
      Supplies table schemas and statistics, replacing live introspection.
      Parameters:
      catalog - the catalog provider; must not be null
      Returns:
      this builder
      Since:
      1.0
    • baseDirectory

      public Relix.Builder baseDirectory(Path directory)
      Sets the directory a source's relative path resolves against.

      source Orders from csv("./orders.csv") names a file relative to something, and in a script that something is the script's own directory. An embedded session has no script, so it is this — the working directory unless said otherwise, which is what a program run from its own root expects.

      Parameters:
      directory - the base directory; must not be null
      Returns:
      this builder
      Since:
      1.0
    • clock

      public Relix.Builder clock(Clock clock)
      Fixes the clock the session's temporal functions read, so NOW() is reproducible.
      Parameters:
      clock - the clock; must not be null
      Returns:
      this builder
      Since:
      1.0
    • functions

      public Relix.Builder functions(FunctionLibrary library)
      Installs a library of the caller's own functions before the session opens.
      Parameters:
      library - the library; must not be null
      Returns:
      this builder
      Since:
      1.0
    • connector

      public Relix.Builder connector(RelixConnector connector)
      Installs a connector of the caller's own, as Relix.connector(com.darkcollective.relix.processor.connector.RelixConnector) does, for a session declared in one expression.
      Parameters:
      connector - the connector; must not be null
      Returns:
      this builder, for chaining
      Since:
      1.0
    • maxFixpointRounds

      public Relix.Builder maxFixpointRounds(int rounds)
      Caps how many rounds a recursive FIX may iterate before the engine stops it.

      The safety valve for a recursion that does not converge — a counting semiring over a cyclic graph is the standard case, where the fixpoint is genuinely infinite rather than merely slow. Unbounded by default, because a cap that fires is an answer that is silently incomplete and that should be a thing a caller asked for.

      Parameters:
      rounds - the maximum iterations; must be at least 1
      Returns:
      this builder, for chaining
      Since:
      1.0
    • maxMaterializedRows

      public Relix.Builder maxMaterializedRows(int rows)
      Caps how many rows a single blocking operator may buffer before the engine stops it.

      A blocking operator — γ, τ, a deduplicating set operation, ÷, a full outer or hash join's build side — cannot emit a row until it has read its whole input, so it holds that input in the JVM heap. Nothing in the plan says how large that is. toList() refuses an unbounded relation, and so does the planner for a blocking operator over one, but a bounded table can be far larger than the heap: the query then dies with an OutOfMemoryError attributable to no operator in particular. Under a cap the query stops instead with an error naming the operator that was buffering and the limit it crossed — which is a thing a caller can act on.

      The cap is per operator, not per session total — the shape maxFixpointRounds(int) has, which likewise bounds one fixpoint rather than a run's total rounds. A running total would refuse a recursion that buffers the same thousand rows each round while its peak memory never moves. So a plan with many blocking operators can exceed it in aggregate while no single operator does; what it converts is the failure where one operator swallows a table.

      Unbounded by default, because a cap that fires turns a slow answer into no answer, and that should be a thing a caller asked for.

      Parameters:
      rows - the maximum rows one blocking operator may buffer; must be at least 1
      Returns:
      this builder, for chaining
      Since:
      1.0
    • maxProcessedRows

      public Relix.Builder maxProcessedRows(long rows)
      Caps the work one execution may do, counted as rows passed from one operator to the next.

      Every row an operator hands to the operator above it is charged, so the count is the sum of every operator's output. It stops the queries a cap on buffering or on output cannot: a selection over an endless generator that matches nothing, which never yields a row, and an aggregate over a large product, which holds nothing and yields one. It is a total for the whole execution, recursion included, because what it bounds is work rather than memory. The same query over the same data charges the same count on any machine.

      Unbounded by default. A query that crosses it fails with a QueryExecutionException naming the limit.

      Parameters:
      rows - the most rows one execution may process; must be at least 1
      Returns:
      this builder, for chaining
      Since:
      1.0
    • timeout

      public Relix.Builder timeout(Duration timeout)
      Limits how long one execution may run.

      The clock starts when a query starts executing, and is checked as rows pass between operators. That reaches every loop in the engine that moves rows, but not a call that does not return to the engine: a JDBC driver waiting on its database, or a solver's search. Unlike maxProcessedRows(long) the outcome depends on the machine, so prefer that limit where the answer should be reproducible, and use this one as a backstop.

      Unlimited by default. A query that crosses it fails with a QueryExecutionException naming the limit.

      Parameters:
      timeout - the longest one execution may run; must be positive
      Returns:
      this builder, for chaining
      Since:
      1.0
    • provisioners

      public Relix.Builder provisioners(DriverProvisioner drivers, ConnectorProvisioner connectors)
      Supplies the provisioners that fetch a missing JDBC driver or connector plugin.

      A session provisions nothing by default, and that is a decision rather than an oversight: a library call that reached out to the network and wrote a jar into the user's home directory would be a surprise an embedder cannot see coming, and a driver an embedded program needs is a dependency of that program.

      An application is a different matter — it can ask, report progress and be told no — so one that wants the behaviour states it here, which is where the decision is visible.

      Parameters:
      drivers - the JDBC driver provisioner; must not be null
      connectors - the connector-plugin provisioner; must not be null
      Returns:
      this builder, for chaining
      Since:
      1.0
    • scriptLoader

      public Relix.Builder scriptLoader(ScriptLoader loader)
      Supplies where an import statement resolves from.

      A session serves imports from nothing by default: it is assembled in memory and has no file of its own, so there is no directory for a relative path to be relative to. An application that reads scripts from disk supplies FileSystemScriptLoader; one that holds them already can serve them from a map, which is what the seam is for.

      Parameters:
      loader - the loader; must not be null
      Returns:
      this builder, for chaining
      Since:
      1.0
    • relationships

      public Relix.Builder relationships(SchemaGraph graph)
      Supplies relationships beyond the ones the session declares.

      Where an edge comes from that a relate statement did not: a host that has observed one — two relations repeatedly joined on the same columns — can offer it here, and the analyser resolves a later join the same way it would resolve a declared one.

      They are supplemental rather than authoritative: a declared edge wins, and nothing here changes what a query means, only what the engine can infer when a query does not say.

      Parameters:
      graph - the additional edges; must not be null
      Returns:
      this builder, for chaining
      Since:
      1.0
    • sessionEvents

      public Relix.Builder sessionEvents(List<QueryEvent> events)
      Supplies the extent of the relix.events catalog relation.

      A statement cannot observe itself — its events postdate the analysis that would have to register them — so what a query selecting from relix.events sees is a feed some earlier run produced, and this is where a host that kept one hands it back. A session given none correctly reports no events rather than pretending to none.

      Parameters:
      events - the feed; must not be null
      Returns:
      this builder, for chaining
      Since:
      1.0
    • sandbox

      public Relix.Builder sandbox(Sandbox sandbox)
      Restricts what this session's users may reach, and how much one query may do.

      Sessions are open by default. A closed sandbox accepts internal declarations (inline tables, views, functions) from Relix.define(String) and Relix.script(String), but an external one (a file, database or HTTP source, a connection, an import) only when the sandbox's own declarations contain the same one. It also caps input length and result size. See Sandbox.

      Relix relix = Relix.builder()
              .sandbox(Sandbox.load(Path.of("sandbox.json")))
              .build();
      
      Parameters:
      sandbox - the sandbox; must not be null
      Returns:
      this builder, for chaining
      Since:
      1.0
    • allowUnresolved

      public Relix.Builder allowUnresolved()
      Accepts relations whose names the analyser could not resolve, instead of rejecting them.

      What makes offline work full-strength for a reference that needs a catalog: warehouse.orders composes, renders and optimises with the database unreachable, at the cost of the weaker plan an unknown schema implies.

      Off by default, because the same signal is what a typo produces. Turning it on trades an error at composition time for one at execution time, which is the right trade only when composing is the whole intent — tuning a query in CI, on a machine with no access to production.

      Returns:
      this builder
      Since:
      1.0
    • build

      public Relix build()
      Builds the session.
      Returns:
      the session
      Since:
      1.0