diff --git a/README.md b/README.md index 3f9f2711..43c01b5c 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,9 @@ 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):** download `install.cmd` and `install.ps1` side by side. It takes the same flags as `install.ps1` 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, takes the same flags, and only launches it with `-ExecutionPolicy Bypass`, for that one process. ```bat -install.cmd +curl -O https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/main/install.cmd && install.cmd ``` ### Connect and Query @@ -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.4.jar` | -| ES 7.x | `softclient4es7-jdbc-driver-0.2.4.jar` | -| ES 8.x | `softclient4es8-jdbc-driver-0.2.4.jar` | -| ES 9.x | `softclient4es9-jdbc-driver-0.2.4.jar` | +| 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` | > **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.4 + 0.2.5 ``` **Gradle:** ```groovy -implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.2.4' +implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.2.5' ``` **sbt:** ```scala -libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.2.4" +libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.2.5" ``` 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.3" +libraryDependencies += "app.softnetwork.elastic" %% "softclient4es8-java-client" % "0.20.4" // Add the community extensions for materialized views (optional) -libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-community-extensions" % "0.2.3" +libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-community-extensions" % "0.2.4" // Add the arrow extensions for cross-index JOIN (required for JOINs; Java 11+) -libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-arrow-extensions" % "0.2.4" +libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-arrow-extensions" % "0.2.5" // Add the JDBC driver if you want to use it from Scala (optional) -libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-jdbc-driver" % "0.2.4" +libraryDependencies += "app.softnetwork.elastic" %% "softclient4es-jdbc-driver" % "0.2.5" ``` ```scala diff --git a/documentation/client/adbc_driver.md b/documentation/client/adbc_driver.md index e5fc5cf5..f37d2f76 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.4.jar` | -| ES 7.x | `softclient4es7-adbc-driver-0.2.4.jar` | -| ES 8.x | `softclient4es8-adbc-driver-0.2.4.jar` | -| ES 9.x | `softclient4es9-adbc-driver-0.2.4.jar` | +| 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` | ### 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.4 + 0.2.5 ``` **Gradle:** ```groovy -implementation 'app.softnetwork.elastic:softclient4es8-adbc-driver:0.2.4' +implementation 'app.softnetwork.elastic:softclient4es8-adbc-driver:0.2.5' ``` **sbt:** ```scala -libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-adbc-driver" % "0.2.4" +libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-adbc-driver" % "0.2.5" ``` --- diff --git a/documentation/client/arrow_flight_sql.md b/documentation/client/arrow_flight_sql.md index d52ed630..f2cc2720 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.4.jar +java -jar softclient4es8-arrow-flight-sql-0.2.5.jar ``` | Elasticsearch | Artifact | |---------------|----------| -| ES 6.x | `softclient4es6-arrow-flight-sql-0.2.4.jar` | -| ES 7.x | `softclient4es7-arrow-flight-sql-0.2.4.jar` | -| ES 8.x | `softclient4es8-arrow-flight-sql-0.2.4.jar` | -| ES 9.x | `softclient4es9-arrow-flight-sql-0.2.4.jar` | +| 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` | --- diff --git a/documentation/client/download_analytics.md b/documentation/client/download_analytics.md index bf34903e..1852aaca 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.4` | The published artifact version | +| `version` | `0.2.5` | 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 c4a50574..a9b1f33f 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.4.jar` | -| ES 7.x | `softclient4es7-jdbc-driver-0.2.4.jar` | -| ES 8.x | `softclient4es8-jdbc-driver-0.2.4.jar` | -| ES 9.x | `softclient4es9-jdbc-driver-0.2.4.jar` | +| 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` | ### 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.4 + 0.2.5 ``` **Gradle:** ```groovy -implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.2.4' +implementation 'app.softnetwork.elastic:softclient4es8-jdbc-driver:0.2.5' ``` **sbt:** ```scala -libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.2.4" +libraryDependencies += "app.softnetwork.elastic" % "softclient4es8-jdbc-driver" % "0.2.5" ``` --- diff --git a/documentation/client/repl.md b/documentation/client/repl.md index 0ee93738..ec4c1c47 100644 --- a/documentation/client/repl.md +++ b/documentation/client/repl.md @@ -51,6 +51,26 @@ It provides: > 1.5.x and the JOIN engine is built on Apache Arrow 18.x — both ship Java-11 > bytecode. See [Extensions](#extensions-cross-index-joins-materialized-views). +**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, + 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 + 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`. + +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. + +On Linux and macOS `install.sh` still expects a suitable Java to be present. + ### Network Requirements - Network access to JFrog repository (`softnetwork.jfrog.io`) @@ -97,14 +117,18 @@ Windows client default is `Restricted`, and the machine may also be set to `install.ps1` with `-ExecutionPolicy Bypass` **for that one process**, changing nothing on the machine and needing no elevation. -Download **both** files, keep them in the same directory, then: +One file is enough. If `install.ps1` is not sitting next to it, `install.cmd` +downloads one: ```bat curl -O https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/main/install.cmd -curl -O https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/main/install.ps1 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`. + `install.cmd` accepts exactly the flags `install.ps1` does and forwards them verbatim, so every option, default and fallback documented below applies unchanged — there is no second implementation to drift: @@ -163,8 +187,9 @@ install.cmd -ListVersions -EsVersion 8 • 0.20.1 • 0.20.2 • 0.20.3 + • 0.20.4 - Total: 2 version(s) + Total: 4 version(s) To install a specific version: ./install.sh --es-version 8 --version @@ -192,7 +217,7 @@ install.cmd -ListVersions -EsVersion 8 ./install.sh --list-versions --es-version 8 # Install specific version -./install.sh --es-version 8 --version 0.20.3 +./install.sh --es-version 8 --version 0.20.4 # Install for Elasticsearch 9 (requires Java 17+) ./install.sh --es-version 9 @@ -201,7 +226,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.3 +./install.sh --target ~/tools/softclient4es --es-version 7 --version 0.20.4 ``` #### Windows @@ -214,7 +239,7 @@ install.cmd -ListVersions -EsVersion 8 .\install.ps1 -ListVersions -EsVersion 8 # Install specific version -.\install.ps1 -EsVersion 8 -Version 0.20.3 +.\install.ps1 -EsVersion 8 -Version 0.20.4 # Install for Elasticsearch 9 (requires Java 17+) .\install.ps1 -EsVersion 9 @@ -223,7 +248,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.3 +.\install.ps1 -Target "C:\tools\softclient4es" -EsVersion 7 -Version 0.20.4 ``` --- @@ -247,6 +272,8 @@ softclient4es/ │ # + extension jars and dependencies, see Extensions) ├── logs/ # Log files directory │ └── softclient4es.log # (created at runtime) +├── jdk/ # Windows only, and ONLY when the installer had to +│ └── bin/java.exe # bootstrap a JDK — the launcher prefers it ├── LICENSE ├── README.md ├── VERSION diff --git a/documentation/sql/joins.md b/documentation/sql/joins.md index bf2884d2..eedbf3ac 100644 --- a/documentation/sql/joins.md +++ b/documentation/sql/joins.md @@ -111,7 +111,48 @@ ORDER BY COUNT(*) DESC; -- Engineering (3), Marketing (2) survive HAVING ``` -> **Two ORDER BY gotchas:** the JOIN planner has two ordering restrictions — you cannot `ORDER BY` a **SELECT alias** (use `ORDER BY COUNT(*)`, not `ORDER BY cnt`), and you cannot `ORDER BY` a column that exists on **both** sides of the JOIN (order by a column unique to one side, e.g. `d.dept_name`, not the shared join key `d.dept_id`). +> **SELECT aliases and ordinals work here** — since arrow-extensions **0.2.5** (REPL bundle `0.20.4`, JDBC / ADBC / Flight SQL driver `0.2.5`). `ORDER BY cnt`, `HAVING cnt > 1`, `GROUP BY` on an alias and ordinal forms such as `ORDER BY 2` all resolve against the final SELECT list *after* the join, and alias matching is case-insensitive. Two limits remain: an alias is **not** legal in `SELECT`, `ON` or `WHERE` — nothing has been computed at that point — and it must be written **bare**, since `ORDER BY d.cnt` qualifies a name no table owns and fails inside DuckDB. Before 0.2.5 all of these were rejected with `Ambiguous column`. +> +> **The remaining ORDER BY gotcha:** you cannot `ORDER BY` a column that exists on **both** sides of the JOIN — order by a column unique to one side, e.g. `d.dept_name`, not the shared join key `d.dept_id`. + +### JOIN cardinality — fan-out on a non-unique key + +A JOIN on a key that is **not unique** on the other side multiplies rows. That is standard SQL and the engine is doing it correctly, but it is the easiest way to get plausible-looking wrong numbers, because the row multiplication is invisible in the output. + +With one tenant that has **2** EU error rows, **3** US rows and **2** AP rows: + +```sql +SELECT eu.tenant_id, + COUNT(*) AS eu_errors, + AVG(us.latency_ms) AS us_avg_latency, + AVG(ap.latency_ms) AS ap_avg_latency +FROM eu_events AS eu +JOIN us_events AS us ON eu.tenant_id = us.tenant_id +JOIN ap_events AS ap ON eu.tenant_id = ap.tenant_id +WHERE eu.level = 'ERROR' +GROUP BY eu.tenant_id; +``` + +`eu_errors` comes back as **12** — that is 2 × 3 × 2, one row per combination. The answer meant by the query is **2**. + +**Why this is worse than one wrong column.** Within a group whose rows all fan out by the same factor: + +| Aggregate | Under fan-out | +|---|---| +| `COUNT`, `SUM` | **inflated** by the fan-out factor | +| `AVG`, `MIN`, `MAX` | **unchanged** — uniform duplication preserves them | + +So in the example above both averages are exactly right, and only the count is wrong. Three columns out of four corroborate a result that is 6× off, and nothing in the output signals that a fan-out happened. *Do not use "the averages look sensible" as a sanity check on a JOIN.* (If the factor varies across rows within a group — because you grouped by something coarser than the join key — then `AVG` is silently weighted too, and it is wrong as well.) + +**What to do instead:** + +- Count a key from **one** side rather than rows of the joined product: `COUNT(DISTINCT eu.event_id)`. +- Or aggregate **before** joining, so each side contributes one row per key — a materialized view per leg is the durable form of this. +- Sanity-check the row count against the left side alone before adding aggregates. + +**`INNER JOIN` also drops rows.** A key absent from *any* joined table disappears from the result entirely — a tenant running in EU and US but not APAC vanishes from a query whose name says "every region". Use `LEFT JOIN` when the left side is the population you actually mean. + +**In Federation specifically:** each leg is staged and joined coordinator-local, so a fan-out inflates the staged intermediate *and* consumes the joined-output row cap (`maxQueryResults`, Community 10,000 / Pro 1,000,000). Hitting that cap is reported — see [Row truncation at the result cap](#row-truncation-at-the-result-cap) — but a truncated fan-out is still an answer to a question you did not ask. ### ORDER BY … LIMIT (top-N) diff --git a/install.cmd b/install.cmd index 2fc4aec1..3754490e 100644 --- a/install.cmd +++ b/install.cmd @@ -11,7 +11,9 @@ 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 Pass the same flags you would pass to install.ps1: +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 rem install.cmd rem install.cmd -ListVersions -EsVersion 8 @@ -28,14 +30,43 @@ rem =========================================================================== setlocal +set "PS1_URL=https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/refs/heads/main/install.ps1" set "PS1=%~dp0install.ps1" -if not exist "%PS1%" ( - echo [ERROR] install.ps1 was not found next to install.cmd. 1>&2 - echo [ERROR] Expected: %PS1% 1>&2 - echo [ERROR] Download both files from the same release and keep them together. 1>&2 - exit /b 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. +rem Written as goto + a separate errorlevel test rather than `if ... call ... ||`: +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 +:run powershell -NoProfile -ExecutionPolicy Bypass -File "%PS1%" %* exit /b %ERRORLEVEL% + +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 --------------------------------------------------------------------------- +:fetch_ps1 +where curl.exe >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] install.ps1 is not next to install.cmd and curl.exe is unavailable. 1>&2 + echo [ERROR] curl.exe ships with Windows 10 build 1803+ and Windows Server 2019+. 1>&2 + echo [ERROR] Download install.ps1 manually into the same directory as install.cmd. 1>&2 + exit /b 1 +) +set "PS1=%TEMP%\softclient4es-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%" +if errorlevel 1 ( + echo [ERROR] Could not download install.ps1 from %PS1_URL% 1>&2 + exit /b 1 +) +echo [INFO] Using %PS1% +exit /b 0 diff --git a/install.ps1 b/install.ps1 index 38b0108f..7a43fcdd 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.3 -NoExtensions + .\install.ps1 -EsVersion 7 -Version 0.20.4 -NoExtensions "@ exit 0 @@ -122,10 +122,17 @@ function Get-RequiredJavaVersion { } } -# Major version of the `java` on PATH, or 0 when it cannot be determined. -function Get-JavaMajorVersion { +# Major version reported by a SPECIFIC java executable, or 0 when it cannot be +# determined. Taking the exe as a parameter is what lets JAVA_HOME and the PATH +# `java` be probed by the same code - they routinely disagree. +function Get-JavaMajorFromExe { + param([string]$Exe) + if (-not $Exe) { return 0 } try { - $out = (& java -version 2>&1 | Select-String -Pattern 'version' | Select-Object -First 1).ToString() + # Select-Object -First 1 matters: with two matching lines (e.g. a + # deprecation notice from _JAVA_OPTIONS) the pipeline yields an array whose + # ToString() is "System.Object[]", no regex matches, and the version reads 0. + $out = (& $Exe -version 2>&1 | Select-String -Pattern 'version' | Select-Object -First 1).ToString() if ($out -match '"1\.(\d+)') { return [int]$Matches[1] } # 1.8.x elseif ($out -match '"(\d+)') { return [int]$Matches[1] } # 11.x, 17.x } @@ -133,8 +140,155 @@ function Get-JavaMajorVersion { return 0 } +# Major version of the `java` on PATH, or 0 when it cannot be determined. +function Get-JavaMajorVersion { + $onPath = Get-Command java -ErrorAction SilentlyContinue + if (-not $onPath) { return 0 } + return (Get-JavaMajorFromExe -Exe $onPath.Source) +} + $REQUIRED_JAVA_VERSION = Get-RequiredJavaVersion -EsVer $EsVersion +# The JDK this installer bootstraps when the host cannot satisfy the floor. 17 +# covers BOTH floors (11 for ES 6/7/8, 17 for ES 9), so there is one download to +# reason about rather than two. +$BOOTSTRAP_JAVA_VERSION = 17 +# Adoptium redirects this to the current GA Temurin 17 JDK *zip* for windows/x64 - +# an archive, deliberately not an MSI: unpacking needs no administrator rights. +$TEMURIN_ZIP_URL = "https://api.adoptium.net/v3/binary/latest/$BOOTSTRAP_JAVA_VERSION/ga/windows/x64/jdk/hotspot/normal/eclipse?project=jdk" +# Lives INSIDE the install tree: `uninstall.ps1` then removes it with everything +# else, and the launcher finds it by relative path with no machine-wide state. +$EMBEDDED_JDK_DIR = Join-Path $Target "jdk" + +# ============================================================================= +# Java resolution — probe, then bootstrap a portable JDK rather than give up +# ============================================================================= +# Resolution order, and it is the SAME order the generated launcher uses, which +# is what keeps "the installer worked" and "the REPL starts" from disagreeing: +# +# \jdk\bin\java.exe (bootstrapped here, if it was needed) +# -> %JAVA_HOME%\bin\java.exe +# -> `java` on PATH +# +# So JAVA_HOME, when it is defined, is the thing actually tested — not the PATH +# `java`, which is frequently a different and older JVM. + +# Set by Resolve-Java; read by Check-Prerequisites, the launcher writer and the +# summary. +$script:JavaMajor = 0 +$script:JavaSource = "not found" +$script:EmbeddedJdkHome = $null + +function Install-EmbeddedJdk { + Write-Info "Installing a portable Temurin $BOOTSTRAP_JAVA_VERSION JDK (zip, no administrator rights)..." + + $zip = Join-Path $env:TEMP "softclient4es-temurin$BOOTSTRAP_JAVA_VERSION.zip" + try { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + # Same reason as the JAR download: Write-Progress per chunk makes a + # ~180 MB download on Windows PowerShell 5.1 look hung. + $previousProgress = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + try { + Invoke-WebRequest -Uri $TEMURIN_ZIP_URL -OutFile $zip -UseBasicParsing -ErrorAction Stop + } + finally { $ProgressPreference = $previousProgress } + + # The archive unpacks as jdk-17.x.y+z\ — a version-stamped directory. Unpack + # 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. + $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 } + New-Item -ItemType Directory -Force -Path $staging | Out-Null + + try { + Expand-Archive -Path $zip -DestinationPath $staging -Force + + $inner = Get-ChildItem $staging -Directory | Select-Object -First 1 + if (-not $inner) { + Write-Err "The Temurin archive did not unpack as expected (no directory inside $staging)" + return $null + } + Move-Item -Path $inner.FullName -Destination $EMBEDDED_JDK_DIR + } + finally { + Remove-Item -Recurse -Force $staging -ErrorAction SilentlyContinue + Remove-Item $zip -ErrorAction SilentlyContinue + } + + $exe = Join-Path (Join-Path $EMBEDDED_JDK_DIR "bin") "java.exe" + if (-not (Test-Path $exe)) { + Write-Err "No java.exe under $EMBEDDED_JDK_DIR after unpacking" + return $null + } + + Write-Success "Installed Temurin $BOOTSTRAP_JAVA_VERSION to $EMBEDDED_JDK_DIR" + return $EMBEDDED_JDK_DIR + } + catch { + Write-Err "Could not download or unpack the Temurin JDK: $($_.Exception.Message)" + Write-Err "URL: $TEMURIN_ZIP_URL" + return $null + } +} + +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 } + + if ($jhMajor -gt 0) { + $script:JavaMajor = $jhMajor + $script:JavaSource = "JAVA_HOME ($env:JAVA_HOME)" + } + 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 -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 + if (-not $jdkHome) { + Write-Err "Java $REQUIRED_JAVA_VERSION or higher is required for ES$EsVersion and could not be installed." + Write-Err "Install a JDK ${REQUIRED_JAVA_VERSION}+ manually and re-run, or set JAVA_HOME to one." + 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 + Write-Success "Java $($script:JavaMajor) ready — JAVA_HOME and PATH updated for THIS session" + + return $true +} + # ============================================================================= # List Available Versions # ============================================================================= @@ -264,6 +418,13 @@ function Resolve-LatestVersion { return @($versions)[-1] } +# ============================================================================= +# 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. +if (-not (Resolve-Java)) { exit 1 } + # ============================================================================= # Bundle Selection: default install = ONE self-contained -all assembly # ============================================================================= @@ -296,11 +457,13 @@ if ($WITH_EXTENSIONS) { Write-Warn "No -all bundles published for $BUNDLE_ARTIFACT_NAME - falling back to the plain artifact" } - # The bundle needs Java 11+ (Arrow / logback bytecode). Check-Prerequisites - # aborts below the ES-version floor anyway; this keeps the fallback honest. - $javaMajor = Get-JavaMajorVersion - if ($USE_BUNDLE -and $javaMajor -gt 0 -and $javaMajor -lt 11) { - Write-Warn "Java $javaMajor found - the -all bundle requires Java 11+; falling back to the plain artifact" + # The bundle needs Java 11+ (Arrow / logback bytecode). Resolve-Java has already + # guaranteed >= $REQUIRED_JAVA_VERSION (11 or 17), bootstrapping a JDK if the + # host could not supply one, so this can only fire if that guarantee is ever + # weakened. Kept as a guard rather than deleted: silently shipping the bundle + # to a Java 8 host is a crash at first launch, not a warning. + if ($USE_BUNDLE -and $script:JavaMajor -gt 0 -and $script:JavaMajor -lt 11) { + Write-Warn "Java $($script:JavaMajor) found - the -all bundle requires Java 11+; falling back to the plain artifact" $USE_BUNDLE = $false $Version = $REQUESTED_VERSION } @@ -347,30 +510,19 @@ if ($USE_BUNDLE -and -not (Test-UrlExists -Url $DOWNLOAD_URL)) { # Check Prerequisites # ============================================================================= +# Resolve-Java already probed, and bootstrapped a JDK if it had to. This asserts +# the outcome rather than re-deriving it: ONE parse of `java -version` per run, so +# a localised or multi-line output cannot be read two different ways. function Check-Prerequisites { Write-Info "Checking prerequisites..." - if (-not (Get-Command java -ErrorAction SilentlyContinue)) { - Write-Err "Java is not installed." - Write-Err "ES$EsVersion requires Java $REQUIRED_JAVA_VERSION or higher." - exit 1 - } - - # One parse for the whole installer (Get-JavaMajorVersion), so a localised or - # multi-line `java -version` cannot be read two different ways. - $javaVersion = Get-JavaMajorVersion - - if ($javaVersion -eq 0) { - Write-Warn "Could not determine Java version" - } - elseif ($javaVersion -lt $REQUIRED_JAVA_VERSION) { + if ($script:JavaMajor -lt $REQUIRED_JAVA_VERSION) { Write-Err "Java $REQUIRED_JAVA_VERSION or higher is required for ES$EsVersion." - Write-Err "Found: Java $javaVersion" + Write-Err "Resolved: Java $($script:JavaMajor) via $($script:JavaSource)" exit 1 } - else { - Write-Success "Java $javaVersion found (required: ${REQUIRED_JAVA_VERSION}+)" - } + + Write-Success "Java $($script:JavaMajor) via $($script:JavaSource) (required: ${REQUIRED_JAVA_VERSION}+)" } # ============================================================================= @@ -694,6 +846,19 @@ if not exist "%JAR_FILE%" ( REM Create logs directory if it doesn't exist if not exist "%LOG_DIR%" mkdir "%LOG_DIR%" +REM Java resolution, in the SAME order the installer used: +REM 1. the JDK bundled into this install (present only when the installer had to +REM bootstrap one because the host had no Java, or too old a Java) +REM 2. %JAVA_HOME% +REM 3. whatever `java` is on PATH +REM Prepending to PATH rather than calling an absolute exe keeps every `java` +REM below unchanged and sidesteps quoting a path that contains spaces. `setlocal` +REM at the top means this PATH edit dies with the script. +REM Each `if` is its own line: cmd expands every %%VAR%% in a parenthesised block +REM in ONE parse pass, so a block would not see the value just assigned. +if exist "%BASE_DIR%\jdk\bin\java.exe" set "JAVA_HOME=%BASE_DIR%\jdk" +if defined JAVA_HOME if exist "%JAVA_HOME%\bin\java.exe" set "PATH=%JAVA_HOME%\bin;%PATH%" + if "%JAVA_OPTS%"=="" set JAVA_OPTS=-Xmx512m REM Java major version (1.8.x -> 8, 11.x -> 11), also our Java presence check. @@ -757,6 +922,21 @@ if (-not (Test-Path `$LogDir)) { New-Item -ItemType Directory -Path `$LogDir | Out-Null } +# Java resolution, in the SAME order the installer used: +# 1. the JDK bundled into this install (present only when the installer had to +# bootstrap one because the host had no Java, or too old a Java) +# 2. `$env:JAVA_HOME +# 3. whatever `java` is on PATH +# Prepending to PATH keeps every `java` below unchanged; the assignment is +# process-scoped, so it dies with this script. +`$BundledJdk = Join-Path `$BaseDir "jdk" +if (Test-Path (Join-Path `$BundledJdk "bin\java.exe")) { + `$env:JAVA_HOME = `$BundledJdk +} +if (`$env:JAVA_HOME -and (Test-Path (Join-Path `$env:JAVA_HOME "bin\java.exe"))) { + `$env:PATH = (Join-Path `$env:JAVA_HOME "bin") + ";" + `$env:PATH +} + # Check Java. Select-Object -First 1 matters: with two matching lines (e.g. a # deprecation notice from _JAVA_OPTIONS) the pipeline yields an array whose # ToString() is "System.Object[]", no regex matches, and the version reads 0. @@ -869,6 +1049,7 @@ Elasticsearch: $EsVersion Version: $Version Scala: $ScalaVersion Java Required: ${REQUIRED_JAVA_VERSION}+ +Java In Use: $($script:JavaMajor) via $($script:JavaSource) Artifact: $ARTIFACT_NAME Install type: $installType $licenseLine @@ -893,6 +1074,7 @@ function Print-Summary { Write-Host " Elasticsearch version: $EsVersion" Write-Host " SoftClient4ES version: $Version" Write-Host " Java required: ${REQUIRED_JAVA_VERSION}+" + Write-Host " Java in use: $($script:JavaMajor) via $($script:JavaSource)" if ($USE_BUNDLE) { Write-Host " Install type: -all bundle (engine + extensions, incl. cross-index JOIN)" } else { @@ -911,6 +1093,10 @@ function Print-Summary { Write-Host " | \-- $JAR_NAME" Write-Host " +-- logs\" Write-Host " | \-- (runtime logs)" + if ($script:EmbeddedJdkHome) { + Write-Host " +-- jdk\" + Write-Host " | \-- (bundled Temurin $BOOTSTRAP_JAVA_VERSION - the launcher prefers it)" + } if ($USE_BUNDLE) { Write-Host " +-- licenses\" Write-Host " | \-- (per-component licences)" diff --git a/install.sh b/install.sh index 98e130cf..983c55cc 100755 --- a/install.sh +++ b/install.sh @@ -174,7 +174,7 @@ Examples: $0 $0 --list-versions --es-version 8 $0 --target /opt/softclient4es --es-version 8 --no-extensions - $0 -t ~/tools/softclient4es -e 7 -v 0.20.3 --no-extensions + $0 -t ~/tools/softclient4es -e 7 -v 0.20.4 --no-extensions Detected OS: $OS_TYPE