diff --git a/README.md b/README.md
index 9c551001..65645ab2 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ curl -fsSL https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/main/
irm https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/main/install.ps1 | iex
```
-**Windows (cmd.exe, when `.ps1` files are blocked):** one file — it fetches `install.ps1` if it is not beside it, takes the same flags, and only launches it with `-ExecutionPolicy Bypass`, for that one process.
+**Windows (cmd.exe, when `.ps1` files are blocked):** one file — it fetches `install.ps1` if it is not beside it (from a pinned release tag, SHA-256 verified), takes the same flags, and only launches it with `-ExecutionPolicy Bypass`, for that one process.
```bat
curl -O https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/main/install.cmd && install.cmd
```
@@ -216,10 +216,10 @@ Download the self-contained fat JAR for your Elasticsearch version:
| Elasticsearch Version | Artifact |
|-----------------------|----------------------------------------|
-| ES 6.x | `softclient4es6-jdbc-driver-0.2.5.jar` |
-| ES 7.x | `softclient4es7-jdbc-driver-0.2.5.jar` |
-| ES 8.x | `softclient4es8-jdbc-driver-0.2.5.jar` |
-| ES 9.x | `softclient4es9-jdbc-driver-0.2.5.jar` |
+| ES 6.x | `softclient4es6-jdbc-driver-0.3.0.jar` |
+| ES 7.x | `softclient4es7-jdbc-driver-0.3.0.jar` |
+| ES 8.x | `softclient4es8-jdbc-driver-0.3.0.jar` |
+| ES 9.x | `softclient4es9-jdbc-driver-0.3.0.jar` |
> **Java 11+ recommended** (17+ for ES 9.x): **cross-index JOINs require Java 11+** — the embedded JOIN engine is built on Apache Arrow 18.x, which ships Java-11 bytecode.
@@ -236,20 +236,20 @@ Driver class: app.softnetwork.elastic.jdbc.ElasticDriver
app.softnetwork.elastic
softclient4es8-jdbc-driver
- 0.2.5
+ 0.3.0
```
**Gradle:**
```groovy
-implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.2.5'
+implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.3.0'
```
**sbt:**
```scala
-libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.2.5"
+libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.3.0"
```
The JDBC driver JARs are Scala-version-independent (no `_2.12` or `_2.13` suffix) and include all required dependencies.
@@ -337,13 +337,13 @@ For programmatic access, add SoftClient4ES to your project.
resolvers += "Softnetwork" at "https://softnetwork.jfrog.io/artifactory/releases/"
// Choose your Elasticsearch version
-libraryDependencies += "app.softnetwork.elastic" %% "softclient4es8-java-client" % "0.20.4"
+libraryDependencies += "app.softnetwork.elastic" %% "softclient4es8-java-client" % "0.21.0"
// Add the community extensions for materialized views (optional)
-libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-community-extensions" % "0.2.4"
+libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-community-extensions" % "0.3.0"
// Add the arrow extensions for cross-index JOIN (required for JOINs; Java 11+)
-libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-arrow-extensions" % "0.2.5"
+libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-arrow-extensions" % "0.3.0"
// Add the JDBC driver if you want to use it from Scala (optional)
-libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-jdbc-driver" % "0.2.5"
+libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-jdbc-driver" % "0.3.0"
```
```scala
diff --git a/documentation/client/adbc_driver.md b/documentation/client/adbc_driver.md
index f37d2f76..532a0117 100644
--- a/documentation/client/adbc_driver.md
+++ b/documentation/client/adbc_driver.md
@@ -23,10 +23,10 @@ Download the self-contained fat JAR for your Elasticsearch version:
| Elasticsearch | Artifact |
|----------------|-----------------------------------------------------|
-| ES 6.x | `softclient4es6-adbc-driver-0.2.5.jar` |
-| ES 7.x | `softclient4es7-adbc-driver-0.2.5.jar` |
-| ES 8.x | `softclient4es8-adbc-driver-0.2.5.jar` |
-| ES 9.x | `softclient4es9-adbc-driver-0.2.5.jar` |
+| ES 6.x | `softclient4es6-adbc-driver-0.3.0.jar` |
+| ES 7.x | `softclient4es7-adbc-driver-0.3.0.jar` |
+| ES 8.x | `softclient4es8-adbc-driver-0.3.0.jar` |
+| ES 9.x | `softclient4es9-adbc-driver-0.3.0.jar` |
### Maven / Gradle / sbt
@@ -36,20 +36,20 @@ Download the self-contained fat JAR for your Elasticsearch version:
app.softnetwork.elastic
softclient4es8-adbc-driver
- 0.2.5
+ 0.3.0
```
**Gradle:**
```groovy
-implementation 'app.softnetwork.elastic:softclient4es8-adbc-driver:0.2.5'
+implementation 'app.softnetwork.elastic:softclient4es8-adbc-driver:0.3.0'
```
**sbt:**
```scala
-libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-adbc-driver" % "0.2.5"
+libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-adbc-driver" % "0.3.0"
```
---
diff --git a/documentation/client/arrow_flight_sql.md b/documentation/client/arrow_flight_sql.md
index f2cc2720..f05dae29 100644
--- a/documentation/client/arrow_flight_sql.md
+++ b/documentation/client/arrow_flight_sql.md
@@ -49,15 +49,15 @@ Available images per ES version:
### Fat JAR
```bash
-java -jar softclient4es8-arrow-flight-sql-0.2.5.jar
+java -jar softclient4es8-arrow-flight-sql-0.3.0.jar
```
| Elasticsearch | Artifact |
|---------------|----------|
-| ES 6.x | `softclient4es6-arrow-flight-sql-0.2.5.jar` |
-| ES 7.x | `softclient4es7-arrow-flight-sql-0.2.5.jar` |
-| ES 8.x | `softclient4es8-arrow-flight-sql-0.2.5.jar` |
-| ES 9.x | `softclient4es9-arrow-flight-sql-0.2.5.jar` |
+| ES 6.x | `softclient4es6-arrow-flight-sql-0.3.0.jar` |
+| ES 7.x | `softclient4es7-arrow-flight-sql-0.3.0.jar` |
+| ES 8.x | `softclient4es8-arrow-flight-sql-0.3.0.jar` |
+| ES 9.x | `softclient4es9-arrow-flight-sql-0.3.0.jar` |
---
diff --git a/documentation/client/download_analytics.md b/documentation/client/download_analytics.md
index 1852aaca..096f8bb9 100644
--- a/documentation/client/download_analytics.md
+++ b/documentation/client/download_analytics.md
@@ -15,7 +15,7 @@ beacon to a public endpoint with exactly these fields:
|---------------|----------|-----------------------------------------------|
| `source` | `portal` | Where the count came from (the docs button) |
| `driver` | `jdbc` | Which driver family (`jdbc` or `adbc`) |
-| `version` | `0.2.5` | The published artifact version |
+| `version` | `0.3.0` | The published artifact version |
| `count_delta` | `1` | One download |
A timestamp is added on the server. That is the **entire** record.
diff --git a/documentation/client/jdbc.md b/documentation/client/jdbc.md
index a9b1f33f..73ac44b2 100644
--- a/documentation/client/jdbc.md
+++ b/documentation/client/jdbc.md
@@ -20,10 +20,10 @@ Download the self-contained fat JAR for your Elasticsearch version. The JARs are
| Elasticsearch | Artifact |
|---------------|----------|
-| ES 6.x | `softclient4es6-jdbc-driver-0.2.5.jar` |
-| ES 7.x | `softclient4es7-jdbc-driver-0.2.5.jar` |
-| ES 8.x | `softclient4es8-jdbc-driver-0.2.5.jar` |
-| ES 9.x | `softclient4es9-jdbc-driver-0.2.5.jar` |
+| ES 6.x | `softclient4es6-jdbc-driver-0.3.0.jar` |
+| ES 7.x | `softclient4es7-jdbc-driver-0.3.0.jar` |
+| ES 8.x | `softclient4es8-jdbc-driver-0.3.0.jar` |
+| ES 9.x | `softclient4es9-jdbc-driver-0.3.0.jar` |
### Build Tool Integration
@@ -33,20 +33,20 @@ Download the self-contained fat JAR for your Elasticsearch version. The JARs are
app.softnetwork.elastic
softclient4es8-jdbc-driver
- 0.2.5
+ 0.3.0
```
**Gradle:**
```groovy
-implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.2.5'
+implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.3.0'
```
**sbt:**
```scala
-libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.2.5"
+libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.3.0"
```
---
diff --git a/documentation/client/repl.md b/documentation/client/repl.md
index ec4c1c47..4b73d7ff 100644
--- a/documentation/client/repl.md
+++ b/documentation/client/repl.md
@@ -54,20 +54,33 @@ It provides:
**On Windows you do not have to install Java yourself.** Since `0.20.4`,
`install.ps1` (and therefore `install.cmd`) resolves Java in this order:
-1. `%JAVA_HOME%\bin\java.exe` — when `JAVA_HOME` is set, that is the JVM tested,
+1. `\jdk\bin\java.exe` — a JDK a previous run bootstrapped into the
+ same install directory. It is reused as it stands, so re-running the
+ installer (to upgrade, to add `-NoExtensions`, to retry a failed download)
+ never downloads the ~180 MB Temurin zip a second time.
+2. `%JAVA_HOME%\bin\java.exe` — when `JAVA_HOME` is set, that is the JVM tested,
not whatever `java` happens to be first on `PATH`; the two frequently differ.
-2. the `java` on `PATH`, when `JAVA_HOME` is not set.
-3. Neither is present, or the one found is **below the floor** for your ES
+3. the `java` on `PATH`, when `JAVA_HOME` is not set.
+4. None of those is present, or the one found is **below the floor** for your ES
version ⇒ the installer downloads a portable **Temurin 17** JDK (a zip from
Adoptium, never an MSI, so it needs **no administrator rights**) and unpacks
- it to `\jdk`.
+ it to `\jdk`. The new JDK is unpacked and checked before any
+ existing one is removed, so a failed download never leaves you without a JVM.
Java 17 satisfies both floors, so there is only ever one JDK to think about. The
bootstrapped JDK lives **inside the install directory**: `uninstall.ps1` removes
it along with everything else, and nothing machine-wide is modified — the
installer sets `JAVA_HOME` and `PATH` **for its own session only**. Later
sessions do not need them, because the generated launcher applies the same order
-and finds `\jdk` by relative path.
+and finds `\jdk` by relative path — and refuses to start, naming the
+version it found, if that JVM turns out to be below the floor (a stale
+`JAVA_HOME` is the usual cause) rather than letting the JVM fail with
+`UnsupportedClassVersionError`.
+
+If an install fails part way through — a version that does not exist, an
+unreachable repository — a bootstrapped JDK is left in place on purpose and the
+installer says where it is: the next attempt reuses it. Delete the install
+directory if you are not going to retry.
On Linux and macOS `install.sh` still expects a suitable Java to be present.
@@ -126,8 +139,22 @@ install.cmd
```
A local `install.ps1` always wins, so a downloaded pair stays self-consistent —
-put both files in the same directory when you want a pinned copy rather than
-whatever is on `main`.
+put both files in the same directory when you want a specific `install.ps1`.
+
+What the fallback fetches is **pinned to a release tag, not to `main`**, and is
+checked against a SHA-256 recorded in `install.cmd` before it is run; a mismatch
+is a hard failure, and the download goes to a fresh temporary directory that is
+removed when the run ends. So `install.cmd` on its own installs with the
+installer of the release it belongs to, never with whatever is currently on
+`main`. To use `main`'s installer instead:
+
+```bat
+set SOFTCLIENT4ES_INSTALL_REF=main
+install.cmd
+```
+
+That drops the integrity check — the pinned hash belongs to the pinned tag —
+and `install.cmd` says so unless you also set `SOFTCLIENT4ES_INSTALL_SHA256`.
`install.cmd` accepts exactly the flags `install.ps1` does and forwards them
verbatim, so every option, default and fallback documented below applies
@@ -135,7 +162,7 @@ unchanged — there is no second implementation to drift:
```bat
install.cmd -ListVersions -EsVersion 8
-install.cmd -Target "C:\tools\softclient4es" -EsVersion 8 -Version 0.20.4
+install.cmd -Target "C:\tools\softclient4es" -EsVersion 8 -Version 0.21.0
install.cmd -EsVersion 9 -NoExtensions
install.cmd -Help
```
@@ -188,8 +215,9 @@ install.cmd -ListVersions -EsVersion 8
• 0.20.2
• 0.20.3
• 0.20.4
+ • 0.21.0
- Total: 4 version(s)
+ Total: 5 version(s)
To install a specific version:
./install.sh --es-version 8 --version
@@ -217,7 +245,7 @@ install.cmd -ListVersions -EsVersion 8
./install.sh --list-versions --es-version 8
# Install specific version
-./install.sh --es-version 8 --version 0.20.4
+./install.sh --es-version 8 --version 0.21.0
# Install for Elasticsearch 9 (requires Java 17+)
./install.sh --es-version 9
@@ -226,7 +254,7 @@ install.cmd -ListVersions -EsVersion 8
./install.sh --target /opt/softclient4es
# Full custom installation
-./install.sh --target ~/tools/softclient4es --es-version 7 --version 0.20.4
+./install.sh --target ~/tools/softclient4es --es-version 7 --version 0.21.0
```
#### Windows
@@ -239,7 +267,7 @@ install.cmd -ListVersions -EsVersion 8
.\install.ps1 -ListVersions -EsVersion 8
# Install specific version
-.\install.ps1 -EsVersion 8 -Version 0.20.4
+.\install.ps1 -EsVersion 8 -Version 0.21.0
# Install for Elasticsearch 9 (requires Java 17+)
.\install.ps1 -EsVersion 9
@@ -248,7 +276,7 @@ install.cmd -ListVersions -EsVersion 8
.\install.ps1 -Target "C:\tools\softclient4es"
# Full custom installation
-.\install.ps1 -Target "C:\tools\softclient4es" -EsVersion 7 -Version 0.20.4
+.\install.ps1 -Target "C:\tools\softclient4es" -EsVersion 7 -Version 0.21.0
```
---
diff --git a/documentation/sql/ddl_statements.md b/documentation/sql/ddl_statements.md
index 865f727f..1df95702 100644
--- a/documentation/sql/ddl_statements.md
+++ b/documentation/sql/ddl_statements.md
@@ -58,8 +58,8 @@ A SQL table corresponds to:
| SQL Definition | Elasticsearch Structure |
|-----------------------------------------|----------------------------------------------------|
-| `CREATE TABLE` without `PARTITIONED BY` | **Concrete index** |
-| `CREATE TABLE` with `PARTITIONED BY` | **Index template** (legacy ES6 or composable ES7+) |
+| `CREATE TABLE` without `PARTITION BY` | **Concrete index** |
+| `CREATE TABLE` with `PARTITION BY` | **Index template** (legacy ES6 or composable ES7+) |
### Index-backed table (no partitioning)
@@ -79,13 +79,17 @@ Creates:
### Template-backed table (with partitioning)
+The clause is `PARTITION BY ()` — the column first, the granularity in
+parentheses after it. The parentheses are required when a granularity is given; omit the
+granularity entirely and it defaults to `DAY`.
+
```sql
CREATE TABLE users (
id INT,
birthdate DATE,
PRIMARY KEY (id)
)
-PARTITIONED BY (birthdate MONTH);
+PARTITION BY birthdate (MONTH);
```
Creates:
@@ -304,7 +308,7 @@ CREATE TABLE users (
birthdate DATE,
PRIMARY KEY (id)
)
-PARTITIONED BY (birthdate MONTH);
+PARTITION BY birthdate (MONTH);
```
### Table Options (index settings)
diff --git a/install.cmd b/install.cmd
index 3754490e..46dab175 100644
--- a/install.cmd
+++ b/install.cmd
@@ -11,14 +11,15 @@ rem THIS process only, changes nothing on the machine, and needs no elevation.
rem
rem It is a wrapper and nothing else - every option, default, fallback and
rem message lives in install.ps1, so the two entry points can never drift.
-rem When install.ps1 is not sitting next to it, it downloads one, so
-rem install.cmd on its own is a complete install. Pass the same flags you
-rem would pass to install.ps1:
+rem When install.ps1 is not sitting next to it, it downloads one - from a
+rem pinned release tag, checked against a pinned SHA-256 before it is run - so
+rem install.cmd on its own is a complete install. Pass the same flags you would
+rem pass to install.ps1:
rem
rem install.cmd
rem install.cmd -ListVersions -EsVersion 8
rem install.cmd -Target "C:\tools\softclient4es" -EsVersion 8 -Version 1.0.0
-rem install.cmd -EsVersion 7 -Version 0.20.4 -NoExtensions
+rem install.cmd -EsVersion 7 -Version 0.21.0 -NoExtensions
rem install.cmd -Help
rem
rem Caveat worth knowing: -ExecutionPolicy Bypass is overridden when the policy
@@ -30,8 +31,40 @@ rem ===========================================================================
setlocal
-set "PS1_URL=https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/refs/heads/main/install.ps1"
+rem ---------------------------------------------------------------------------
+rem Where a missing install.ps1 is fetched from.
+rem
+rem PINNED TO A RELEASE TAG, not to main. This file also travels inside release
+rem bundles, and one that quietly pulled whatever happens to be on main would
+rem produce exactly the mix that the "a local install.ps1 always wins" rule
+rem below exists to prevent - silently, since a user who ran a released .cmd has
+rem no reason to think they are running main. A tag is also immutable, which is
+rem what makes the SHA-256 worth checking at all: the script is about to be run
+rem with -ExecutionPolicy Bypass, and "it arrived over TLS" only says the
+rem transport was sound, not that the file is the one the release intended.
+rem
+rem RELEASE RITUAL: bump BOTH lines together when cutting a release.
+rem curl -fsSL https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES//install.ps1 | shasum -a 256
+rem A mismatch is a hard failure, so a bumped tag with a stale hash breaks the
+rem fallback loudly instead of running something unverified.
+rem
+rem To track development instead: set SOFTCLIENT4ES_INSTALL_REF=main
+rem The pinned hash belongs to the pinned tag, so overriding the ref drops the
+rem integrity check unless SOFTCLIENT4ES_INSTALL_SHA256 supplies another one -
+rem and says so when it does not.
+rem ---------------------------------------------------------------------------
+set "PS1_REF=v0.21.0"
+set "PS1_SHA256=e1275bd269c8bb922d28827b6c74e965bbe7b5139b4e16138bacaac7c03912ae"
+
+rem One `if` per line, never a parenthesised block: cmd expands every %VAR% in a
+rem block in ONE parse pass, before running any line in it.
+if defined SOFTCLIENT4ES_INSTALL_REF set "PS1_REF=%SOFTCLIENT4ES_INSTALL_REF%"
+if defined SOFTCLIENT4ES_INSTALL_REF set "PS1_SHA256=%SOFTCLIENT4ES_INSTALL_SHA256%"
+
+set "PS1_URL=https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/%PS1_REF%/install.ps1"
set "PS1=%~dp0install.ps1"
+set "PS1_TMPDIR="
+set "RC=1"
rem A local install.ps1 always wins: a downloaded pair must stay self-consistent,
rem and a released bundle must never be silently mixed with main.
@@ -40,17 +73,23 @@ rem how cmd binds `||` inside an `if` body is ambiguous, and this is not a
rem platform where a subtlety can be settled by running it.
if exist "%PS1%" goto run
call :fetch_ps1
-if errorlevel 1 exit /b 1
+if errorlevel 1 goto cleanup
:run
powershell -NoProfile -ExecutionPolicy Bypass -File "%PS1%" %*
-exit /b %ERRORLEVEL%
+set "RC=%ERRORLEVEL%"
+
+rem Single exit point, so a downloaded install.ps1 is removed however this ends -
+rem including the paths that never got as far as running it.
+:cleanup
+if defined PS1_TMPDIR if exist "%PS1_TMPDIR%" rd /s /q "%PS1_TMPDIR%"
+exit /b %RC%
rem ---------------------------------------------------------------------------
-rem Each line of a subroutine is parsed when it is reached, so %PS1% below sees
-rem the value assigned on the previous line. The same code inside the `if not
-rem exist (...)` block above would NOT: cmd expands every %VAR% in a
-rem parenthesised block in ONE parse pass, before running any line in it.
+rem Each line of a subroutine is parsed when it is reached, so %PS1_TMPDIR% below
+rem sees the value assigned on the previous line. The same code inside an `if not
+rem exist (...)` block would NOT: cmd expands every %VAR% in a parenthesised block
+rem in ONE parse pass, before running any line in it.
rem ---------------------------------------------------------------------------
:fetch_ps1
where curl.exe >nul 2>&1
@@ -60,7 +99,20 @@ if errorlevel 1 (
echo [ERROR] Download install.ps1 manually into the same directory as install.cmd. 1>&2
exit /b 1
)
-set "PS1=%TEMP%\softclient4es-install.ps1"
+
+rem A fresh directory of our own, rather than the fixed %TEMP%\softclient4es-install.ps1
+rem this used to write: that was a predictable path in a shared directory, executed
+rem with -ExecutionPolicy Bypass, and left behind after every run. mkdir fails when
+rem the name already exists, so nothing pre-created can be substituted for it.
+set "PS1_TMPDIR=%TEMP%\softclient4es-install-%RANDOM%%RANDOM%"
+mkdir "%PS1_TMPDIR%" 2>nul
+if errorlevel 1 (
+ echo [ERROR] Could not create a temporary directory: %PS1_TMPDIR% 1>&2
+ set "PS1_TMPDIR="
+ exit /b 1
+)
+set "PS1=%PS1_TMPDIR%\install.ps1"
+
echo [INFO] install.ps1 not found next to install.cmd - downloading it...
echo [INFO] URL: %PS1_URL%
curl.exe -fsSL -o "%PS1%" "%PS1_URL%"
@@ -68,5 +120,46 @@ if errorlevel 1 (
echo [ERROR] Could not download install.ps1 from %PS1_URL% 1>&2
exit /b 1
)
+
+call :verify_ps1
+if errorlevel 1 exit /b 1
+
echo [INFO] Using %PS1%
exit /b 0
+
+rem ---------------------------------------------------------------------------
+rem Integrity check, before the file is handed to powershell.exe.
+rem ---------------------------------------------------------------------------
+:verify_ps1
+if not defined PS1_SHA256 (
+ echo [WARN] SOFTCLIENT4ES_INSTALL_REF is set without SOFTCLIENT4ES_INSTALL_SHA256, 1>&2
+ echo [WARN] so the downloaded install.ps1 was NOT checked against a known hash. 1>&2
+ exit /b 0
+)
+
+where certutil.exe >nul 2>&1
+if errorlevel 1 (
+ echo [ERROR] certutil.exe is unavailable, so install.ps1 cannot be verified - it was NOT run. 1>&2
+ echo [ERROR] Download install.ps1 manually into the same directory as install.cmd. 1>&2
+ exit /b 1
+)
+
+rem certutil prints the digest on the second line. The FOR must set PS1_HASH on a
+rem line of its own, and the space-stripping must live OUTSIDE any block, or the
+rem one-parse-pass rule above would have it read the previous value.
+set "PS1_HASH="
+for /f "skip=1 tokens=* delims=" %%h in ('certutil -hashfile "%PS1%" SHA256') do if not defined PS1_HASH set "PS1_HASH=%%h"
+rem Older certutil builds print the digest as space-separated byte pairs.
+set "PS1_HASH=%PS1_HASH: =%"
+
+if /i not "%PS1_HASH%"=="%PS1_SHA256%" (
+ echo [ERROR] install.ps1 failed its integrity check - it was NOT run. 1>&2
+ echo [ERROR] expected: %PS1_SHA256% 1>&2
+ echo [ERROR] actual: %PS1_HASH% 1>&2
+ echo [ERROR] url: %PS1_URL% 1>&2
+ echo [ERROR] Download install.ps1 manually into the same directory as install.cmd. 1>&2
+ exit /b 1
+)
+
+echo [INFO] install.ps1 SHA-256 verified against %PS1_REF%
+exit /b 0
diff --git a/install.ps1 b/install.ps1
index 7a43fcdd..8a403f0f 100644
--- a/install.ps1
+++ b/install.ps1
@@ -63,7 +63,7 @@ Examples:
.\install.ps1
.\install.ps1 -ListVersions -EsVersion 8
.\install.ps1 -Target "C:\tools\softclient4es" -EsVersion 8 -Version 1.0.0
- .\install.ps1 -EsVersion 7 -Version 0.20.4 -NoExtensions
+ .\install.ps1 -EsVersion 7 -Version 0.21.0 -NoExtensions
"@
exit 0
@@ -174,11 +174,28 @@ $EMBEDDED_JDK_DIR = Join-Path $Target "jdk"
# `java`, which is frequently a different and older JVM.
# Set by Resolve-Java; read by Check-Prerequisites, the launcher writer and the
-# summary.
+# summary. $script:EmbeddedJdkHome is also what tells the failure path that this
+# install depends on a JDK under $Target.
$script:JavaMajor = 0
$script:JavaSource = "not found"
$script:EmbeddedJdkHome = $null
+# Point this run at a JDK inside the install tree - the one the launcher will use.
+# SESSION scope only, deliberately not [Environment]::SetEnvironmentVariable(...,"User"):
+# a machine-wide JAVA_HOME would silently repoint every other tool on the box, and
+# would dangle after uninstall. Later sessions need nothing - the launcher finds
+# \jdk by relative path.
+function Use-EmbeddedJdk {
+ param([string]$JdkHome, [int]$Major)
+
+ $script:EmbeddedJdkHome = $JdkHome
+ $script:JavaMajor = $Major
+ $script:JavaSource = "bundled JDK ($JdkHome)"
+
+ $env:JAVA_HOME = $JdkHome
+ $env:PATH = (Join-Path $JdkHome "bin") + ";" + $env:PATH
+}
+
function Install-EmbeddedJdk {
Write-Info "Installing a portable Temurin $BOOTSTRAP_JAVA_VERSION JDK (zip, no administrator rights)..."
@@ -198,9 +215,16 @@ function Install-EmbeddedJdk {
# to a staging dir and MOVE that one level up, so the final JAVA_HOME is the
# fixed path \jdk. The launcher hard-codes `%BASE_DIR%\jdk\bin`, and
# it must not have to glob for a name that changes with every Temurin build.
+ #
+ # Stage, verify, THEN swap: an existing \jdk is removed only once a
+ # usable replacement is on disk. The unpack needs the 180 MB zip plus ~300 MB
+ # expanded plus the ~309 MB jar in the same tree, so running out of disk lands
+ # squarely in this window, and deleting first left a previously working
+ # install with no JVM at all — broken by the very installer the user would
+ # then re-run to fix it (issue #234). $staging is scratch by construction, so
+ # clearing that one up front is fine.
$staging = "$EMBEDDED_JDK_DIR.unpack"
- if (Test-Path $staging) { Remove-Item -Recurse -Force $staging }
- if (Test-Path $EMBEDDED_JDK_DIR) { Remove-Item -Recurse -Force $EMBEDDED_JDK_DIR }
+ if (Test-Path $staging) { Remove-Item -Recurse -Force $staging }
New-Item -ItemType Directory -Force -Path $staging | Out-Null
try {
@@ -211,6 +235,13 @@ function Install-EmbeddedJdk {
Write-Err "The Temurin archive did not unpack as expected (no directory inside $staging)"
return $null
}
+ if (-not (Test-Path (Join-Path (Join-Path $inner.FullName "bin") "java.exe"))) {
+ Write-Err "The Temurin archive did not unpack as expected (no bin\java.exe under $($inner.FullName))"
+ return $null
+ }
+
+ # Known-good from here: the previous JDK, if any, can go.
+ if (Test-Path $EMBEDDED_JDK_DIR) { Remove-Item -Recurse -Force $EMBEDDED_JDK_DIR }
Move-Item -Path $inner.FullName -Destination $EMBEDDED_JDK_DIR
}
finally {
@@ -237,35 +268,58 @@ function Install-EmbeddedJdk {
function Resolve-Java {
Write-Info "Resolving Java (ES$EsVersion requires ${REQUIRED_JAVA_VERSION}+)..."
- # JAVA_HOME first, exactly as the launcher will. Probing the PATH `java` when
- # JAVA_HOME is set would validate a JVM the REPL is never going to run.
- $javaHomeExe = if ($env:JAVA_HOME) { Join-Path (Join-Path $env:JAVA_HOME "bin") "java.exe" } else { "" }
- $jhMajor = if ($javaHomeExe -and (Test-Path $javaHomeExe)) { Get-JavaMajorFromExe -Exe $javaHomeExe } else { 0 }
+ # \jdk first — the JDK a previous run of this installer bootstrapped,
+ # and the first thing both generated launchers look at. Without this probe every
+ # re-run downloads the ~180 MB Temurin zip again and unpacks it over a perfectly
+ # good JDK (JAVA_HOME is set for the session only, so a later shell has nothing
+ # pointing at it either), and the installer contradicts the resolution order its
+ # own launchers document (issue #233).
+ $bundledExe = Join-Path (Join-Path $EMBEDDED_JDK_DIR "bin") "java.exe"
+ $bundledMajor = if (Test-Path $bundledExe) { Get-JavaMajorFromExe -Exe $bundledExe } else { 0 }
+
+ if ($bundledMajor -ge $REQUIRED_JAVA_VERSION) {
+ Use-EmbeddedJdk -JdkHome $EMBEDDED_JDK_DIR -Major $bundledMajor
+ Write-Success "Java $bundledMajor found via $($script:JavaSource) (required: ${REQUIRED_JAVA_VERSION}+) — nothing to download"
+ return $true
+ }
- if ($jhMajor -gt 0) {
- $script:JavaMajor = $jhMajor
- $script:JavaSource = "JAVA_HOME ($env:JAVA_HOME)"
+ if ($bundledMajor -gt 0) {
+ # The launcher prefers \jdk whenever java.exe is there, so a bundled
+ # JDK below the floor cannot be left in place and worked around with JAVA_HOME
+ # — it has to be replaced. Straight to the bootstrap, which swaps it out.
+ Write-Warn "The JDK bundled at $EMBEDDED_JDK_DIR is Java $bundledMajor — below the required ${REQUIRED_JAVA_VERSION}+; replacing it"
}
else {
- if ($env:JAVA_HOME) {
- Write-Warn "JAVA_HOME is set to '$env:JAVA_HOME' but no usable java.exe was found under it"
+ # Then JAVA_HOME, exactly as the launcher will. Probing the PATH `java` when
+ # JAVA_HOME is set would validate a JVM the REPL is never going to run.
+ $javaHomeExe = if ($env:JAVA_HOME) { Join-Path (Join-Path $env:JAVA_HOME "bin") "java.exe" } else { "" }
+ $jhMajor = if ($javaHomeExe -and (Test-Path $javaHomeExe)) { Get-JavaMajorFromExe -Exe $javaHomeExe } else { 0 }
+
+ if ($jhMajor -gt 0) {
+ $script:JavaMajor = $jhMajor
+ $script:JavaSource = "JAVA_HOME ($env:JAVA_HOME)"
}
- $pathMajor = Get-JavaMajorVersion
- if ($pathMajor -gt 0) {
- $script:JavaMajor = $pathMajor
- $script:JavaSource = "PATH"
+ else {
+ if ($env:JAVA_HOME) {
+ Write-Warn "JAVA_HOME is set to '$env:JAVA_HOME' but no usable java.exe was found under it"
+ }
+ $pathMajor = Get-JavaMajorVersion
+ if ($pathMajor -gt 0) {
+ $script:JavaMajor = $pathMajor
+ $script:JavaSource = "PATH"
+ }
}
- }
- if ($script:JavaMajor -ge $REQUIRED_JAVA_VERSION) {
- Write-Success "Java $($script:JavaMajor) found via $($script:JavaSource) (required: ${REQUIRED_JAVA_VERSION}+)"
- return $true
- }
+ if ($script:JavaMajor -ge $REQUIRED_JAVA_VERSION) {
+ Write-Success "Java $($script:JavaMajor) found via $($script:JavaSource) (required: ${REQUIRED_JAVA_VERSION}+)"
+ return $true
+ }
- if ($script:JavaMajor -eq 0) {
- Write-Warn "No usable Java found"
- } else {
- Write-Warn "Java $($script:JavaMajor) found via $($script:JavaSource) — below the required ${REQUIRED_JAVA_VERSION}+"
+ if ($script:JavaMajor -eq 0) {
+ Write-Warn "No usable Java found"
+ } else {
+ Write-Warn "Java $($script:JavaMajor) found via $($script:JavaSource) — below the required ${REQUIRED_JAVA_VERSION}+"
+ }
}
$jdkHome = Install-EmbeddedJdk
@@ -275,15 +329,7 @@ function Resolve-Java {
return $false
}
- $script:EmbeddedJdkHome = $jdkHome
- $script:JavaMajor = Get-JavaMajorFromExe -Exe (Join-Path (Join-Path $jdkHome "bin") "java.exe")
- $script:JavaSource = "bundled JDK ($jdkHome)"
-
- # SESSION scope only — deliberately not [Environment]::SetEnvironmentVariable(...,"User").
- # A machine-wide JAVA_HOME would silently repoint every other tool on the box.
- # Future sessions do not need it: the launcher prefers \jdk directly.
- $env:JAVA_HOME = $jdkHome
- $env:PATH = (Join-Path $jdkHome "bin") + ";" + $env:PATH
+ Use-EmbeddedJdk -JdkHome $jdkHome -Major (Get-JavaMajorFromExe -Exe (Join-Path (Join-Path $jdkHome "bin") "java.exe"))
Write-Success "Java $($script:JavaMajor) ready — JAVA_HOME and PATH updated for THIS session"
return $true
@@ -293,6 +339,16 @@ function Resolve-Java {
# List Available Versions
# =============================================================================
+# One listing per artifact per run. The pre-flight below consults the same lists
+# the bundle-selection block does, and an HTTP call that has already been answered
+# must not be paid for - or answered differently - twice in one run.
+$script:VersionListings = @{}
+
+# Why a listing failed, kept for the pre-flight: -Quiet has to stay silent (a
+# missing -all bundle is a normal, expected 404) but a genuine outage must not
+# reach the user as a bare "no versions found".
+$script:LastListingError = $null
+
function Get-AvailableVersions {
param(
# Default to the PLAIN artifact, never $ARTIFACT_NAME: that one is
@@ -303,6 +359,10 @@ function Get-AvailableVersions {
[switch]$Quiet
)
+ if ($script:VersionListings.ContainsKey($Artifact)) {
+ return $script:VersionListings[$Artifact]
+ }
+
$apiUrl = "${JFROG_API_URL}/${Artifact}"
try {
@@ -315,9 +375,15 @@ function Get-AvailableVersions {
Where-Object { $_ -notmatch '^\.' } |
Sort-Object { [Version]($_ -replace '-SNAPSHOT', '.0' -replace '[^0-9.]', '') }
+ # Successes only: a listing that failed keeps its own semantics (fail hard,
+ # or empty under -Quiet) if it is asked for again.
+ $versions = @($versions)
+ if ($versions.Count -gt 0) { $script:VersionListings[$Artifact] = $versions }
+
return $versions
}
catch {
+ $script:LastListingError = $_.Exception.Message
if ($Quiet) { return @() }
Write-Err "Failed to fetch versions from repository"
Write-Err "Artifact: $Artifact"
@@ -418,11 +484,48 @@ function Resolve-LatestVersion {
return @($versions)[-1]
}
+# =============================================================================
+# Pre-flight: settle the inputs before anything large is downloaded
+# =============================================================================
+# Resolving Java can write ~300 MB into $Target, and it used to be the FIRST thing
+# to touch the disk - so a typo in -Version, an artifact that does not exist for
+# the chosen -EsVersion / -ScalaVersion, or an unreachable repository was only
+# discovered by Download-Jar, leaving an orphaned jdk\ behind that nothing in the
+# failure output even mentioned (issue #236). The listings are the same two cheap
+# calls bundle selection makes below, memoised, so this costs nothing.
+function Test-RequestedVersion {
+ $artifacts = @()
+ if ($WITH_EXTENSIONS) { $artifacts += $BUNDLE_ARTIFACT_NAME }
+ $artifacts += $PLAIN_ARTIFACT_NAME
+
+ $known = @()
+ foreach ($artifact in $artifacts) {
+ $known += @(Get-AvailableVersions -Artifact $artifact -Quiet)
+ }
+
+ if ($known.Count -eq 0) {
+ Write-Err "No versions found for $($artifacts -join ' or ')"
+ Write-Err "Check -EsVersion $EsVersion and -ScalaVersion $ScalaVersion, and that the repository is reachable:"
+ Write-Err $JFROG_API_URL
+ if ($script:LastListingError) { Write-Err $script:LastListingError }
+ exit 1
+ }
+
+ if ($Version -ne "latest" -and $known -notcontains $Version) {
+ Write-Err "Version '$Version' is not published for $($artifacts -join ' or ')"
+ Write-Err "Run with -ListVersions to see available versions."
+ exit 1
+ }
+}
+
+Test-RequestedVersion
+
# =============================================================================
# Resolve Java before anything else that depends on it
# =============================================================================
# Runs AFTER the -ListVersions early exit (listing versions must not download a
-# JDK) and BEFORE bundle selection, which reads the resolved major.
+# JDK), AFTER the pre-flight above (a bad -Version must not cost a JDK download)
+# and BEFORE bundle selection, which reads the resolved major.
if (-not (Resolve-Java)) { exit 1 }
# =============================================================================
@@ -875,6 +978,20 @@ if not defined JVER (
)
for /f "tokens=1,2 delims=." %%a in ("%JVER%") do if "%%a"=="1" (set JAVA_MAJOR=%%b) else (set JAVA_MAJOR=%%a)
+REM Refuse a JVM below the floor instead of letting it fail with
+REM UnsupportedClassVersionError, which names a class-file version rather than a
+REM Java one and reads as a broken install. The .ps1 launcher has always had this
+REM check; the .bat computed JAVA_MAJOR and spent it only on --add-opens, so a
+REM stale %JAVA_HOME% - which outranks the PATH here, deliberately, because that
+REM is the JVM the installer probed - silently won (issue #235).
+REM The "not 0" guard keeps parity with the .ps1 launcher: refuse only a version
+REM we positively read as too low, never one we failed to parse.
+if not "%JAVA_MAJOR%"=="0" if %JAVA_MAJOR% LSS %REQUIRED_JAVA% (
+ echo Error: Java %REQUIRED_JAVA%+ is required. Found: Java %JAVA_MAJOR% >&2
+ if defined JAVA_HOME echo JAVA_HOME takes precedence over the PATH here - point it at a Java %REQUIRED_JAVA%+ JDK, or clear it. >&2
+ exit /b 1
+)
+
REM The extensions (Apache Arrow / DuckDB) need reflective access on Java 9+.
REM JAVA_MAJOR is initialised to 0 above so this comparison always has a left
REM operand (an empty one is a cmd syntax error that aborts the whole script).
@@ -954,6 +1071,9 @@ try {
if (`$javaVersion -gt 0 -and `$javaVersion -lt `$RequiredJava) {
Write-Error "Java `$RequiredJava+ is required. Found: Java `$javaVersion"
+ if (`$env:JAVA_HOME) {
+ Write-Error "JAVA_HOME takes precedence over the PATH here - point it at a Java `$RequiredJava+ JDK, or clear it."
+ }
exit 1
}
}
@@ -1144,15 +1264,31 @@ Write-Host " SoftClient4ES Installer" -ForegroundColor Cyan
Write-Host "==================================================================" -ForegroundColor Cyan
Write-Host ""
-Check-Prerequisites
-Create-Directories
-Download-Jar
-Extract-BundleLicenses
-Show-LicenseNotice
-Download-Docs
-Create-Config
-Create-LogbackConfig # <-- Création du fichier logback.xml
-Create-Launcher
-Create-Uninstaller
-Create-VersionInfo
-Print-Summary
+# The finally runs on `exit` too, so it covers every failure path below - which is
+# the point: whatever went wrong, the user is told about the JDK sitting in $Target
+# rather than discovering ~300 MB of it later (issue #236).
+$script:InstallCompleted = $false
+try {
+ Check-Prerequisites
+ Create-Directories
+ Download-Jar
+ Extract-BundleLicenses
+ Show-LicenseNotice
+ Download-Docs
+ Create-Config
+ Create-LogbackConfig # <-- Création du fichier logback.xml
+ Create-Launcher
+ Create-Uninstaller
+ Create-VersionInfo
+ Print-Summary
+ $script:InstallCompleted = $true
+}
+finally {
+ if (-not $script:InstallCompleted -and $script:EmbeddedJdkHome) {
+ Write-Host ""
+ Write-Warn "The install did not complete. A portable Temurin $BOOTSTRAP_JAVA_VERSION JDK (~300 MB) is at:"
+ Write-Warn " $($script:EmbeddedJdkHome)"
+ Write-Warn "It is kept on purpose - a re-run reuses it instead of downloading it again."
+ Write-Warn "Delete that directory if you are not going to retry."
+ }
+}