Skip to content

JDBC Driver

SoftClient4ES provides a JDBC Type 4 driver that lets you connect any JDBC-compatible tool to Elasticsearch — DBeaver, IntelliJ DataGrip, Apache Superset, Tableau, and custom Java/Scala applications.

Connection Details

PropertyValue
JDBC URLjdbc:elastic://localhost:9200
Driver classapp.softnetwork.elastic.jdbc.ElasticDriver
Group IDapp.softnetwork.elastic

Driver JARs

Download the self-contained fat JAR for your Elasticsearch version. The JARs are Scala-version-independent and include all required dependencies.

ElasticsearchArtifact
ES 6.xsoftclient4es6-jdbc-driver-0.3.3.jar
ES 7.xsoftclient4es7-jdbc-driver-0.3.3.jar
ES 8.xsoftclient4es8-jdbc-driver-0.3.3.jar
ES 9.xsoftclient4es9-jdbc-driver-0.3.3.jar

The driver requires Java 11+ (17+ for ES 9.x) — the embedded JOIN engine is built on Apache Arrow 18.x, which ships Java-11 bytecode. See Cross-Index JOIN.

Build Tool Integration

Maven

<dependency>
<groupId>app.softnetwork.elastic</groupId>
<artifactId>softclient4es8-jdbc-driver</artifactId>
<version>0.3.3</version>
</dependency>

Gradle

implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.3.3'

sbt

libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.3.3"

JDBC URL Format

jdbc:elastic://host:port[?param=value&...]

Authentication Parameters

ParameterDescription
userUsername for basic authentication
passwordPassword for basic authentication
api-keyElasticsearch API key
bearerOAuth/JWT bearer token
schemeConnection scheme (http or https, default: http)

Examples

# Basic connection
jdbc:elastic://localhost:9200
# With authentication
jdbc:elastic://es.example.com:9200?user=elastic&password=changeme
# HTTPS with API key
jdbc:elastic://es.example.com:9243?scheme=https&api-key=your-api-key

Java Example

import java.sql.*;
String url = "jdbc:elastic://localhost:9200";
try (Connection conn = DriverManager.getConnection(url)) {
try (Statement stmt = conn.createStatement()) {
// DDL
stmt.execute("CREATE TABLE IF NOT EXISTS demo (id INT, name VARCHAR, PRIMARY KEY (id))");
// DML
stmt.execute("INSERT INTO demo (id, name) VALUES (1, 'Alice'), (2, 'Bob')");
// DQL
try (ResultSet rs = stmt.executeQuery("SELECT * FROM demo ORDER BY id")) {
while (rs.next()) {
System.out.printf("id=%d, name=%s%n",
rs.getInt("id"), rs.getString("name"));
}
}
// Cleanup
stmt.execute("DROP TABLE IF EXISTS demo");
}
}

Your first query

A plain SELECT is the fastest way to confirm the connection works:

SELECT * FROM my_index LIMIT 10;
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM my_index LIMIT 10")) {
while (rs.next()) {
System.out.println(rs.getString(1));
}
}

Your first JOIN

Elasticsearch SQL can’t JOIN across indices — SoftClient4ES does. Free in Community: up to 2 cross-index JOINs per query (a 3-table JOIN). The JOIN runs in an embedded DuckDB engine; your existing single ES cluster needs no extra infrastructure.

A cross-index INNER JOIN over two ES indices — jdbc_join_emp (employees) and jdbc_join_dept (departments):

SELECT e.name, e.salary, d.dept_name
FROM jdbc_join_emp e
JOIN jdbc_join_dept d ON e.dept_id = d.dept_id;
-- 5 rows (the orphan employee with dept_id = 99 is dropped by the INNER JOIN)

Denormalize the joined result into a brand-new index with CREATE TABLE … AS SELECT (CTAS):

CREATE TABLE jdbc_row1_ctas_join_target AS
SELECT e.name, e.salary, d.dept_name
FROM jdbc_join_emp e
JOIN jdbc_join_dept d ON e.dept_id = d.dept_id;

Upsert the joined rows into an existing index idempotently with INSERT … ON CONFLICT (col) DO UPDATE:

INSERT INTO jdbc_row1_insert_join_upsert_target
SELECT e.emp_id, e.name, e.salary, d.dept_name
FROM jdbc_join_emp e
JOIN jdbc_join_dept d ON e.dept_id = d.dept_id
ON CONFLICT (emp_id) DO UPDATE;

Note: CREATE TABLE … AS … ON CONFLICT and INSERT … ON CONFLICT … DO NOTHING are rejected at the JDBC boundary — use INSERT … ON CONFLICT … DO UPDATE for idempotent upserts.

Bind parameters through a JOIN with a PreparedStatement:

PreparedStatement ps = conn.prepareStatement(
"SELECT e.name, d.dept_name " +
"FROM jdbc_join_emp e " +
"JOIN jdbc_join_dept d ON e.dept_id = d.dept_id " +
"WHERE e.salary > ?");
ps.setDouble(1, 3500.0); // lower threshold → more rows
try (ResultSet rs = ps.executeQuery()) { /* … */ }
ps.setDouble(1, 6500.0); // higher threshold → fewer rows (Carol = 8000 survives)
try (ResultSet rs = ps.executeQuery()) { /* … */ }

For the full JOIN matrix — passthrough, cross-cluster conveyor, and the multi-source coordinator — see the Cross-Index JOIN walkthrough.

Cross-index JOIN on JDK 16 and later

The JOIN engine is built on Apache Arrow, which reaches into java.nio to address off-heap memory. JDK 16 and later deny that access by default (JEP 396), so on a modern JVM the cross-index JOIN — and only the cross-index JOIN — needs one flag:

--add-opens=java.base/java.nio=ALL-UNNAMED

That one flag is the whole requirement: it is verified by a test that runs Arrow with that flag and no other module open. Everything else in the driver (queries, DDL, DML, SHOW, metadata browsing) works without it.

This matters in practice because a BI tool starts its own JVM and chooses its own flags — Tableau bundles Zulu 17.

Below JDK 16 no flag is needed, but the floor is JDK 11: the Arrow release this driver bundles is compiled for Java 11, so JDK 8, 9 and 10 cannot run the JOIN engine at all.

If the flag is missing, the driver normally refuses the JOIN immediately with a message naming the flag, rather than running the query and failing later:

Cross-index JOIN is unavailable in this JVM: Apache Arrow cannot reach its off-heap memory layer because JDK 16 and later deny the reflective access it needs. Add --add-opens=java.base/java.nio=ALL-UNNAMED to the JVM’s startup arguments. …

(“Normally” because the check is deliberately fail-open: a JVM it cannot read confidently is allowed to try, and Apache Arrow’s own error is reported if it then fails.)

The same flag can also be supplied to a JVM you do not launch through the JAVA_TOOL_OPTIONS environment variable, or through whatever mechanism the host provides for its own JVM arguments. The per-tool procedure has not yet been verified for any specific BI tool, so this page does not print one — the flag and the constraint are what is documented until it has been.

Apache Arrow’s own message, if you see it in a log, names a longer form of the same flag:

Failed to initialize MemoryUtil. You must start Java with
`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
(See https://arrow.apache.org/docs/java/install.html)

Both work here. Arrow names its own module first, which matters when Arrow is on the module path; this driver ships as a classpath fat JAR, where ALL-UNNAMED alone is what is needed and is the shorter thing to type.

Prepared statements

Elasticsearch has no server-side prepared statements, so the driver substitutes bound parameters into the statement text on the client, before the engine parses it. That model decides what can and cannot be bound:

  • Values are escaped; identifiers are not bindable. Every value is rendered as a SQL literal using the engine’s own escaping, so a bound value becomes exactly one literal — or, for setArray, a list of literals whose commas and parentheses the driver writes. It never becomes anything else. No setter injects a table or column name.
  • A ? is only a placeholder outside quotes and comments. WHERE label = 'what?' AND name = ? has exactly one parameter.
  • One statement per PreparedStatement. A template containing more than one statement is refused when it is prepared. That is the JDBC contract, and it is also what makes the length bound below true: with more than one statement the engine parses on a different thread whose stack the driver does not control.
  • Parameters are length-bounded. A bound value may render to at most 8192 characters (after escaping — an apostrophe or a backslash contributes two). Above that the driver raises a SQLException naming the limit rather than building a statement the engine’s parser cannot read. Store long text in the document and filter on a shorter key.
  • A parameter you never set is an error, not an implicit NULL, and an index with no matching placeholder is refused at set time. Use setNull to bind SQL NULL deliberately.
PreparedStatement ps = conn.prepareStatement("SELECT * FROM demo WHERE name = ?");
ps.setString(1, "O'Brien"); // renders 'O\'Brien' — one literal, quotes and all
ResultSet rs = ps.executeQuery();

Binding a list with setArray

An array is renderable only where the grammar accepts a value list — the IN position:

PreparedStatement ps = conn.prepareStatement("SELECT * FROM demo WHERE id IN (?)");
ps.setArray(1, conn.createArrayOf("VARCHAR", new Object[] { "a", "b" }));

which becomes:

SELECT * FROM demo WHERE id IN ('a','b')

... IN ? (without the parentheses) works too — the driver adds them. Anywhere else, and for a shape the grammar rejects, setArray raises a SQLException that says which: an empty array, an array containing NULL, an array of booleans, an array mixing text and numbers, and an array mixing whole numbers and decimals. Bind a homogeneous, non-empty array of text or numbers.

Character streams

setAsciiStream, setCharacterStream and setNCharacterStream are supported and read into the statement text under the same 8192-character bound; longer content raises a SQLException rather than being silently truncated. The overloads that take a length bind exactly that many characters, and raise a SQLException if the stream ends sooner.

Permanently unsupported setters

These are not “not yet” — under a text-substitution model there is no correct rendering for them, so they throw SQLFeatureNotSupportedException and always will:

SetterWhy
setBinaryStream, setBlob, setClob, setNClobthe grammar has no binary literal and no LOB locator; use setBytes, which renders Base64 text
setRef, setRowId, setSQLXML, setURLno literal form in the grammar
setUnicodeStreamdeprecated by the JDBC specification itself

setObject also refuses a raw array, a collection, a Reader or an InputStream: those have no literal form, and rendering their toString would produce a query that runs and silently matches nothing. Use createArrayOf plus setArray, or the stream setters.

Supported SQL

The JDBC driver supports the full SQL Gateway syntax:

  • DDL — CREATE/ALTER/DROP TABLE, pipelines, watchers, enrich policies
  • DML — INSERT, UPDATE, DELETE, COPY INTO
  • DQL — SELECT with WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, UNION ALL, JOIN UNNEST, window functions
  • SHOW/DESCRIBE — Tables, pipelines, watchers, enrich policies

BI Tool Setup

DBeaver

  1. Open Database > New Database Connection
  2. Choose Driver Manager > New
  3. Set Driver Name: SoftClient4ES
  4. Add the fat JAR file
  5. Set Driver Class: app.softnetwork.elastic.jdbc.ElasticDriver
  6. Set URL Template: jdbc:elastic://{host}:{port}
  7. Create a new connection using this driver

See the DBeaver guide for detailed setup instructions.

IntelliJ DataGrip

  1. Open Database > + > Driver
  2. Add the fat JAR
  3. Set Driver Class: app.softnetwork.elastic.jdbc.ElasticDriver
  4. Create a new data source with URL jdbc:elastic://localhost:9200

Version compatibility

DriverScalaES versionsClientsProcess model
JDBCpublished for Scala 2.13ES 6.x / 7.x / 8.x / 9.xJava/JVM + any JDBC tool (DBeaver, DataGrip, Superset)In-process

The fat JARs are Scala-version-independent for consumers — they bundle their own Scala runtime, so a Java application (or a Scala 2.12 project) can use the driver without matching Scala versions. You almost never need to think about the Scala axis.

Licensing & self-selection

All client drivers (JDBC, ADBC, and the REPL) plus the Arrow Flight SQL sidecar are free in Community, including up to 2 cross-index JOINs per query (a 3-table JOIN). A 4-table JOIN (a 3rd cross-index JOIN in one query) is rejected by the planner with a message ending … Upgrade to Pro … See: https://portal.softclient4es.com/pricing.

Multi-cluster federation — joining across separate ES clusters — is Pro+ (maxClusters 1 / 5 / ∞). These quickstarts cover the single-cluster shape, which is free. See the pricing & licensing page for the full quota matrix.

What does NOT work yet

Subqueries (IN (SELECT …), EXISTS, scalar, derived tables) and CTEs (WITH) are not supported in the current release — they arrive in a later release. Write the JOIN explicitly instead. See Known Limitations & Roadmap for the full list.

Going further

Telemetry

The JDBC driver sends one anonymous usage ping per day (no IP, no SQL, no PII). Opt out either on the JDBC URL — jdbc:elastic://localhost:9200?telemetry=false — or with softclient4es.telemetry.enabled = false on your application classpath. See Telemetry & Privacy for the full field list and per-surface details.

Known limitations

Subqueries, CTEs (WITH), and set operators beyond UNION ALL are not in this release — and some BI tools auto-generate them. See Known Limitations & Roadmap for exactly what works today, what’s coming in the next release (Quarter 4 2026), and the per-tool workaround.

License

The JDBC driver is licensed under the Elastic License 2.0 — free to use, not open source.