Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.MD
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format
Bump to 0.1.0 to mark the move to JDK 25 (still 0.x — Adhoc is not yet API-stable).

### Added
- Pivotable Preferences modal: new "Enable Vue devtools" toggle. Persisted as `localStorage["adhoc.vueDevtools"]`; also activatable per-tab via the `?devtools` URL flag. The bootstrap in `index.html` reads the flag synchronously and sets `window.__VUE_PROD_DEVTOOLS__` before Vue's ESM module evaluates, so the Vue 3 devtools hook is exposed even on the minified build.
- Pivotable SPA: bumped vue-router 4.6.3 → 5.0.7 (which absorbs `unplugin-vue-router` into core; the SPA only uses the runtime `dist/vue-router.esm-browser.js`, no file-based routing) and vue 3.5.32 → 3.5.34 (vue-router 5.0.7's peer minimum). The vue-router 5 pom drags in unplugin build-time transitives that aren't all published as webjars; a wildcard `<exclusion>` blocks Maven from chasing them while the ESM browser bundle remains fully self-contained.
- Plan fragments: the live plan now also shows the table-engine secondary DAG (one node per merged `TableQueryV4`) and, for `JooqTableWrapper`-backed tables, an inlined-SQL leaf under each `TABLE_QUERY`. New write-only `IQueryPlanRegistry.publishFragment(queryId, anchor, subtree)` hook routes per-query observations into the matching `LiveQueryPlanSource`; idempotent within `(anchor, subtree.subject)`. New `IPlanFragmentSink` + `PlanFragmentScope` (JDK 25 `ScopedValue`) let any engine extension publish sub-nodes without learning the `(registry, queryId)` pair.
- Pivotable SPA: top-level `<BackendStatusBanner>` surfaces backend outages (metadata probe 5xx / network error) as a red alert with a "Retry now" button and a countdown to the next auto-retry (capped exponential backoff: 2s, 5s, 10s, 30s, 60s). State exposed on the adhoc pinia store as `backendStatus`.
- New `GET /api/v1/cubes/queries/{queryUuid}/plan/{summary,snapshot,children}` endpoints (webmvc + webflux). `queryUuid` is the UUID returned by `POST /cubes/query/asynchronous`; the engine adopts it as its own `AdhocQueryId.queryId` (see `SubmittedQueryIdScope`). Response contract: 200 when the plan is registered, 204+`Retry-After: 1` while still queueing, 204 for any terminal "no plan to serve" state, 404 only when the endpoint itself is missing — see [`docs/api.md`](docs/api.md). Default registry is `BoundedQueryPlanRegistry` (cap = 200 000 nodes).
Expand Down
14 changes: 9 additions & 5 deletions CLAUDE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,23 @@ New executor jobs are proposed monthly by the Discovery Agent (see `.claude/disc

## Updating a JS dependency (Pivotable SPA)

JavaScript libraries served to the SPA are wired through **three distinct layers** that MUST all be updated together. Renovate (or any manual bump) typically only touches the WebJar version in `pivotable/js/pom.xml`; the other layers do **not** auto-update and a partial update silently breaks one of the four loading modes (webjars / cdn × min / dev). Before declaring a JS-dep bump done, walk through every layer:
JavaScript libraries served to the SPA are wired through **FIVE distinct layers** that MUST all be updated together. Renovate (or any manual bump) typically only touches the WebJar version in `pivotable/js/pom.xml`; the other layers do **not** auto-update and a partial update silently breaks one of the four loading modes (webjars / cdn × min / dev). Before declaring a JS-dep bump done, walk through every layer:

1. **WebJar version (`pivotable/js/pom.xml`)** — the `<version>` of the `org.webjars[.npm]:<artifact>` dependency. This is what Spring Boot serves under `/webjars/<artifact>/<version>/...`.
2. **All four importmaps under `pivotable/js/src/main/resources/static/ui/`** — `importmap-webjars.json`, `importmap-webjars-min.json`, `importmap-cdn.json`, `importmap-cdn-min.json`. The version segment of every URL referencing the bumped library MUST match the pom.xml version. The unit test `unit-tests/index-html-default-mode.spec.js` (`cdn importmap mirrors the webjars importmap — same keys, same versions` and `min importmaps carry the same keys and versions as their full-build counterparts`) enforces alignment between the four files but does **not** check alignment with `pom.xml`.
3. **Direct `<script>` / `<link>` tags in `pivotable/js/src/main/resources/static/index.html`** — some libraries (bootstrap CSS, bootstrap-icons, slickgrid CSS) are loaded as stylesheets *outside* the importmap via the `STYLESHEETS` table near the top of `index.html`. Each entry has hardcoded `cdn`, `cdnMin`, `webjars`, `webjarsMin` URLs that embed the version. These MUST be bumped in the same change.
2. **npm version (`pivotable/js/package.json`)** — the `<version>` of the corresponding `devDependencies` entry. Drives `npm install`, the local vitest run and the Vite dev server. Renovate raises this as a **separate PR** from the webjars one (one comes from the npm registry, the other from Maven Central / webjars), and the two must land at the same version.
3. **All four importmaps under `pivotable/js/src/main/resources/static/ui/`** — `importmap-webjars.json`, `importmap-webjars-min.json`, `importmap-cdn.json`, `importmap-cdn-min.json`. The version segment of every URL referencing the bumped library MUST match the pom.xml version. The unit test `unit-tests/index-html-default-mode.spec.js` (`cdn importmap mirrors the webjars importmap — same keys, same versions` and `min importmaps carry the same keys and versions as their full-build counterparts`) enforces alignment between the four files but does **not** check alignment with `pom.xml`.
4. **Direct `<script>` / `<link>` tags in `pivotable/js/src/main/resources/static/index.html`** — some libraries (bootstrap CSS, bootstrap-icons, slickgrid CSS) are loaded as stylesheets *outside* the importmap via the `STYLESHEETS` table near the top of `index.html`. Each entry has hardcoded `cdn`, `cdnMin`, `webjars`, `webjarsMin` URLs that embed the version. These MUST be bumped in the same change.
5. **`pivotable/js/vite.config.js`** — the Vite dev server resolves bare module specifiers (e.g. `import "mermaid"`) against an inline mapping that mirrors the webjars importmap. Each entry hardcodes a `/webjars/<artifact>/<version>/...` URL; out-of-date versions here mean Vitest unit specs (which run under jsdom and follow the same resolution table) load a mismatched library, plus dev-server hot-reload pulls stale assets.

`lib-version-alignment.spec.js` (under `unit-tests/`) is the cross-layer check — it parses every layer above and fails when any URL is out of sync. The hardcoded-versions assertion in `index-html-default-mode.spec.js` is also a layer cross-check; when adding a new dep, append its `/webjars/<artifact>/<version>/...` path to its `REQUIRED_WEBJAR_PATHS` so a missed bump is caught at vitest time.

**Sanity-check after a bump:**

```bash
cd pivotable/js && npx vitest run unit-tests/index-html-default-mode.spec.js
cd pivotable/js && npx vitest run unit-tests/index-html-default-mode.spec.js unit-tests/lib-version-alignment.spec.js
```

This verifies the four importmaps stay aligned and every webjars URL embeds a version segment.
These verify the five layers stay aligned and every webjars URL embeds a version segment.

**If the library is unused** (declared in `pom.xml` but referenced by no importmap, no script tag, no JS import): a Renovate bump has no SPA-side counterpart to update — the bump is harmless but pointless. Flag it to the user; the right follow-up is usually to drop the dead `pom.xml` dependency rather than to invent an importmap entry.

Expand Down
15 changes: 15 additions & 0 deletions pivotable/js/e2e-tests/_url.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @ts-check
//
// Shared base URL for every spec under `e2e-tests/`. Reads `PIVOTABLE_BASE_URL` from the
// environment so a developer can point the suite at a non-default backend without editing
// any spec — e.g. when the default `:8080` slot is already occupied by another instance
// and the user is running their backend on `:8090`:
//
// PIVOTABLE_BASE_URL=http://localhost:8090 npx playwright test --project=chromium
//
// Defaults to `http://localhost:8080`, matching the historical behaviour and the
// `webServer` recipe in `pivotable/CONTRIBUTING.md`.
//
// Exported as `BASE_URL` (not `URL`) to avoid shadowing the global `URL` constructor.

export const BASE_URL = process.env.PIVOTABLE_BASE_URL ?? "http://localhost:8080";
2 changes: 1 addition & 1 deletion pivotable/js/e2e-tests/localhost8080-column-chip.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
2 changes: 1 addition & 1 deletion pivotable/js/e2e-tests/localhost8080-drillthrough.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import queryPivotable from "./query-pivotable.mjs";
// We reach the page via the in-app `Show schema` link rather than navigating directly,
// so the test also doubles as a regression for that link.

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
2 changes: 1 addition & 1 deletion pivotable/js/e2e-tests/localhost8080-grid-autofit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import queryPivotable from "./query-pivotable.mjs";
// guarantee: it drives a click on the icon in a real browser against a real
// SlickGrid instance, and asserts the column ordering is unchanged.

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { test, expect } from "@playwright/test";
// vue runtime under `?cdn` in dev mode) silently crashes the SPA on first render and
// shows nothing but the splash. Without an automated check, the regression escapes to
// production.
const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

const COMBINATIONS = [
{ name: "default (webjars + min)", search: "" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
2 changes: 1 addition & 1 deletion pivotable/js/e2e-tests/localhost8080-queries.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect, request } from "@playwright/test";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
// Create a new repository
Expand Down
2 changes: 1 addition & 1 deletion pivotable/js/e2e-tests/localhost8080-query-chatbot.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import queryPivotable from "./query-pivotable.mjs";
// stack — login, query submission, grid render — so the SPA's mount logic (`tabularView.queryUuid` gate, watch
// on prop change) is still wired through real code.

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import queryPivotable from "./query-pivotable.mjs";
// `running` states are too fast to observe deterministically on this fixture — covered by the Vitest reducer spec
// instead.

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import queryPivotable from "./query-pivotable.mjs";
// `127.0.0.1:8080`, one over `localhost:8080` — same backend, different host literal,
// proving the URL synthesis treats each as a distinct entry.

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
2 changes: 1 addition & 1 deletion pivotable/js/e2e-tests/localhost8080-token-expiry.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from "./_coverage-fixture.mjs";

import queryPivotable from "./query-pivotable.mjs";

const url = "http://localhost:8080";
import { BASE_URL as url } from "./_url.mjs";

test.beforeAll(async ({ request }) => {
const response = await queryPivotable.clear(request, url);
Expand Down
4 changes: 3 additions & 1 deletion pivotable/js/e2e-tests/query-pivotable.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @ts-check
import { expect } from "@playwright/test";

import { BASE_URL } from "./_url.mjs";

const addColumn = async function (page, column) {
await page.getByRole("searchbox", { name: "Search" }).dblclick();
await page.getByRole("searchbox", { name: "Search" }).fill(column);
Expand Down Expand Up @@ -55,7 +57,7 @@ export default {
addColumn: addColumn,

async queryPivotable(page) {
await page.goto("http://localhost:8080/");
await page.goto(BASE_URL + "/");
await page.getByRole("link", { name: /You need to login/ }).click();
await page.getByRole("link", { name: "pivotable-unsafe_fakeuser" }).click();
await page.getByRole("button", { name: /^Login$/i }).click();
Expand Down
Loading
Loading