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
| Property | Value |
|---|---|
| JDBC URL | jdbc:elastic://localhost:9200 |
| Driver class | app.softnetwork.elastic.jdbc.ElasticDriver |
| Group ID | app.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.
| Elasticsearch | Artifact |
|---|---|
| ES 6.x | softclient4es6-jdbc-driver-0.3.3.jar |
| ES 7.x | softclient4es7-jdbc-driver-0.3.3.jar |
| ES 8.x | softclient4es8-jdbc-driver-0.3.3.jar |
| ES 9.x | softclient4es9-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
| Parameter | Description |
|---|---|
user | Username for basic authentication |
password | Password for basic authentication |
api-key | Elasticsearch API key |
bearer | OAuth/JWT bearer token |
scheme | Connection scheme (http or https, default: http) |
Examples
# Basic connectionjdbc:elastic://localhost:9200
# With authenticationjdbc:elastic://es.example.com:9200?user=elastic&password=changeme
# HTTPS with API keyjdbc:elastic://es.example.com:9243?scheme=https&api-key=your-api-keyJava 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_nameFROM jdbc_join_emp eJOIN 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 ASSELECT e.name, e.salary, d.dept_nameFROM jdbc_join_emp eJOIN 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_targetSELECT e.emp_id, e.name, e.salary, d.dept_nameFROM jdbc_join_emp eJOIN jdbc_join_dept d ON e.dept_id = d.dept_idON CONFLICT (emp_id) DO UPDATE;Note:
CREATE TABLE … AS … ON CONFLICTandINSERT … ON CONFLICT … DO NOTHINGare rejected at the JDBC boundary — useINSERT … ON CONFLICT … DO UPDATEfor 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 rowstry (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-UNNAMEDThat 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-UNNAMEDto 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
SQLExceptionnaming 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. UsesetNullto bind SQLNULLdeliberately.
PreparedStatement ps = conn.prepareStatement("SELECT * FROM demo WHERE name = ?");ps.setString(1, "O'Brien"); // renders 'O\'Brien' — one literal, quotes and allResultSet 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:
| Setter | Why |
|---|---|
setBinaryStream, setBlob, setClob, setNClob | the grammar has no binary literal and no LOB locator; use setBytes, which renders Base64 text |
setRef, setRowId, setSQLXML, setURL | no literal form in the grammar |
setUnicodeStream | deprecated 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
- Open Database > New Database Connection
- Choose Driver Manager > New
- Set Driver Name:
SoftClient4ES - Add the fat JAR file
- Set Driver Class:
app.softnetwork.elastic.jdbc.ElasticDriver - Set URL Template:
jdbc:elastic://{host}:{port} - Create a new connection using this driver
See the DBeaver guide for detailed setup instructions.
IntelliJ DataGrip
- Open Database > + > Driver
- Add the fat JAR
- Set Driver Class:
app.softnetwork.elastic.jdbc.ElasticDriver - Create a new data source with URL
jdbc:elastic://localhost:9200
Version compatibility
| Driver | Scala | ES versions | Clients | Process model |
|---|---|---|---|---|
| JDBC | published for Scala 2.13 | ES 6.x / 7.x / 8.x / 9.x | Java/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
- Cross-Index JOIN walkthrough — the full JOIN matrix (rows 1/2/3) with worked examples.
- Multi-cluster federation operator guide — the Pro+ path: JOIN across separate ES clusters.
- Known Limitations & Roadmap — what works in the current release vs what’s coming.
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.