Skip to content
85 changes: 52 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,10 @@ Download the self-contained fat JAR for your Elasticsearch version:

| Elasticsearch Version | Artifact |
|-----------------------|----------------------------------------|
| ES 6.x | `softclient4es6-jdbc-driver-0.1.0.jar` |
| ES 7.x | `softclient4es7-jdbc-driver-0.1.0.jar` |
| ES 8.x | `softclient4es8-jdbc-driver-0.1.0.jar` |
| ES 9.x | `softclient4es9-jdbc-driver-0.1.0.jar` |
| ES 6.x | `softclient4es6-jdbc-driver-0.1.1.jar` |
| ES 7.x | `softclient4es7-jdbc-driver-0.1.1.jar` |
| ES 8.x | `softclient4es8-jdbc-driver-0.1.1.jar` |
| ES 9.x | `softclient4es9-jdbc-driver-0.1.1.jar` |

```text
JDBC URL: jdbc:elastic://localhost:9200
Expand All @@ -184,30 +184,58 @@ Driver class: app.softnetwork.elastic.jdbc.ElasticDriver

### Maven / Gradle / sbt

**Maven:**

```xml
<dependency>
<groupId>app.softnetwork.elastic</groupId>
<artifactId>softclient4es8-jdbc-driver</artifactId>
<version>0.1.0</version>
<version>0.1.1</version>
</dependency>
```

**Gradle:**

```groovy
implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.1.1'
```

**sbt:**

```scala
libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.1.1"
```

The JDBC driver JARs are Scala-version-independent (no `_2.12` or `_2.13` suffix) and include all required dependencies.

---

## 🛠️ Scala Library Integration

For programmatic access, add SoftClient4ES to your project:
For programmatic access, add SoftClient4ES to your project.

### Client Library Matrix

| Elasticsearch | Artifact | Scala | JDK |
|----------------|------------------------------|------------|------|
| 6.x | `softclient4es6-jest-client` | 2.12, 2.13 | 8+ |
| 6.x | `softclient4es6-rest-client` | 2.12, 2.13 | 8+ |
| 7.x | `softclient4es7-rest-client` | 2.12, 2.13 | 8+ |
| 8.x | `softclient4es8-java-client` | 2.12, 2.13 | 8+ |
| 9.x | `softclient4es9-java-client` | 2.13 only | 17+ |

### sbt Setup

```scala
// build.sbt
resolvers += "Softnetwork" at "https://softnetwork.jfrog.io/artifactory/releases/"

// Choose your Elasticsearch version
libraryDependencies += "app.softnetwork.elastic" %% "softclient4es8-java-client" % "0.17.4"
libraryDependencies += "app.softnetwork.elastic" %% "softclient4es8-java-client" % "0.18.0"
// Add the community extensions for materialized views (optional)
libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-community-extensions" % "0.1.1"
// Add the JDBC driver if you want to use it from Scala (optional)
libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-jdbc-driver" % "0.1.1"
```

```scala
Expand Down Expand Up @@ -286,34 +314,25 @@ Seamlessly sync event-sourced systems with Elasticsearch.

## 📦 Editions and Licensing

SoftClient4ES is available in two editions:

### Community Edition (Open Source)

Licensed under the **Apache License 2.0**. Includes the core SQL engine, REPL client, Scala library, and the community extensions library with limited materialized views support:

| Feature | Community |
|--------------------------------------------------------------------|-------------|
| Full SQL DDL (CREATE, ALTER, DROP TABLE) | Yes |
| Full SQL DML (INSERT, UPDATE, DELETE, COPY INTO) | Yes |
| Full SQL DQL (SELECT, JOIN UNNEST, aggregations, window functions) | Yes |
| Pipelines, Watchers, Enrich Policies | Yes |
| Interactive REPL client | Yes |
| Scala library (Akka Streams) | Yes |
| Community extensions library (Scala) | Yes |
| Materialized Views (CREATE, REFRESH, DESCRIBE) | Yes (max 3) |
| Elasticsearch 6, 7, 8, 9 support | Yes |
SoftClient4ES uses a dual-license model:

### Pro / Enterprise Edition (Commercial)
- **Core** (SQL engine, REPL client, Scala library) — **Apache License 2.0** (open source)
- **JDBC Driver** and **Materialized Views** — **Elastic License 2.0** (free to use, not open source)

Adds the **JDBC driver** (which includes the community extensions) and raises materialized view limits:
### Feature Matrix

| Feature | Community | Pro | Enterprise |
|--------------------------------------|-----------|---------|------------|
| Everything in Community | Yes | Yes | Yes |
| JDBC driver (DBeaver, Tableau, etc.) | - | Yes | Yes |
| Maximum materialized views | 3 | Limited | Unlimited |
| Priority support | - | - | Yes |
| Feature | Community | Pro | Enterprise |
|--------------------------------------------------------------------|-----------|---------|------------|
| Full SQL DDL (CREATE, ALTER, DROP TABLE) | Yes | Yes | Yes |
| Full SQL DML (INSERT, UPDATE, DELETE, COPY INTO) | Yes | Yes | Yes |
| Full SQL DQL (SELECT, JOIN UNNEST, aggregations, window functions) | Yes | Yes | Yes |
| Pipelines, Watchers, Enrich Policies | Yes | Yes | Yes |
| Interactive REPL client | Yes | Yes | Yes |
| Scala library (Akka Streams) | Yes | Yes | Yes |
| Elasticsearch 6, 7, 8, 9 support | Yes | Yes | Yes |
| JDBC driver (DBeaver, Tableau, etc.) | Yes | Yes | Yes |
| Materialized Views (CREATE, REFRESH, DESCRIBE) | Max 3 | Limited | Unlimited |
| Priority support | - | - | Yes |

### Elasticsearch License Requirements

Expand Down Expand Up @@ -349,7 +368,7 @@ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines

The core SQL engine and REPL client are licensed under the **Apache License 2.0** — see [LICENSE](LICENSE) for details.

The JDBC driver and Materialized Views extension are available under a commercial license. Contact us for pricing information.
The JDBC driver and Materialized Views extension are licensed under the **Elastic License 2.0** — free to use, not open source.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2486,7 +2486,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : (param1 % 2)"
| }
| },
| "identifier_mul_identifier2_minus_10": {
| "__c7": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); def param2 = (doc['identifier2'].size() == 0 ? null : doc['identifier2'].value); def lv0 = ((param1 == null || param2 == null) ? null : (param1 * param2)); (lv0 == null) ? null : (lv0 - 10)"
Expand Down Expand Up @@ -2546,109 +2546,109 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
| }
| },
| "script_fields": {
| "abs_identifier_plus_1_0_mul_2": {
| "__c2": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); ((param1 == null) ? null : Double.valueOf(Math.abs(param1)) + 1.0) * ((double) 2)"
| }
| },
| "ceil_identifier": {
| "__c3": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.ceil(param1))"
| }
| },
| "floor_identifier": {
| "__c4": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.floor(param1))"
| }
| },
| "sqrt_identifier": {
| "__c5": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.sqrt(param1))"
| }
| },
| "exp_identifier": {
| "__c6": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.exp(param1))"
| }
| },
| "log_identifier": {
| "__c7": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.log(param1))"
| }
| },
| "log10_identifier": {
| "__c8": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.log10(param1))"
| }
| },
| "pow_identifier_3": {
| "__c9": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.pow(param1, 3))"
| }
| },
| "round_identifier": {
| "__c10": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); def param2 = Math.pow(10, 0); (param1 == null || param2 == null) ? null : Long.valueOf(Math.round((param1 * param2) / param2))"
| }
| },
| "round_identifier_2": {
| "__c11": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); def param2 = Math.pow(10, 2); (param1 == null || param2 == null) ? null : Long.valueOf(Math.round((param1 * param2) / param2))"
| }
| },
| "sign_identifier": {
| "__c12": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : (param1 > 0 ? 1 : (param1 < 0 ? -1 : 0))"
| }
| },
| "cos_identifier": {
| "__c13": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.cos(param1))"
| }
| },
| "acos_identifier": {
| "__c14": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.acos(param1))"
| }
| },
| "sin_identifier": {
| "__c15": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.sin(param1))"
| }
| },
| "asin_identifier": {
| "__c16": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.asin(param1))"
| }
| },
| "tan_identifier": {
| "__c17": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.tan(param1))"
| }
| },
| "atan_identifier": {
| "__c18": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.atan(param1))"
| }
| },
| "atan2_identifier_3_0": {
| "__c19": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['identifier'].size() == 0 ? null : doc['identifier'].value); (param1 == null) ? null : Double.valueOf(Math.atan2(param1, 3.0))"
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ThisBuild / organization := "app.softnetwork"

name := "softclient4es"

ThisBuild / version := "0.17.4"
ThisBuild / version := "0.18.0"

ThisBuild / scalaVersion := scala213

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,16 +449,7 @@ trait ElasticConversion {
wrapperAggs.flatMap { entry =>
val aggName = normalizeAggregationKey(entry.getKey)
val aggValue = entry.getValue
val docCount = Option(aggValue.get("doc_count"))
.map(_.asLong())
.getOrElse(0L)

// Add the doc_count to the context if necessary
val currentContext = if (docCount > 0) {
parentContext + (s"${aggName}_doc_count" -> docCount)
} else {
parentContext
}
val currentContext = parentContext

// Extract subaggregations (excluding doc_count)
val subAggsNode = mapper.createObjectNode()
Expand Down Expand Up @@ -502,13 +493,9 @@ trait ElasticConversion {
val allTopHits = extractAllTopHits(bucket, fieldAliases, aggregations)

val bucketKey = extractBucketKey(bucket)
val docCount = Option(bucket.get("doc_count"))
.map(_.asLong())
.getOrElse(0L)

val currentContext = parentContext ++ ListMap(
aggName -> bucketKey,
s"${aggName}_doc_count" -> docCount
aggName -> bucketKey
) ++ metrics ++ allTopHits

// Check for sub-aggregations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,7 @@ trait GatewayApi extends IndicesApi with ElasticClientHelpers {
// ========================================================================

def run(sql: String)(implicit system: ActorSystem): Future[ElasticResult[QueryResult]] = {
logger.info(s"📥 SQL: $sql")
val normalizedQuery =
sql
.split("\n")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ trait SearchApi extends ElasticConversion with ElasticClientHelpers {
* alias if present, otherwise the source field name. Returns empty Seq for SELECT * queries.
*/
protected def extractOutputFieldNames(single: SingleSearch): Seq[String] = {
val fields = single.select.fields
val fields = single.select.fieldsWithComputedAliases
if (fields.size == 1 && fields.head.identifier.identifierName == "*") Seq.empty
else fields.map(f => f.fieldAlias.map(_.alias).getOrElse(f.sourceField))
}
Expand Down
Loading
Loading