diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index a61fd29..7e7e0ed 100644 --- a/.github/workflows/build_deploy.yml +++ b/.github/workflows/build_deploy.yml @@ -76,4 +76,100 @@ jobs: docker.io/eclipsedaanse/probe:snapshot ghcr.io/eclipse-daanse/probe:snapshot # docker.io/eclipsedaanse/probe:${{ github.sha }} -# ghcr.io/eclipse-daanse/probe:${{ github.sha }} \ No newline at end of file +# ghcr.io/eclipse-daanse/probe:${{ github.sha }} + + pivot_container_deploy: + if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} + needs: build_test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + db: [postgres, mssql, oracle, duckdb, mysql, mariadb, h2] + + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 + + - name: Download pivot artifacts + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + with: + name: jar-files + path: . + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 25 + cache: maven + + - name: Set up QEMU + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.2.1 + with: + platforms: all + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + + # Build the image for the host platform and load it into the local + # Docker daemon so the integration test can verify it before it is pushed. + - name: Build Docker Image for verification + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: . + file: "application/pivot/${{ matrix.db }}/container/Dockerfile" + platforms: linux/amd64 + load: true + tags: daanse-pivot-${{ matrix.db }}:ci + + # Runs the Testcontainers integration test against the image built above + # (-Dpivot.image) instead of building a throwaway image. + - name: Verify Docker Image with integration test + run: > + mvn -B -am -pl application/pivot/common,application/pivot/${{ matrix.db }} + package failsafe:integration-test failsafe:verify + -Dpivot.image=daanse-pivot-${{ matrix.db }}:ci + + # The Docker Hub description is the database specific README plus the + # shared container documentation maintained once in the common module. + - name: Assemble Docker Hub description + run: > + cat application/pivot/${{ matrix.db }}/README.md + application/pivot/common/README.container.md + > readme-dockerhub.md + + - name: Update dockerhub repo description + uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + repository: eclipsedaanse/daanse-pivot-${{ matrix.db }} + readme-filepath: ./readme-dockerhub.md + + - name: Login to Docker Hub + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Login to Github Docker Registry + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push Multi-Arch Docker Image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: . + file: "application/pivot/${{ matrix.db }}/container/Dockerfile" + platforms: linux/amd64,linux/arm64/v8 + push: true + tags: | + docker.io/eclipsedaanse/daanse-pivot-${{ matrix.db }}:snapshot + ghcr.io/eclipse-daanse/daanse-pivot-${{ matrix.db }}:snapshot \ No newline at end of file diff --git a/application/pivot/.gitignore b/application/pivot/.gitignore deleted file mode 100644 index 6cfd5c7..0000000 --- a/application/pivot/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -log/* -!log/.keep -catalog/* -!catalog/.keep -load/* -!load/.keep \ No newline at end of file diff --git a/application/pivot/README.md b/application/pivot/README.md index 41658ad..01e173f 100644 --- a/application/pivot/README.md +++ b/application/pivot/README.md @@ -4,45 +4,79 @@ group: Applications --- # Eclipse Daanse Pivot Application -**Status: Under Construction** +Pivot is the production Daanse OLAP (XMLA) server. It is built once per supported +database, so every image ships exactly one JDBC DataSource implementation, its +driver and the matching SQL dialect — nothing more. -Pivot is the foundation for every production Daanse OLAP server. This container can be adapted to specific needs and requirements. +| Database | Module | Container image | +|------------|------------|------------------------------------------| +| PostgreSQL | `postgres` | `eclipsedaanse/daanse-pivot-postgres` | +| MS SQL Server | `mssql` | `eclipsedaanse/daanse-pivot-mssql` | +| Oracle | `oracle` | `eclipsedaanse/daanse-pivot-oracle` | +| DuckDB | `duckdb` | `eclipsedaanse/daanse-pivot-duckdb` | +| MySQL | `mysql` | `eclipsedaanse/daanse-pivot-mysql` | +| MariaDB | `mariadb` | `eclipsedaanse/daanse-pivot-mariadb` | +| H2 | `h2` | `eclipsedaanse/daanse-pivot-h2` | -## Key Configuration Areas +All images are published with the `:snapshot` tag to Docker Hub +(`docker.io/eclipsedaanse/...`) and the GitHub Container Registry +(`ghcr.io/eclipse-daanse/...`). -### Database Connectivity -- **Dialects and Data Sources**: Support for multiple different database connections -- **Multi-Database Support**: Can connect to various database types simultaneously +## How it works -### XMLA Endpoints -- **Multiple Endpoints**: Configure the number of XMLA endpoints -- **Path Configuration**: Customize endpoint paths -- **Port Mapping**: Assign specific ports to different endpoints +The `common` module contains the shared runtime description +(`daanse.pivot.base.bndrun`) and a setup service that reads the common +environment variables and wires the server: catalog mapping provider → +context group → XMLA service → XMLA servlet (+ optional CORS filter). -### Catalog Management -- **Web Endpoint Assignment**: Map catalogs to specific web endpoints -- **Flexible Routing**: Configure how catalogs are accessed through different entry points +Each database module contributes a small configurator component that reads the +`DAANSE_JDBC_*` environment variables, registers the single DataSource and +creates the OLAP context against it. Every property offered by the underlying +Daanse DataSource bundle is configurable; unset variables fall back to the +bundle defaults. -### Observability Features -- **Health Checks**: Built-in health monitoring capabilities -- **Readiness Checks**: Ensure services are ready to handle requests -- **Caching Configuration**: Configurable caching settings for optimal performance +## Configuration -### Security & Authentication -- **Authentication Providers**: Support for necessary authentication mechanisms -- **Full Control**: Complete control over security configurations -- **Customizable Access**: Adapt authentication to organizational needs +The server is configured exclusively through environment variables. -## Benefits +Common variables (all images): see the per-image README in each module +(`postgres/README.md`, ...) — they document the shared set +(`DAANSE_CATALOG_RESOURCE`, `DAANSE_XMLA_PATH`, `DAANSE_CORS_*`, ...) plus the +full database specific `DAANSE_JDBC_*` set. -### Security & Risk Reduction -This container provides full control and adaptability while ensuring that unnecessary components are not activated at runtime, significantly reducing security risks. +The OLAP catalog (mapping `.xmi` files) is provided via a volume mounted at +`/app/catalog`. A single file is referenced by `DAANSE_CATALOG_RESOURCE`; +additional cross-referenced resources can be included with +`DAANSE_CATALOG_ADDITIONAL_GLOBS`. -### Production Ready -Designed specifically for production environments with enterprise-grade features and reliability. +## Authentication -### Customization -Every aspect can be tailored to meet specific organizational requirements and use cases. +By default the XMLA endpoint is anonymous. Every image can authenticate via +HTTP Basic against an LDAP directory (`DAANSE_LDAP_*` variables, roles from +LDAP groups); `DAANSE_AUTH_ANONYMOUS=false` enforces a login. A self contained +example with an LLDAP side container lives in `common/example/ldap`. + +## Examples + +Self contained demos live in `common/example`: a Docker Compose demo +(`compose/`), the same demo as a Kubernetes pod for Podman/`kubectl` +(`kube/`), and a Compose demo with LDAP authentication (`ldap/`). Each embeds +a seeded database and a minimal catalog in one file. + +## Container builds + +Each database module carries its image description in `/container/Dockerfile` +and a Testcontainers integration test that builds the image from that +Dockerfile and verifies it end-to-end (XMLA query against a real database). +CI builds every image in a matrix, runs the integration test against the built +image (`-Dpivot.image=...`) and pushes it multi-arch to Docker Hub and GHCR. +`build_images.sh` does the same locally: + +```bash +./build_images.sh # build all images as daanse-pivot-:local +TAG=dev ./build_images.sh postgres h2 # selected databases, custom tag +PUSH=true IMAGE_PREFIX=docker.io/eclipsedaanse ./build_images.sh +``` ## Source Code -The pivot application source can be found at: `https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot` \ No newline at end of file +The pivot application source can be found at: `https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot` diff --git a/application/pivot/build_images.sh b/application/pivot/build_images.sh new file mode 100755 index 0000000..34dd1b2 --- /dev/null +++ b/application/pivot/build_images.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +#******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +#******************************************************************************* +# +# Builds the pivot server jars and container images and verifies each image +# with its Testcontainers integration test. The tests run against exactly the +# image built here (-Dpivot.image), the same flow the CI pipeline uses. +# +# Usage: ./build_images.sh [db ...] +# db one or more of: postgres mssql oracle duckdb mysql mariadb h2 +# (default: all) +# +# Environment: +# TAG image tag (default: local) +# PUSH set to "true" to push the verified images +# IMAGE_PREFIX registry/namespace for pushed images +# (default: docker.io/eclipsedaanse) +set -euo pipefail + +ALL_DBS=(postgres mssql oracle duckdb mysql mariadb h2) +DBS=("$@") +if [ ${#DBS[@]} -eq 0 ]; then + DBS=("${ALL_DBS[@]}") +fi + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TAG="${TAG:-local}" +PUSH="${PUSH:-false}" +IMAGE_PREFIX="${IMAGE_PREFIX:-docker.io/eclipsedaanse}" + +# The build requires JDK 25 (bndruns target JavaSE-25). +JAVA_MAJOR="$(java -version 2>&1 | sed -nE 's/.*version "([0-9]+).*/\1/p' | head -1)" +if [ "${JAVA_MAJOR:-0}" -lt 25 ]; then + echo "ERROR: JDK 25+ required, found ${JAVA_MAJOR:-none}. Set JAVA_HOME accordingly." >&2 + exit 1 +fi + +# Testcontainers may be configured to a different container daemon than the +# docker CLI default (docker.host in ~/.testcontainers.properties, e.g. a +# rootless Podman socket). Build against the same daemon so the verification +# tests find the image. +TC_PROPS="$HOME/.testcontainers.properties" +if [ -z "${DOCKER_HOST:-}" ] && [ -f "$TC_PROPS" ]; then + TC_HOST="$(sed -nE 's/^docker\.host=(.+)$/\1/p' "$TC_PROPS" | head -1)" + TC_HOST="${TC_HOST//\\/}" + if [ -n "$TC_HOST" ]; then + export DOCKER_HOST="$TC_HOST" + echo "==> Using container daemon from ${TC_PROPS}: ${DOCKER_HOST}" + fi +fi + +MODULES="application/pivot/common" +for db in "${DBS[@]}"; do + MODULES="${MODULES},application/pivot/${db}" +done + +cd "$REPO_ROOT" + +echo "==> Building jars for: ${DBS[*]}" +mvn -B -am -pl "$MODULES" package + +for db in "${DBS[@]}"; do + echo "==> Building image daanse-pivot-${db}:${TAG}" + # --load makes buildx builders load the result into the image store + docker build --load -f "application/pivot/${db}/container/Dockerfile" \ + -t "daanse-pivot-${db}:${TAG}" "$REPO_ROOT" +done + +for db in "${DBS[@]}"; do + echo "==> Verifying daanse-pivot-${db}:${TAG} with the integration test" + mvn -B -pl "application/pivot/${db}" \ + failsafe:integration-test failsafe:verify \ + "-Dpivot.image=daanse-pivot-${db}:${TAG}" +done + +if [ "$PUSH" = "true" ]; then + for db in "${DBS[@]}"; do + image="${IMAGE_PREFIX}/daanse-pivot-${db}:${TAG}" + echo "==> Pushing ${image}" + docker tag "daanse-pivot-${db}:${TAG}" "$image" + docker push "$image" + done +fi + +echo "==> Done: ${DBS[*]} (tag: ${TAG})" diff --git a/application/pivot/catalog/.keep b/application/pivot/catalog/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/application/pivot/common/README.container.md b/application/pivot/common/README.container.md new file mode 100644 index 0000000..0b2f902 --- /dev/null +++ b/application/pivot/common/README.container.md @@ -0,0 +1,226 @@ +## The pivot container + +The Daanse pivot server ships the Daanse ROLAP engine and the XMLA endpoint +together with exactly one JDBC DataSource implementation, its driver and the +matching SQL dialect. It is configured entirely through environment variables; +unset variables keep the defaults of the underlying Daanse bundles. + +The HTTP port inside the container is `8080`. The XMLA endpoint is available +at the path configured by `DAANSE_XMLA_PATH` (default `/xmla`). + +If a required variable is missing, the configurator logs an error and the +server starts **without** an OLAP context: the endpoint answers, but knows no +catalogs and returns empty results. Check the container log first when a +query comes back empty. + +## Try it — complete demos + +The repository ships self contained demos under +[`application/pivot/common/example`](https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot/common/example) — +each is one file embedding a seeded database and a minimal OLAP catalog, and +answers an MDX query right after start: + +| Example | Run with | +|---|---| +| [`compose/docker-compose.yml`](https://github.com/eclipse-daanse/org.eclipse.daanse.server/blob/main/application/pivot/common/example/compose/docker-compose.yml) | `docker compose up` | +| [`kube/pivot-pod.yaml`](https://github.com/eclipse-daanse/org.eclipse.daanse.server/blob/main/application/pivot/common/example/kube/pivot-pod.yaml) | `podman kube play pivot-pod.yaml` or `kubectl apply -f pivot-pod.yaml` | +| [`ldap/docker-compose.yml`](https://github.com/eclipse-daanse/org.eclipse.daanse.server/blob/main/application/pivot/common/example/ldap/docker-compose.yml) | `docker compose up` — adds LDAP authentication (see below) | + +The Kubernetes pod, abbreviated — the same YAML runs unchanged with rootless +Podman and on a cluster (there, replace the `hostPort` with a Service): + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: daanse-pivot-demo +data: + init.sql: | + CREATE TABLE "Fact" ("KEY" VARCHAR(100), "VALUE" INTEGER); + INSERT INTO "Fact" VALUES ('A', 1), ('B', 2), ('C', 3); + catalog.xmi: | + ...your OLAP catalog mapping... +--- +apiVersion: v1 +kind: Pod +metadata: + name: daanse-pivot-demo +spec: + containers: + - name: db + image: docker.io/library/postgres:17-alpine + env: + - { name: POSTGRES_DB, value: demo } + - { name: POSTGRES_USER, value: daanse } + - { name: POSTGRES_PASSWORD, value: secret } + volumeMounts: + - { name: demo, mountPath: /docker-entrypoint-initdb.d/init.sql, subPath: init.sql } + - name: pivot + image: docker.io/eclipsedaanse/daanse-pivot-postgres:snapshot + ports: + - { containerPort: 8080, hostPort: 8080 } + env: + # containers in a pod share the network namespace + - { name: DAANSE_JDBC_HOST, value: localhost } + - { name: DAANSE_JDBC_DBNAME, value: demo } + - { name: DAANSE_JDBC_USER, value: daanse } + - { name: DAANSE_JDBC_PASSWORD, value: secret } + volumeMounts: + - { name: demo, mountPath: /app/catalog/catalog.xmi, subPath: catalog.xmi } + volumes: + - name: demo + configMap: + name: daanse-pivot-demo +``` + +## Catalog + +The OLAP catalog (Daanse ROLAP mapping, `.xmi`) is mounted at `/app/catalog`. +By default the file `/app/catalog/catalog.xmi` is loaded. If the mapping is +split over several cross-referencing files, point +`DAANSE_CATALOG_ADDITIONAL_GLOBS` at them, e.g. +`DAANSE_CATALOG_ADDITIONAL_GLOBS=/app/catalog/**/*.xmi`. + +## Common environment variables + +| Variable | Default | Description | +|---|---|---| +| `DAANSE_CATALOG_RESOURCE` | `/app/catalog/catalog.xmi` | Primary catalog mapping file (EMF XMI with a Catalog root element) | +| `DAANSE_CATALOG_ADDITIONAL_GLOBS` | *(unset)* | Comma separated glob patterns for additional mapping resources | +| `DAANSE_CONTEXT_NAME` | `daanse` | Name of the OLAP context | +| `DAANSE_CONTEXT_DESCRIPTION` | `Daanse XMLA Server` | Description of the OLAP context | +| `DAANSE_USE_AGGREGATES` | `false` | Use aggregate tables | +| `DAANSE_XMLA_PATH` | `/xmla` | Servlet pattern of the XMLA endpoint | +| `DAANSE_CORS_ENABLED` | `true` | Register the CORS filter | +| `DAANSE_CORS_ALLOWED_ORIGINS` | `*` | Comma separated allowed origins | +| `DAANSE_CORS_ALLOWED_HEADERS` | `*` | Comma separated allowed headers | +| `DAANSE_CORS_ALLOW_CREDENTIALS` | `true` | Allow credentials in CORS requests | + +## Authentication + +By default the endpoint is **anonymous**: every request is served, carrying no +roles — so only catalogs without access control answer. Real authentication in +front of the container (reverse proxy) is always an option; the image itself +offers HTTP Basic authentication against an LDAP directory. + +Setting `DAANSE_LDAP_URL` switches it on: credentials are verified by an LDAP +bind, and the caller's roles come from LDAP groups when +`DAANSE_LDAP_GROUP_SEARCH_BASE` is set (roles are intersected with the roles +the catalog declares — name the groups after the catalog's roles). Anonymous +requests are still served until `DAANSE_AUTH_ANONYMOUS=false`, which makes the +endpoint challenge every request. + +| Variable | Default | Description | +|---|---|---| +| `DAANSE_AUTH_ANONYMOUS` | `true` | Serve requests without credentials (with no roles) | +| `DAANSE_AUTH_REALM` | `Daanse XMLA` | Realm of the HTTP Basic challenge | +| `DAANSE_LDAP_URL` | *(unset)* | LDAP server, e.g. `ldaps://ldap.example.org:636`; setting it enables authentication | +| `DAANSE_LDAP_USER_DN_PATTERN` | *(unset)* | Direct bind DN pattern, e.g. `uid={0},ou=people,dc=example,dc=org` | +| `DAANSE_LDAP_USER_SEARCH_BASE` | *(unset)* | Search base for users (alternative to the DN pattern) | +| `DAANSE_LDAP_USER_SEARCH_FILTER` | `(uid={0})` | Search filter for users | +| `DAANSE_LDAP_SERVICE_BIND_DN` | *(unset)* | Service account for searches | +| `DAANSE_LDAP_SERVICE_BIND_PASSWORD` | *(unset)* | Password of the service account | +| `DAANSE_LDAP_TRANSPORT_SECURITY` | `LDAPS` | `LDAPS`, `STARTTLS` or `NONE` | +| `DAANSE_LDAP_ALLOW_UNENCRYPTED` | `false` | Must be `true` for `NONE` — passwords then cross the wire in the clear | +| `DAANSE_LDAP_CONNECT_TIMEOUT_MILLIS` | `5000` | Connect timeout | +| `DAANSE_LDAP_READ_TIMEOUT_MILLIS` | `10000` | Read timeout | +| `DAANSE_LDAP_REFERRAL` | `follow` | Referral handling | +| `DAANSE_LDAP_GROUP_SEARCH_BASE` | *(unset)* | Search base for the caller's groups; setting it enables role resolution | +| `DAANSE_LDAP_GROUP_SEARCH_FILTER` | `(member={0})` | Search filter for groups | +| `DAANSE_LDAP_GROUP_NAME_ATTRIBUTE` | `cn` | Attribute holding the group (= role) name | +| `DAANSE_LDAP_MEMBER_OF_ATTRIBUTE` | *(unset)* | Read groups from this user attribute instead of searching | + +### No LDAP at hand? Use a side container + +The repository ships a complete example under +[`application/pivot/common/example/ldap`](https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot/common/example/ldap): +one self contained `docker-compose.yml` that runs an [LLDAP](https://github.com/lldap/lldap) +side container next to the pivot — a lightweight LDAP server whose image comes +from the LLDAP project itself. The users (`admin`/`admin123`, +`analyst`/`analyst123`) and the group `Administrator` are plain text JSON +configs embedded in the compose file and loaded by LLDAP's official bootstrap +script; no extra files needed. LLDAP also serves a web UI on port 17170 to +manage users and groups later. + +```yaml +configs: + groups.json: + content: | + { "name": "Administrator" } + admin.json: + content: | + { "id": "admin", "email": "admin@example.org", "groups": ["lldap_admin", "Administrator"] } + analyst.json: + content: | + { "id": "analyst", "email": "analyst@example.org", "password": "analyst123" } + +services: + lldap: + image: lldap/lldap:stable + environment: + LLDAP_LDAP_BASE_DN: dc=example,dc=org + LLDAP_LDAP_USER_PASS: admin123 + LLDAP_KEY_SEED: change-me-for-anything-real + LLDAP_JWT_SECRET: change-me-too-at-least-32-characters-long + ports: ["17170:17170"] # web UI, optional + + lldap-bootstrap: # one-shot job loading the users and groups above + image: lldap/lldap:stable + depends_on: [lldap] + entrypoint: ["/bin/sh", "-c", "sleep 2 && /app/bootstrap.sh"] + restart: on-failure + environment: + LLDAP_URL: http://lldap:17170 + LLDAP_ADMIN_USERNAME: admin + LLDAP_ADMIN_PASSWORD: admin123 + DO_CLEANUP: "false" + configs: + - { source: groups.json, target: /bootstrap/group-configs/groups.json } + - { source: admin.json, target: /bootstrap/user-configs/admin.json } + - { source: analyst.json, target: /bootstrap/user-configs/analyst.json } + + pivot: + image: eclipsedaanse/daanse-pivot-postgres:snapshot # any daanse-pivot- + depends_on: [lldap] + ports: ["8080:8080"] + volumes: ["./catalog:/app/catalog:ro"] + environment: + # ... DAANSE_JDBC_* of your database ... + DAANSE_LDAP_URL: ldap://lldap:3890 + DAANSE_LDAP_TRANSPORT_SECURITY: NONE + DAANSE_LDAP_ALLOW_UNENCRYPTED: "true" + DAANSE_LDAP_USER_DN_PATTERN: uid={0},ou=people,dc=example,dc=org + DAANSE_LDAP_SERVICE_BIND_DN: uid=admin,ou=people,dc=example,dc=org + DAANSE_LDAP_SERVICE_BIND_PASSWORD: admin123 + DAANSE_LDAP_GROUP_SEARCH_BASE: ou=groups,dc=example,dc=org +``` + +Then query with `curl -u admin:admin123 ...` — or edit the inline JSON to your +own users and groups; the group names are the roles. + +## Connection pool + +The engine never talks to the DataSource directly. A single MDX query fans out +to as many statements as the segment cache has threads, so all of them go +through a pool. The defaults are sized for that fan-out; lower +`DAANSE_POOL_MAXIMUM_POOL_SIZE` if the database has a stricter connection limit +than the server does — Oracle refuses rather than queues once `processes` is +reached. + +| Variable | Default | Description | +|---|---|---| +| `DAANSE_POOL_POOL_NAME` | *(unset)* | Pool name; shows up in thread names and metrics | +| `DAANSE_POOL_MAXIMUM_POOL_SIZE` | `100` | Upper bound on physical connections | +| `DAANSE_POOL_MINIMUM_IDLE` | `5` | Connections kept open while idle | +| `DAANSE_POOL_CONNECTION_TIMEOUT` | `30000` | How long a caller waits for a free connection (ms) | +| `DAANSE_POOL_IDLE_TIMEOUT` | `600000` | Idle connections are evicted after this (ms) | +| `DAANSE_POOL_MAX_LIFETIME` | `1800000` | A connection is retired this long after it was opened (ms) | +| `DAANSE_POOL_LEAK_THRESHOLD` | `300000` | A connection held longer than this is reported (ms, 0 disables) | +| `DAANSE_POOL_READ_ONLY` | *(matches the DataSource)* | Mode the pool hands connections out in. The image sets it from its own DataSource — DuckDB opens read-only and rejects a change on the connection. Override only if you know the driver allows it. | + +## Mount points + +| Path | Purpose | +|---|---| +| `/app/catalog` | OLAP catalog mapping (`.xmi`); declared as a volume | +| `/app/data` | Database file for file based databases (DuckDB, H2); unused otherwise | diff --git a/application/pivot/common/daanse.pivot.base.bndrun b/application/pivot/common/daanse.pivot.base.bndrun new file mode 100644 index 0000000..fb047ef --- /dev/null +++ b/application/pivot/common/daanse.pivot.base.bndrun @@ -0,0 +1,167 @@ +#******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# SmartCity Jena - initial +# Stefan Bischof (bipolis.org) - initial +#******************************************************************************* + +# Base run description of the Daanse pivot server. Database specific bndruns +# -include this file and add their DataSource, JDBC driver and SQL dialect. + +-runtrace: true + +-runproperties: \ + org.slf4j.simpleLogger.defaultLogLevel=error,\ + org.osgi.service.http.port=8080,\ + logback.configurationFile=./logback.xml + +-runpath: \ + ch.qos.logback.classic,\ + ch.qos.logback.core,\ + slf4j.api + +-runee: JavaSE-25 +-runfw: org.apache.felix.framework + +-runrequires: \ + bnd.identity;id='org.eclipse.daanse.server.application.pivot.common',\ + bnd.identity;id='org.glassfish.hk2.osgi-resource-locator',\ + bnd.identity;id='org.apache.felix.metatype',\ + bnd.identity;id='org.eclipse.daanse.olap.xmla.connector',\ + bnd.identity;id='org.eclipse.daanse.olap.xmla.connector.relational',\ + bnd.identity;id='org.eclipse.daanse.olap.xmla.connector.relational.oledb',\ + bnd.identity;id='org.eclipse.daanse.olap.xmla.connector.multidimensional',\ + bnd.identity;id='org.eclipse.daanse.xmla.server.whiteboard.servlet',\ + bnd.identity;id='org.eclipse.daanse.xmla.server.auth.roles',\ + bnd.identity;id='org.eclipse.daanse.xmla.server.auth.basic',\ + bnd.identity;id='org.eclipse.daanse.xmla.server.auth.store.ldap',\ + bnd.identity;id='ch.qos.logback.classic',\ + bnd.identity;id='org.eclipse.daanse.sql.jdbc.impl',\ + bnd.identity;id='org.eclipse.daanse.mdx.parser.api',\ + bnd.identity;id='org.eclipse.daanse.mdx.parser.ccc',\ + bnd.identity;id='org.eclipse.daanse.dmv.parser.ccc',\ + bnd.identity;id='org.eclipse.daanse.rolap.mapping.model',\ + bnd.identity;id='org.eclipse.daanse.sql.guard.api',\ + bnd.identity;id='org.eclipse.daanse.sql.guard.jsqltranspiler',\ + bnd.identity;id='org.eclipse.daanse.rolap.core',\ + bnd.identity;id='org.eclipse.daanse.jdbc.datasource.pools.hikari',\ + bnd.identity;id='org.eclipse.daanse.jakarta.servlet.filter.cors',\ + bnd.identity;id='org.eclipse.daanse.lcid.basic',\ + bnd.identity;id='org.eclipse.daanse.lcid.cfg',\ + bnd.identity;id='org.eclipse.fennec.emf.osgi.component',\ + bnd.identity;id='org.apache.aries.spifly.dynamic.bundle' + +# This will help us keep -runbundles sorted +-runstartlevel: \ + order=sortbynameversion,\ + begin=-1 +-runbundles: \ + ai.starlake.transpiler;version='[1.8.0,1.8.1)',\ + ch.qos.logback.classic;version='[1.5.6,1.5.7)',\ + com.fasterxml.woodstox.woodstox-core;version='[7.1.1,7.1.2)',\ + com.github.ben-manes.caffeine;version='[3.1.2,3.1.3)',\ + com.zaxxer.HikariCP;version='[7.1.0,7.1.1)',\ + net.sf.jsqlparser;version='[5.3.167,5.3.168)',\ + org.apache.aries.spifly.dynamic.bundle;version='[1.3.7,1.3.8)',\ + org.apache.commons.commons-collections4;version='[4.5.0,4.5.1)',\ + org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ + org.apache.felix.http.jetty12;version='[2.0.4,2.0.5)',\ + org.apache.felix.http.servlet-api;version='[6.1.0,6.1.1)',\ + org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ + org.apache.felix.scr;version='[2.2.10,2.2.11)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.businessinformation;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.datatypes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.keysindexes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.behavioral;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.instance;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.resource.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jakarta.servlet.filter.cors;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.hikari;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.cfg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.spi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.util.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational.oledb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.mapping.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.an;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.bi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.cg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.edm;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.empty;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine200;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300_300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.exception;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.ext;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.io;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.mddataset;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.msxmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.multipleresults;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.mining;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.server;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.soap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.xmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.adapter.emf;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.roles;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.store.ldap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.whiteboard.servlet;version='[0.0.1,0.0.2)',\ + org.eclipse.emf.common;version='[2.44.0,2.44.1)',\ + org.eclipse.emf.ecore;version='[2.41.0,2.41.1)',\ + org.eclipse.emf.ecore.xmi;version='[2.39.0,2.39.1)',\ + org.eclipse.fennec.emf.osgi.component;version='[0.1.1,0.1.2)',\ + org.glassfish.hk2.osgi-resource-locator;version='[2.4.0,2.4.1)',\ + org.objectweb.asm;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.commons;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree.analysis;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.util;version='[9.9.0,9.9.1)',\ + org.osgi.service.cm;version='[1.6.1,1.6.2)',\ + org.osgi.service.component;version='[1.5.1,1.5.2)',\ + org.osgi.service.log;version='[1.4.0,1.4.1)',\ + org.osgi.util.converter;version='[1.0.9,1.0.10)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.3.0,1.3.1)',\ + org.osgi.util.pushstream;version='[1.1.0,1.1.1)',\ + stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file diff --git a/application/pivot/common/example/compose/docker-compose.yml b/application/pivot/common/example/compose/docker-compose.yml new file mode 100644 index 0000000..962fcae --- /dev/null +++ b/application/pivot/common/example/compose/docker-compose.yml @@ -0,0 +1,67 @@ +# Complete, self contained Daanse pivot demo: a PostgreSQL side container is +# seeded with a tiny fact table, the pivot serves the matching OLAP catalog - +# both embedded below as inline configs, so this one file is all it takes: +# +# docker compose up +# curl -X POST http://localhost:8080/xmla \ +# -H 'Content-Type: text/xml' \ +# -H 'SOAPAction: "urn:schemas-microsoft-com:xml-analysis:Execute"' \ +# -d 'SELECT [Measures].[Measure-Sum] ON COLUMNS FROM [MinimalCube]Daanse Tutorial - Cube MinimalMultidimensional' +# +# The answer contains the cell value 6 (1+2+3). Requires docker compose >= 2.23 +# for inline configs. Swap in your own catalog and database to go further; the +# sibling ../ldap example adds LDAP authentication on top. + +configs: + init.sql: + content: | + CREATE TABLE "Fact" ("KEY" VARCHAR(100), "VALUE" INTEGER); + INSERT INTO "Fact" VALUES ('A', 1), ('B', 2), ('C', 3); + catalog.xmi: + content: | + + + + + + + + + + + + + + + + + + + + + +services: + db: + image: postgres:17-alpine + environment: + POSTGRES_DB: demo + POSTGRES_USER: daanse + POSTGRES_PASSWORD: secret + configs: + - source: init.sql + target: /docker-entrypoint-initdb.d/init.sql + + pivot: + image: eclipsedaanse/daanse-pivot-postgres:snapshot + depends_on: + - db + ports: + - "8080:8080" + configs: + - source: catalog.xmi + target: /app/catalog/catalog.xmi + environment: + DAANSE_JDBC_HOST: db + DAANSE_JDBC_DBNAME: demo + DAANSE_JDBC_USER: daanse + DAANSE_JDBC_PASSWORD: secret diff --git a/application/pivot/common/example/kube/pivot-pod.yaml b/application/pivot/common/example/kube/pivot-pod.yaml new file mode 100644 index 0000000..655cd93 --- /dev/null +++ b/application/pivot/common/example/kube/pivot-pod.yaml @@ -0,0 +1,85 @@ +# Complete, self contained Daanse pivot demo as a Kubernetes pod - runs +# unchanged with Podman or a real cluster: +# +# podman kube play pivot-pod.yaml # or: kubectl apply -f pivot-pod.yaml +# curl -X POST http://localhost:8080/xmla \ +# -H 'Content-Type: text/xml' \ +# -H 'SOAPAction: "urn:schemas-microsoft-com:xml-analysis:Execute"' \ +# -d 'SELECT [Measures].[Measure-Sum] ON COLUMNS FROM [MinimalCube]Daanse Tutorial - Cube MinimalMultidimensional' +# +# The answer contains the cell value 6 (1+2+3). A PostgreSQL container seeded +# with a tiny fact table and the pivot share the pod; the catalog and the seed +# SQL come from the ConfigMaps below. On a real cluster replace the hostPort +# with a Service and the ConfigMap catalog with your own mapping. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: daanse-pivot-demo +data: + init.sql: | + CREATE TABLE "Fact" ("KEY" VARCHAR(100), "VALUE" INTEGER); + INSERT INTO "Fact" VALUES ('A', 1), ('B', 2), ('C', 3); + catalog.xmi: | + + + + + + + + + + + + + + + + + + + + +--- +apiVersion: v1 +kind: Pod +metadata: + name: daanse-pivot-demo +spec: + containers: + - name: db + image: docker.io/library/postgres:17-alpine + env: + - name: POSTGRES_DB + value: demo + - name: POSTGRES_USER + value: daanse + - name: POSTGRES_PASSWORD + value: secret + volumeMounts: + - name: demo + mountPath: /docker-entrypoint-initdb.d/init.sql + subPath: init.sql + - name: pivot + image: docker.io/eclipsedaanse/daanse-pivot-postgres:snapshot + ports: + - containerPort: 8080 + hostPort: 8080 + env: + - name: DAANSE_JDBC_HOST + value: localhost # containers in a pod share the network namespace + - name: DAANSE_JDBC_DBNAME + value: demo + - name: DAANSE_JDBC_USER + value: daanse + - name: DAANSE_JDBC_PASSWORD + value: secret + volumeMounts: + - name: demo + mountPath: /app/catalog/catalog.xmi + subPath: catalog.xmi + volumes: + - name: demo + configMap: + name: daanse-pivot-demo diff --git a/application/pivot/common/example/ldap/bootstrap/group-configs/groups.json b/application/pivot/common/example/ldap/bootstrap/group-configs/groups.json new file mode 100644 index 0000000..848b23b --- /dev/null +++ b/application/pivot/common/example/ldap/bootstrap/group-configs/groups.json @@ -0,0 +1,3 @@ +{ + "name": "Administrator" +} diff --git a/application/pivot/common/example/ldap/bootstrap/user-configs/admin.json b/application/pivot/common/example/ldap/bootstrap/user-configs/admin.json new file mode 100644 index 0000000..a5bb870 --- /dev/null +++ b/application/pivot/common/example/ldap/bootstrap/user-configs/admin.json @@ -0,0 +1,6 @@ +{ + "id": "admin", + "email": "admin@example.org", + "displayName": "Admin", + "groups": ["lldap_admin", "Administrator"] +} diff --git a/application/pivot/common/example/ldap/bootstrap/user-configs/analyst.json b/application/pivot/common/example/ldap/bootstrap/user-configs/analyst.json new file mode 100644 index 0000000..a8afff3 --- /dev/null +++ b/application/pivot/common/example/ldap/bootstrap/user-configs/analyst.json @@ -0,0 +1,6 @@ +{ + "id": "analyst", + "email": "analyst@example.org", + "displayName": "Analyst", + "password": "analyst123" +} diff --git a/application/pivot/common/example/ldap/docker-compose.yml b/application/pivot/common/example/ldap/docker-compose.yml new file mode 100644 index 0000000..202d3e3 --- /dev/null +++ b/application/pivot/common/example/ldap/docker-compose.yml @@ -0,0 +1,87 @@ +# Daanse pivot with LDAP backed basic authentication, using an LLDAP side +# container (https://github.com/lldap/lldap - the image comes from the LLDAP +# project itself and is a lightweight LDAP server). For everyone who wants to +# try the authentication without running their own LDAP. +# +# This one file is self contained: the users and groups are embedded below as +# inline configs and loaded by LLDAP's official bootstrap script, so +# +# docker compose up +# curl -u admin:admin123 -X POST http://localhost:8080/xmla ... +# +# is all it takes (requires docker compose >= 2.23; older versions mount the +# sibling bootstrap/ directory instead - same content). LLDAP also serves a +# web UI on http://localhost:17170 (admin / admin123). +# +# The pivot image is the PostgreSQL one here; any daanse-pivot- image works +# the same way, only the DAANSE_JDBC_* variables differ. + +configs: + # Group names become the caller's roles and are intersected with the roles + # the catalog declares - name groups after your catalog's roles. + groups.json: + content: | + { "name": "Administrator" } + # admin is LLDAP's built-in administrator (password from LLDAP_LDAP_USER_PASS) + # and doubles as the service bind account; here it joins the Administrator role. + admin.json: + content: | + { "id": "admin", "email": "admin@example.org", "groups": ["lldap_admin", "Administrator"] } + analyst.json: + content: | + { "id": "analyst", "email": "analyst@example.org", "password": "analyst123" } + +services: + lldap: + image: lldap/lldap:stable + environment: + LLDAP_LDAP_BASE_DN: dc=example,dc=org + LLDAP_LDAP_USER_PASS: admin123 + LLDAP_KEY_SEED: change-me-for-anything-real + LLDAP_JWT_SECRET: change-me-too-at-least-32-characters-long + ports: + - "17170:17170" # web UI, optional + + # One-shot job running LLDAP's official bootstrap script against the server. + lldap-bootstrap: + image: lldap/lldap:stable + depends_on: + - lldap + entrypoint: ["/bin/sh", "-c", "sleep 2 && /app/bootstrap.sh"] + restart: on-failure + environment: + LLDAP_URL: http://lldap:17170 + LLDAP_ADMIN_USERNAME: admin + LLDAP_ADMIN_PASSWORD: admin123 + DO_CLEANUP: "false" + configs: + - source: groups.json + target: /bootstrap/group-configs/groups.json + - source: admin.json + target: /bootstrap/user-configs/admin.json + - source: analyst.json + target: /bootstrap/user-configs/analyst.json + + pivot: + image: eclipsedaanse/daanse-pivot-postgres:snapshot + depends_on: + - lldap + ports: + - "8080:8080" + volumes: + - ./catalog:/app/catalog:ro + environment: + # database (PostgreSQL image) + DAANSE_JDBC_HOST: db.example.org + DAANSE_JDBC_DBNAME: warehouse + DAANSE_JDBC_USER: daanse + DAANSE_JDBC_PASSWORD: secret + # LDAP backed basic authentication + DAANSE_LDAP_URL: ldap://lldap:3890 + DAANSE_LDAP_TRANSPORT_SECURITY: NONE + DAANSE_LDAP_ALLOW_UNENCRYPTED: "true" + DAANSE_LDAP_USER_DN_PATTERN: uid={0},ou=people,dc=example,dc=org + DAANSE_LDAP_SERVICE_BIND_DN: uid=admin,ou=people,dc=example,dc=org + DAANSE_LDAP_SERVICE_BIND_PASSWORD: admin123 + DAANSE_LDAP_GROUP_SEARCH_BASE: ou=groups,dc=example,dc=org + # DAANSE_AUTH_ANONYMOUS: "false" # uncomment to require a login diff --git a/application/pivot/common/pom.xml b/application/pivot/common/pom.xml new file mode 100644 index 0000000..53d1225 --- /dev/null +++ b/application/pivot/common/pom.xml @@ -0,0 +1,98 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot + ${revision} + + org.eclipse.daanse.server.application.pivot.common + + + + org.eclipse.daanse + org.eclipse.daanse.olap.common + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.pools.api + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.pools.hikari + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jakarta.servlet.filter.cors + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.lcid.basic + 0.0.1-SNAPSHOT + runtime + + + + org.eclipse.daanse + org.eclipse.daanse.lcid.cfg + 0.0.1-SNAPSHOT + runtime + + + + + org.testcontainers + testcontainers + ${testcontainers.version} + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + diff --git a/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/BasicContextConfigs.java b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/BasicContextConfigs.java new file mode 100644 index 0000000..6bd2a8b --- /dev/null +++ b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/BasicContextConfigs.java @@ -0,0 +1,81 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.common; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; + +import org.eclipse.daanse.rolap.core.api.Constants; +import org.eclipse.daanse.sql.dialect.api.DaanseDialectConstants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Creates the BasicContext that joins the connection pool over the environment + * configured DataSource, the catalog mapping provider and the database specific + * dialect. + */ +public final class BasicContextConfigs { + + private BasicContextConfigs() { + } + + /** + * The pool and the context that draws from it - one lifecycle, since the + * context cannot activate without the pool. Returned in deletion order: + * context first, then the pool it depends on. + */ + public static List createEnvPoolAndContext(ConfigurationAdmin ca, String dialectName) + throws IOException { + return createEnvPoolAndContext(ca, dialectName, null); + } + + /** + * As {@link #createEnvPoolAndContext(ConfigurationAdmin, String)}, with a + * default for the pool's read-only mode - see + * {@link ConnectionPoolConfigs#createEnvPool(ConfigurationAdmin, Boolean)}. + */ + public static List createEnvPoolAndContext(ConfigurationAdmin ca, String dialectName, + Boolean poolReadOnlyDefault) throws IOException { + Configuration pool = ConnectionPoolConfigs.createEnvPool(ca, poolReadOnlyDefault); + return List.of(createEnvBasicContext(ca, dialectName), pool); + } + + public static Configuration createEnvBasicContext(ConfigurationAdmin ca, String dialectName) throws IOException { + Configuration configuration = ca.getFactoryConfiguration(Constants.BASIC_CONTEXT_PID, + ServerConstants.CONFIG_IDENT, "?"); + + Dictionary props = new Hashtable<>(); + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_CONTEXT); + props.put(Constants.BASIC_CONTEXT_REF_NAME_CONNECTION_POOL + ServerConstants.TARGET_EXT, + filter(ServerConstants.PROP_IDENT, ServerConstants.IDENT_POOL)); + props.put(Constants.BASIC_CONTEXT_REF_NAME_CATALOG_MAPPING_SUPPLIER + ServerConstants.TARGET_EXT, + filter(ServerConstants.PROP_IDENT, ServerConstants.IDENT_MAPPING)); + props.put(Constants.BASIC_CONTEXT_REF_NAME_DIALECT_FACTORY + ServerConstants.TARGET_EXT, + filter(DaanseDialectConstants.DIALECT_NAME_PROPERTY, dialectName)); + props.put("name", Env.get(ServerConstants.ENV_CONTEXT_NAME, ServerConstants.DEFAULT_CONTEXT_NAME)); + props.put("description", + Env.get(ServerConstants.ENV_CONTEXT_DESCRIPTION, ServerConstants.DEFAULT_CONTEXT_DESCRIPTION)); + props.put("useAggregates", Env.get(ServerConstants.ENV_USE_AGGREGATES, false)); + + configuration.update(props); + return configuration; + } + + private static String filter(String key, String value) { + return "(" + key + "=" + value + ")"; + } +} diff --git a/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/CommonServerConfigurator.java b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/CommonServerConfigurator.java new file mode 100644 index 0000000..4f499f3 --- /dev/null +++ b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/CommonServerConfigurator.java @@ -0,0 +1,239 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.common; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Optional; + +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.annotations.RequireConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.RequireServiceComponentRuntime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wires the database independent parts of the pivot server from environment + * variables: catalog mapping provider, context group, XMLA connector, the XMLA + * servlet on the HTTP whiteboard and the optional CORS filter. The database specific configurator of + * each image contributes the DataSource and the BasicContext. + */ +@Component(immediate = true) +@RequireConfigurationAdmin +@RequireServiceComponentRuntime +public class CommonServerConfigurator { + + private static final Logger logger = LoggerFactory.getLogger(CommonServerConfigurator.class); + + private static final String PID_XMLA_SERVLET = + "org.eclipse.daanse.xmla.server.whiteboard.servlet.XmlaServlet"; + private static final String PID_XMLA_SERVICE = "daanse.olap.xmla.connector.OlapXmlaConnector"; + private static final String PID_BASIC_AUTHENTICATOR = + "org.eclipse.daanse.xmla.server.auth.basic.BasicAuthenticator"; + private static final String PID_LDAP_CREDENTIALS = + "org.eclipse.daanse.xmla.server.auth.store.ldap.LdapCredentials"; + private static final String PID_LDAP_ROLE_PROVIDER = + "org.eclipse.daanse.xmla.server.auth.store.ldap.LdapRoleProvider"; + + @Reference + ConfigurationAdmin ca; + + private Configuration confMappingProvider; + private Configuration confContextGroup; + private Configuration confXmlaService; + private Configuration confXmlaServlet; + private Configuration confCorsFilter; + private Configuration confBasicAuthenticator; + private Configuration confLdapCredentials; + private Configuration confLdapRoleProvider; + + @Activate + public void activate() throws IOException { + logger.info("Activating pivot server setup"); + + initMappingProvider(); + initContextGroup(); + initXmlaService(); + initXmlaServlet(); + initCorsFilter(); + initAuth(); + + logger.info("Pivot server setup completed"); + } + + private void initMappingProvider() throws IOException { + confMappingProvider = ca.getFactoryConfiguration( + org.eclipse.daanse.rolap.mapping.model.provider.Constants.PID_EMF_MAPPING_PROVIDER, + ServerConstants.CONFIG_IDENT, "?"); + + Dictionary props = new Hashtable<>(); + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_MAPPING); + props.put(org.eclipse.daanse.rolap.mapping.model.provider.Constants.RESOURCE_URL, + Env.get(ServerConstants.ENV_CATALOG_RESOURCE, ServerConstants.DEFAULT_CATALOG_RESOURCE)); + Env.get(ServerConstants.ENV_CATALOG_ADDITIONAL_GLOBS) + .ifPresent(globs -> props.put( + org.eclipse.daanse.rolap.mapping.model.provider.Constants.ADDITIONAL_RESOURCE_GLOBS, + Env.splitList(globs))); + + confMappingProvider.update(props); + } + + private void initContextGroup() throws IOException { + confContextGroup = ca.getFactoryConfiguration( + org.eclipse.daanse.olap.core.api.Constants.BASIC_CONTEXT_GROUP_PID, ServerConstants.CONFIG_IDENT, "?"); + + Dictionary props = new Hashtable<>(); + props.put(org.eclipse.daanse.olap.core.api.Constants.BASIC_CONTEXT_GROUP_REF_NAME_CONTEXTS + + ServerConstants.TARGET_EXT, + "(" + ServerConstants.PROP_IDENT + "=" + ServerConstants.IDENT_CONTEXT + ")"); + + confContextGroup.update(props); + } + + private void initXmlaService() throws IOException { + confXmlaService = ca.getFactoryConfiguration(PID_XMLA_SERVICE, ServerConstants.CONFIG_IDENT, "?"); + + Dictionary props = new Hashtable<>(); + props.put("contextGroup" + ServerConstants.TARGET_EXT, "(service.pid=*)"); + + confXmlaService.update(props); + } + + private void initXmlaServlet() throws IOException { + confXmlaServlet = ca.getFactoryConfiguration(PID_XMLA_SERVLET, ServerConstants.CONFIG_IDENT, "?"); + + Dictionary props = new Hashtable<>(); + props.put("osgi.http.whiteboard.servlet.pattern", + Env.get(ServerConstants.ENV_XMLA_PATH, ServerConstants.DEFAULT_XMLA_PATH)); + // Anonymous requests are served (with no roles) unless switched off. + props.put("requirePrincipal", !Env.get(ServerConstants.ENV_AUTH_ANONYMOUS, true)); + + confXmlaServlet.update(props); + } + + private void initCorsFilter() throws IOException { + if (!Env.get(ServerConstants.ENV_CORS_ENABLED, true)) { + logger.info("CORS filter disabled via {}", ServerConstants.ENV_CORS_ENABLED); + return; + } + + confCorsFilter = ca.getFactoryConfiguration( + org.eclipse.daanse.jakarta.servlet.filter.cors.api.Constants.PID_FILTER_CORS, + ServerConstants.CONFIG_IDENT, "?"); + + Dictionary props = new Hashtable<>(); + props.put("osgi.http.whiteboard.filter.pattern", "/*"); + props.put(org.eclipse.daanse.jakarta.servlet.filter.cors.api.Constants.PROPERTY_ALLOW_CREDENTIALS_PARAM, + Env.get(ServerConstants.ENV_CORS_ALLOW_CREDENTIALS, true)); + props.put(org.eclipse.daanse.jakarta.servlet.filter.cors.api.Constants.PROPERTY_ALLOWED_ORIGINS_PARAM, + Env.splitList(Env.get(ServerConstants.ENV_CORS_ALLOWED_ORIGINS, "*"))); + props.put(org.eclipse.daanse.jakarta.servlet.filter.cors.api.Constants.PROPERTY_ALLOWED_HEADERS_PARAM, + Env.splitList(Env.get(ServerConstants.ENV_CORS_ALLOWED_HEADERS, "*"))); + + confCorsFilter.update(props); + } + + private void initAuth() throws IOException { + // Without an LDAP url the credential store stays unconfigured, the basic + // authenticator never registers and the endpoint serves anonymously. + Optional url = Env.get(ServerConstants.ENV_LDAP_URL); + if (url.isEmpty()) { + return; + } + + confLdapCredentials = ca.getConfiguration(PID_LDAP_CREDENTIALS, "?"); + confLdapCredentials.update(ldapConnectionProps(url.get())); + + // Roles from LDAP groups; without a group search base callers + // authenticate but carry no roles. + Optional groupSearchBase = Env.get(ServerConstants.ENV_LDAP_GROUP_SEARCH_BASE); + if (groupSearchBase.isPresent()) { + confLdapRoleProvider = ca.getConfiguration(PID_LDAP_ROLE_PROVIDER, "?"); + + Dictionary props = ldapConnectionProps(url.get()); + props.put("groupSearchBase", groupSearchBase.get()); + Env.get(ServerConstants.ENV_LDAP_GROUP_SEARCH_FILTER) + .ifPresent(v -> props.put("groupSearchFilter", v)); + Env.get(ServerConstants.ENV_LDAP_GROUP_NAME_ATTRIBUTE) + .ifPresent(v -> props.put("groupNameAttribute", v)); + Env.get(ServerConstants.ENV_LDAP_MEMBER_OF_ATTRIBUTE) + .ifPresent(v -> props.put("memberOfAttribute", v)); + + confLdapRoleProvider.update(props); + } + + confBasicAuthenticator = ca.getConfiguration(PID_BASIC_AUTHENTICATOR, "?"); + Dictionary basicProps = new Hashtable<>(); + Env.get(ServerConstants.ENV_AUTH_REALM).ifPresent(v -> basicProps.put("realm", v)); + confBasicAuthenticator.update(basicProps); + + logger.info("LDAP backed basic authentication enabled against {}", url.get()); + } + + private Dictionary ldapConnectionProps(String url) { + Dictionary props = new Hashtable<>(); + props.put("url", url); + Env.get(ServerConstants.ENV_LDAP_USER_DN_PATTERN).ifPresent(v -> props.put("userDnPattern", v)); + Env.get(ServerConstants.ENV_LDAP_USER_SEARCH_BASE).ifPresent(v -> props.put("userSearchBase", v)); + Env.get(ServerConstants.ENV_LDAP_USER_SEARCH_FILTER).ifPresent(v -> props.put("userSearchFilter", v)); + Env.get(ServerConstants.ENV_LDAP_SERVICE_BIND_DN).ifPresent(v -> props.put("serviceBindDn", v)); + Env.get(ServerConstants.ENV_LDAP_SERVICE_BIND_PASSWORD) + .ifPresent(v -> props.put("serviceBindPassword", v)); + Env.get(ServerConstants.ENV_LDAP_TRANSPORT_SECURITY).ifPresent(v -> props.put("transportSecurity", v)); + Env.get(ServerConstants.ENV_LDAP_ALLOW_UNENCRYPTED) + .ifPresent(v -> props.put("allowUnencrypted", Boolean.parseBoolean(v))); + Env.get(ServerConstants.ENV_LDAP_CONNECT_TIMEOUT_MILLIS) + .ifPresent(v -> props.put("connectTimeoutMillis", Integer.parseInt(v))); + Env.get(ServerConstants.ENV_LDAP_READ_TIMEOUT_MILLIS) + .ifPresent(v -> props.put("readTimeoutMillis", Integer.parseInt(v))); + Env.get(ServerConstants.ENV_LDAP_REFERRAL).ifPresent(v -> props.put("referral", v)); + return props; + } + + @Deactivate + public void deactivate() throws IOException { + logger.info("Deactivating pivot server setup"); + + if (confMappingProvider != null) { + confMappingProvider.delete(); + } + if (confContextGroup != null) { + confContextGroup.delete(); + } + if (confXmlaService != null) { + confXmlaService.delete(); + } + if (confXmlaServlet != null) { + confXmlaServlet.delete(); + } + if (confCorsFilter != null) { + confCorsFilter.delete(); + } + if (confBasicAuthenticator != null) { + confBasicAuthenticator.delete(); + } + if (confLdapCredentials != null) { + confLdapCredentials.delete(); + } + if (confLdapRoleProvider != null) { + confLdapRoleProvider.delete(); + } + } +} diff --git a/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/ConnectionPoolConfigs.java b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/ConnectionPoolConfigs.java new file mode 100644 index 0000000..de4f2ec --- /dev/null +++ b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/ConnectionPoolConfigs.java @@ -0,0 +1,66 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.common; + +import java.io.IOException; +import java.util.Dictionary; + +import org.eclipse.daanse.jdbc.datasource.pools.api.Constants; +import org.eclipse.daanse.jdbc.datasource.pools.api.ocd.BaseConfig; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Creates the connection pool the context draws from. A single MDX query fans + * out to as many statements as the segment cache has threads, so the engine + * talks to a pool and never to the DataSource directly. + *

+ * The pool attributes are configurable via {@code DAANSE_POOL_*}; unset + * variables keep the defaults of the pool bundle. Which DataSource is pooled is + * not configurable - it is the one this image builds from {@code DAANSE_JDBC_*}. + */ +public final class ConnectionPoolConfigs { + + private ConnectionPoolConfigs() { + } + + public static Configuration createEnvPool(ConfigurationAdmin ca) throws IOException { + return createEnvPool(ca, null); + } + + /** + * As {@link #createEnvPool(ConfigurationAdmin)}, with a default for the + * pool's read-only mode. The pool marks its connections read-only or + * read-write; a DataSource whose connections are fixed to one mode (e.g. a + * read-only DuckDB file) rejects the opposite marking, so its configurator + * passes the matching mode here. {@code DAANSE_POOL_READ_ONLY} still wins. + */ + public static Configuration createEnvPool(ConfigurationAdmin ca, Boolean readOnlyDefault) throws IOException { + Configuration configuration = ca.getFactoryConfiguration( + org.eclipse.daanse.jdbc.datasource.pools.hikari.api.Constants.PID_CONNECTION_POOL, + ServerConstants.CONFIG_IDENT, "?"); + + Dictionary props = EnvConfigMapper.propsFromEnv(BaseConfig.class, + ServerConstants.ENV_POOL_PREFIX); + if (readOnlyDefault != null && props.get(Constants.POOL_PROPERTY_READ_ONLY) == null) { + props.put(Constants.POOL_PROPERTY_READ_ONLY, readOnlyDefault); + } + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_POOL); + props.put(Constants.POOL_PROPERTY_DATASOURCE_TARGET, + "(" + ServerConstants.PROP_IDENT + "=" + ServerConstants.IDENT_DATASOURCE + ")"); + + configuration.update(props); + return configuration; + } +} diff --git a/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/Env.java b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/Env.java new file mode 100644 index 0000000..5d58f12 --- /dev/null +++ b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/Env.java @@ -0,0 +1,55 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.common; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * Access to the environment variables that configure the pivot server. Blank + * values are treated as unset. + */ +public final class Env { + + private Env() { + } + + public static Optional get(String name) { + String value = System.getenv(name); + if (value == null || value.isBlank()) { + return Optional.empty(); + } + return Optional.of(value.trim()); + } + + public static String get(String name, String defaultValue) { + return get(name).orElse(defaultValue); + } + + public static boolean get(String name, boolean defaultValue) { + return get(name).map(Boolean::parseBoolean).orElse(defaultValue); + } + + /** Splits a comma separated environment variable value into its entries. */ + public static String[] splitList(String value) { + return Arrays.stream(value.split(",")).map(String::trim).filter(s -> !s.isEmpty()) + .toArray(String[]::new); + } + + /** The given environment variables that are not set. */ + public static List missing(String... names) { + return Arrays.stream(names).filter(name -> get(name).isEmpty()).toList(); + } +} diff --git a/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/EnvConfigMapper.java b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/EnvConfigMapper.java new file mode 100644 index 0000000..a46fb41 --- /dev/null +++ b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/EnvConfigMapper.java @@ -0,0 +1,165 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.common; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; + +/** + * Maps environment variables onto OSGi configuration properties, driven by the + * OCD config interface of a Daanse DataSource bundle. Every attribute of the + * interface is configurable via {@code DAANSE_JDBC_}; attributes + * whose variable is not set keep the default declared by the bundle. + */ +public final class EnvConfigMapper { + + private EnvConfigMapper() { + } + + /** + * Configuration properties for all attributes of the given config interface + * whose {@code DAANSE_JDBC_*} variable is set. + */ + public static Dictionary propsFromEnv(Class configInterface) { + return propsFromEnv(configInterface, ServerConstants.ENV_JDBC_PREFIX); + } + + /** As {@link #propsFromEnv(Class)}, under a different variable prefix. */ + public static Dictionary propsFromEnv(Class configInterface, String envPrefix) { + Dictionary props = new Hashtable<>(); + for (Method method : attributeMethods(configInterface)) { + String envName = envName(method.getName(), envPrefix); + Env.get(envName).ifPresent( + value -> props.put(propertyName(method.getName()), convert(value, method.getReturnType(), envName))); + } + return props; + } + + /** Environment variable names supported by the given config interface. */ + public static List envNames(Class configInterface) { + return envNames(configInterface, ServerConstants.ENV_JDBC_PREFIX); + } + + /** As {@link #envNames(Class)}, under a different variable prefix. */ + public static List envNames(Class configInterface, String envPrefix) { + return attributeMethods(configInterface).stream().map(method -> envName(method.getName(), envPrefix)).sorted() + .toList(); + } + + private static List attributeMethods(Class configInterface) { + List methods = new ArrayList<>(); + for (Method method : configInterface.getMethods()) { + if (method.getParameterCount() == 0 && !method.isSynthetic() && !Modifier.isStatic(method.getModifiers()) + && isSupported(method.getReturnType())) { + methods.add(method); + } + } + return methods; + } + + private static boolean isSupported(Class type) { + return type == String.class || type == String[].class || type == boolean.class || type == Boolean.class + || type == int.class || type == Integer.class || type == long.class || type == Long.class + || type == short.class || type == Short.class || type == int[].class; + } + + /** + * Configuration property name of a config interface method, following the + * OSGi component property mapping: {@code $} is removed, {@code $$} maps to + * {@code $}, {@code _} maps to {@code .} and {@code __} maps to {@code _}, + * e.g. {@code _password()} configures {@code .password}. + */ + public static String propertyName(String methodName) { + StringBuilder sb = new StringBuilder(methodName.length()); + for (int i = 0; i < methodName.length(); i++) { + char c = methodName.charAt(i); + if (c == '$') { + if (i + 1 < methodName.length() && methodName.charAt(i + 1) == '$') { + sb.append('$'); + i++; + } + } else if (c == '_') { + if (i + 1 < methodName.length() && methodName.charAt(i + 1) == '_') { + sb.append('_'); + i++; + } else { + sb.append('.'); + } + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Environment variable name of a config interface method, e.g. + * {@code currentSchema()} is set via {@code DAANSE_JDBC_CURRENT_SCHEMA} and + * {@code _password()} via {@code DAANSE_JDBC_PASSWORD}. + */ + public static String envName(String methodName) { + return envName(methodName, ServerConstants.ENV_JDBC_PREFIX); + } + + /** As {@link #envName(String)}, under a different variable prefix. */ + public static String envName(String methodName, String envPrefix) { + String name = methodName.replace("$", ""); + while (name.startsWith("_")) { + name = name.substring(1); + } + name = name.replaceAll("([a-z0-9])([A-Z])", "$1_$2"); + name = name.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2"); + return envPrefix + name.replace("__", "_").toUpperCase(Locale.ROOT); + } + + private static Object convert(String value, Class type, String envName) { + try { + if (type == String.class) { + return value; + } + if (type == boolean.class || type == Boolean.class) { + return Boolean.parseBoolean(value); + } + if (type == int.class || type == Integer.class) { + return Integer.parseInt(value); + } + if (type == long.class || type == Long.class) { + return Long.parseLong(value); + } + if (type == short.class || type == Short.class) { + return Short.parseShort(value); + } + if (type == String[].class) { + return Env.splitList(value); + } + if (type == int[].class) { + String[] parts = Env.splitList(value); + int[] ints = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + ints[i] = Integer.parseInt(parts[i]); + } + return ints; + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Environment variable " + envName + " has invalid value '" + value + "'", e); + } + throw new IllegalArgumentException("Unsupported attribute type " + type + " for " + envName); + } +} diff --git a/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/ServerConstants.java b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/ServerConstants.java new file mode 100644 index 0000000..012af47 --- /dev/null +++ b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/ServerConstants.java @@ -0,0 +1,97 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.common; + +/** + * Constants shared between the common pivot setup and the database specific + * configurator components. + */ +public final class ServerConstants { + + private ServerConstants() { + } + + /** Ident used as 2nd argument of {@code getFactoryConfiguration(pid, ident, "?")}. */ + public static final String CONFIG_IDENT = "pivot"; + + /** Service property used to wire the environment configured services together. */ + public static final String PROP_IDENT = "daanse.ident"; + + public static final String IDENT_DATASOURCE = "env-ds"; + public static final String IDENT_POOL = "env-pool"; + public static final String IDENT_MAPPING = "env-cms"; + public static final String IDENT_CONTEXT = "env-ctx"; + + public static final String TARGET_EXT = ".target"; + + /** + * Prefix of the environment variables that configure the DataSource. The + * variable name is derived from the config attribute, e.g. + * {@code currentSchema} is set via {@code DAANSE_JDBC_CURRENT_SCHEMA}. + */ + public static final String ENV_JDBC_PREFIX = "DAANSE_JDBC_"; + + /** + * Prefix of the environment variables that configure the connection pool + * between the DataSource and the context, e.g. {@code maximumPoolSize} is set + * via {@code DAANSE_POOL_MAXIMUM_POOL_SIZE}. + */ + public static final String ENV_POOL_PREFIX = "DAANSE_POOL_"; + + /** + * Whether requests without credentials are served (with no roles). Default + * true; false makes the XMLA servlet challenge every request. + */ + public static final String ENV_AUTH_ANONYMOUS = "DAANSE_AUTH_ANONYMOUS"; + /** Realm of the HTTP Basic challenge. */ + public static final String ENV_AUTH_REALM = "DAANSE_AUTH_REALM"; + + /** + * Environment variables of the LDAP backed HTTP Basic authentication. + * Setting {@link #ENV_LDAP_URL} switches it on: credentials are verified by + * an LDAP bind, roles come from LDAP groups when + * {@link #ENV_LDAP_GROUP_SEARCH_BASE} is set. Unset means anonymous only. + */ + public static final String ENV_LDAP_URL = "DAANSE_LDAP_URL"; + public static final String ENV_LDAP_USER_DN_PATTERN = "DAANSE_LDAP_USER_DN_PATTERN"; + public static final String ENV_LDAP_USER_SEARCH_BASE = "DAANSE_LDAP_USER_SEARCH_BASE"; + public static final String ENV_LDAP_USER_SEARCH_FILTER = "DAANSE_LDAP_USER_SEARCH_FILTER"; + public static final String ENV_LDAP_SERVICE_BIND_DN = "DAANSE_LDAP_SERVICE_BIND_DN"; + public static final String ENV_LDAP_SERVICE_BIND_PASSWORD = "DAANSE_LDAP_SERVICE_BIND_PASSWORD"; + public static final String ENV_LDAP_TRANSPORT_SECURITY = "DAANSE_LDAP_TRANSPORT_SECURITY"; + public static final String ENV_LDAP_ALLOW_UNENCRYPTED = "DAANSE_LDAP_ALLOW_UNENCRYPTED"; + public static final String ENV_LDAP_CONNECT_TIMEOUT_MILLIS = "DAANSE_LDAP_CONNECT_TIMEOUT_MILLIS"; + public static final String ENV_LDAP_READ_TIMEOUT_MILLIS = "DAANSE_LDAP_READ_TIMEOUT_MILLIS"; + public static final String ENV_LDAP_REFERRAL = "DAANSE_LDAP_REFERRAL"; + public static final String ENV_LDAP_GROUP_SEARCH_BASE = "DAANSE_LDAP_GROUP_SEARCH_BASE"; + public static final String ENV_LDAP_GROUP_SEARCH_FILTER = "DAANSE_LDAP_GROUP_SEARCH_FILTER"; + public static final String ENV_LDAP_GROUP_NAME_ATTRIBUTE = "DAANSE_LDAP_GROUP_NAME_ATTRIBUTE"; + public static final String ENV_LDAP_MEMBER_OF_ATTRIBUTE = "DAANSE_LDAP_MEMBER_OF_ATTRIBUTE"; + + public static final String ENV_CATALOG_RESOURCE = "DAANSE_CATALOG_RESOURCE"; + public static final String ENV_CATALOG_ADDITIONAL_GLOBS = "DAANSE_CATALOG_ADDITIONAL_GLOBS"; + public static final String ENV_CONTEXT_NAME = "DAANSE_CONTEXT_NAME"; + public static final String ENV_CONTEXT_DESCRIPTION = "DAANSE_CONTEXT_DESCRIPTION"; + public static final String ENV_USE_AGGREGATES = "DAANSE_USE_AGGREGATES"; + public static final String ENV_XMLA_PATH = "DAANSE_XMLA_PATH"; + public static final String ENV_CORS_ENABLED = "DAANSE_CORS_ENABLED"; + public static final String ENV_CORS_ALLOWED_ORIGINS = "DAANSE_CORS_ALLOWED_ORIGINS"; + public static final String ENV_CORS_ALLOWED_HEADERS = "DAANSE_CORS_ALLOWED_HEADERS"; + public static final String ENV_CORS_ALLOW_CREDENTIALS = "DAANSE_CORS_ALLOW_CREDENTIALS"; + + public static final String DEFAULT_CATALOG_RESOURCE = "/app/catalog/catalog.xmi"; + public static final String DEFAULT_CONTEXT_NAME = "daanse"; + public static final String DEFAULT_CONTEXT_DESCRIPTION = "Daanse XMLA Server"; + public static final String DEFAULT_XMLA_PATH = "/xmla"; +} diff --git a/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/package-info.java b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/package-info.java new file mode 100644 index 0000000..50549a7 --- /dev/null +++ b/application/pivot/common/src/main/java/org/eclipse/daanse/server/application/pivot/common/package-info.java @@ -0,0 +1,16 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("1.0.0") +package org.eclipse.daanse.server.application.pivot.common; diff --git a/application/pivot/common/src/test/java/org/eclipse/daanse/server/application/pivot/common/test/PivotContainers.java b/application/pivot/common/src/test/java/org/eclipse/daanse/server/application/pivot/common/test/PivotContainers.java new file mode 100644 index 0000000..d22acd4 --- /dev/null +++ b/application/pivot/common/src/test/java/org/eclipse/daanse/server/application/pivot/common/test/PivotContainers.java @@ -0,0 +1,151 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.common.test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +import org.testcontainers.images.builder.ImageFromDockerfile; + +/** + * Support for the per-database pivot integration tests: provides the real + * container image of a database module and queries its XMLA endpoint with MDX. + */ +public final class PivotContainers { + + /** + * System property naming a pre-built pivot image to verify. When set (e.g. + * {@code -Dpivot.image=daanse-pivot-postgres:ci}) the tests run against + * exactly that image instead of building one — this is how CI verifies the + * image it later pushes. + */ + public static final String IMAGE_PROPERTY = "pivot.image"; + + /** Catalog name of the static test mapping (src/test/resources/catalog.xmi). */ + public static final String TEST_CATALOG = "Daanse Tutorial - Cube Minimal"; + /** MDX query summing the single measure of the static test mapping. */ + public static final String TEST_MDX = "SELECT [Measures].[Measure-Sum] ON COLUMNS FROM [MinimalCube]"; + + private PivotContainers() { + } + + /** + * The pivot image of the given database module: the pre-built image named + * by the {@value #IMAGE_PROPERTY} system property if set, otherwise built + * from the module's real Dockerfile (runs with the module directory as + * working directory and provides the files the Dockerfile copies as build + * context). + */ + public static Future pivotImage(String db) { + String preBuilt = System.getProperty(IMAGE_PROPERTY); + if (preBuilt != null && !preBuilt.isBlank()) { + return CompletableFuture.completedFuture(preBuilt.trim()); + } + String prefix = "application/pivot/" + db + "/"; + return new ImageFromDockerfile() + .withFileFromPath("Dockerfile", Path.of("container/Dockerfile")) + .withFileFromPath(prefix + "target/daanse.pivot." + db + ".jar", + Path.of("target/daanse.pivot." + db + ".jar")) + .withFileFromPath(prefix + "start", Path.of("start")) + .withFileFromPath(prefix + "logback.xml", Path.of("logback.xml")); + } + + /** Sends an XMLA Execute request with the given MDX statement. */ + public static String executeMdx(String host, int port, String catalog, String mdx) + throws IOException, InterruptedException { + return executeMdx(host, port, catalog, mdx, null).body(); + } + + /** + * As {@link #executeMdx(String, int, String, String)}, optionally with HTTP + * Basic credentials ({@code user:password}), returning the full response so + * tests can assert the status code (e.g. 401). + */ + public static HttpResponse executeMdx(String host, int port, String catalog, String mdx, + String basicCredentials) throws IOException, InterruptedException { + String envelope = """ + + + + + + %s + + + + %s + Multidimensional + TupleFormat + + + + + + """.formatted(mdx, catalog); + + HttpRequest.Builder request = HttpRequest.newBuilder(URI.create("http://" + host + ":" + port + "/xmla")) + .header("Content-Type", "text/xml; charset=utf-8") + .header("SOAPAction", "\"urn:schemas-microsoft-com:xml-analysis:Execute\"") + .timeout(Duration.ofSeconds(30)) + .POST(HttpRequest.BodyPublishers.ofString(envelope)); + if (basicCredentials != null) { + request.header("Authorization", "Basic " + + Base64.getEncoder().encodeToString(basicCredentials.getBytes(StandardCharsets.UTF_8))); + } + + try (HttpClient client = HttpClient.newHttpClient()) { + return client.send(request.build(), HttpResponse.BodyHandlers.ofString()); + } + } + + /** + * Polls the XMLA endpoint until the MDX query returns a cell with the + * expected value — the OLAP context needs a moment to connect to the + * database after container start. + */ + public static String awaitMdxCell(String host, int port, String catalog, String mdx, String expectedValue) + throws InterruptedException { + return awaitMdxCell(host, port, catalog, mdx, expectedValue, null); + } + + /** As {@link #awaitMdxCell(String, int, String, String, String)}, with HTTP Basic credentials. */ + public static String awaitMdxCell(String host, int port, String catalog, String mdx, String expectedValue, + String basicCredentials) throws InterruptedException { + long deadline = System.currentTimeMillis() + Duration.ofMinutes(3).toMillis(); + String last = ""; + while (System.currentTimeMillis() < deadline) { + try { + last = executeMdx(host, port, catalog, mdx, basicCredentials).body(); + if (last.contains(">" + expectedValue + "") + || last.contains(expectedValue + "")) { + return last; + } + } catch (IOException e) { + last = e.toString(); + } + Thread.sleep(2_000); + } + throw new AssertionError( + "MDX did not return expected cell value " + expectedValue + "; last response:\n" + last); + } +} diff --git a/application/pivot/daanse.pivot.bndrun b/application/pivot/daanse.pivot.bndrun deleted file mode 100644 index 110cc86..0000000 --- a/application/pivot/daanse.pivot.bndrun +++ /dev/null @@ -1,124 +0,0 @@ -#******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# This program and the accompanying materials are made -# available under the terms of the Eclipse Public License 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0/ -# -# SPDX-License-Identifier: EPL-2.0 -# -# Contributors: -# SmartCity Jena - initial -# Stefan Bischof (bipolis.org) - initial -#******************************************************************************* - --runtrace: true - --runproperties: \ - org.slf4j.simpleLogger.defaultLogLevel=error,\ - org.osgi.service.http.port=8090,\ - logback.configurationFile=./logback.xml - --runpath: \ - ch.qos.logback.classic,\ - ch.qos.logback.core,\ - slf4j.api - --runee: JavaSE-25 --runfw: org.apache.felix.framework - --runrequires: \ - bnd.identity;id='org.glassfish.hk2.osgi-resource-locator',\ - bnd.identity;id='com.sun.xml.messaging.saaj.impl',\ - bnd.identity;id='org.apache.aries.spifly.dynamic.framework.extension',\ - bnd.identity;id='org.apache.felix.metatype',\ - bnd.identity;id='org.eclipse.daanse.olap.xmla.bridge',\ - bnd.identity;id='org.eclipse.daanse.xmla.server.jakarta.saaj',\ - bnd.identity;id='ch.qos.logback.classic',\ - bnd.identity;id='org.eclipse.daanse.mdx.parser.api',\ - bnd.identity;id='org.eclipse.daanse.mdx.parser.ccc',\ - bnd.identity;id='org.eclipse.daanse.rolap.mapping.model',\ - bnd.identity;id='org.eclipse.daanse.sql.guard.api',\ - bnd.identity;id='org.eclipse.daanse.sql.guard.jsqltranspiler',\ - bnd.identity;id='org.eclipse.daanse.rolap.core',\ - bnd.identity;id='org.apache.felix.fileinstall',\ - bnd.identity;id='org.apache.felix.configadmin.plugin.interpolation',\ - bnd.identity;id='org.eclipse.fennec.emf.osgi.component' - - -# This will help us keep -runbundles sorted --runstartlevel: \ - order=sortbynameversion,\ - begin=-1 - - - - --runbundles: \ - ai.starlake.transpiler;version='[1.8.0,1.8.1)',\ - ch.qos.logback.classic;version='[1.5.6,1.5.7)',\ - com.github.ben-manes.caffeine;version='[3.1.8,3.1.9)',\ - com.sun.xml.messaging.saaj.impl;version='[3.0.4,3.0.5)',\ - jakarta.activation-api;version='[2.1.3,2.1.4)',\ - jakarta.xml.soap-api;version='[3.0.2,3.0.3)',\ - net.sf.jsqlparser;version='[5.3.167,5.3.168)',\ - org.apache.aries.spifly.dynamic.framework.extension;version='[1.3.7,1.3.8)',\ - org.apache.commons.commons-collections4;version='[4.5.0,4.5.1)',\ - org.apache.commons.commons-fileupload;version='[1.6.0,1.6.1)',\ - org.apache.commons.commons-io;version='[2.19.0,2.19.1)',\ - org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ - org.apache.felix.configadmin.plugin.interpolation;version='[1.2.8,1.2.9)',\ - org.apache.felix.fileinstall;version='[3.7.4,3.7.5)',\ - org.apache.felix.http.jetty12;version='[1.1.6,1.1.7)',\ - org.apache.felix.http.servlet-api;version='[3.0.0,3.0.1)',\ - org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ - org.apache.felix.scr;version='[2.2.10,2.2.11)',\ - org.eclipse.daanse.cwm.model.cwm;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.cwm.util;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.jakarta.servlet.soap;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.lcid.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.mdx.model.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.mdx.model.record;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.mdx.parser.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.mdx.parser.ccc;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.olap.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.olap.common;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.olap.format;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.olap.spi;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.olap.util.format;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.olap.xmla.bridge;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.rolap.core;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.rolap.mapping.model;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.sql.statement.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.sql.statement.impl;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.xmla.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.xmla.csdl.model.v2.an;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.xmla.csdl.model.v2.bi;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.xmla.csdl.model.v2.cg;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.xmla.csdl.model.v2.edm;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.xmla.model.record;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.xmla.server.adapter.soapmessage;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.xmla.server.jakarta.saaj;version='[0.0.1,0.0.2)',\ - org.eclipse.emf.common;version='[2.44.0,2.44.1)',\ - org.eclipse.emf.ecore;version='[2.41.0,2.41.1)',\ - org.eclipse.emf.ecore.xmi;version='[2.39.0,2.39.1)',\ - org.eclipse.fennec.emf.osgi.component;version='[0.1.1,0.1.2)',\ - org.glassfish.hk2.osgi-resource-locator;version='[2.5.0,2.5.1)',\ - org.jvnet.staxex.stax-ex;version='[2.1.0,2.1.1)',\ - org.objectweb.asm;version='[9.9.0,9.9.1)',\ - org.objectweb.asm.commons;version='[9.9.0,9.9.1)',\ - org.objectweb.asm.tree;version='[9.9.0,9.9.1)',\ - org.objectweb.asm.tree.analysis;version='[9.9.0,9.9.1)',\ - org.objectweb.asm.util;version='[9.9.0,9.9.1)',\ - org.osgi.service.cm;version='[1.6.1,1.6.2)',\ - org.osgi.service.component;version='[1.5.1,1.5.2)',\ - org.osgi.service.log;version='[1.4.0,1.4.1)',\ - org.osgi.util.converter;version='[1.0.9,1.0.10)',\ - org.osgi.util.function;version='[1.2.0,1.2.1)',\ - org.osgi.util.promise;version='[1.3.0,1.3.1)',\ - org.osgi.util.pushstream;version='[1.1.0,1.1.1)' diff --git a/application/pivot/duckdb/README.md b/application/pivot/duckdb/README.md new file mode 100644 index 0000000..209eec4 --- /dev/null +++ b/application/pivot/duckdb/README.md @@ -0,0 +1,41 @@ +# Daanse Pivot Server — DuckDB + +Production Daanse OLAP (XMLA) server image for **DuckDB**. The image ships the +Daanse ROLAP engine, the XMLA endpoint, the DuckDB JDBC driver, the DataSource +and the matching SQL dialect. It is configured entirely through environment +variables. The DuckDB database is a file mounted into the container and is +opened read-only by default (an in-memory DuckDB would be private to each +connection and is therefore not useful here). + +Note: this image is based on Debian (`eclipse-temurin:25-jre`) instead of +Alpine because the DuckDB native library requires glibc. + +Images: `docker.io/eclipsedaanse/daanse-pivot-duckdb:snapshot` and +`ghcr.io/eclipse-daanse/daanse-pivot-duckdb:snapshot` (linux/amd64, linux/arm64). + +## Quick start + +```bash +docker run --name daanse-pivot \ + -v ./catalog:/app/catalog:ro \ + -v ./database.duckdb:/app/data/database.duckdb:ro \ + -p 8080:8080 \ + eclipsedaanse/daanse-pivot-duckdb:snapshot +``` + +The XMLA endpoint is then available at `http://localhost:8080/xmla`. + +## DuckDB environment variables + +There are no required variables — mount the database file at the default +location and the server starts. + +| Variable | Default | Description | +|---|---|---| +| `DAANSE_JDBC_DATABASE_NAME` | `/app/data/database.duckdb` | Path of the DuckDB database file inside the container | +| `DAANSE_JDBC_READ_ONLY` | `true` | Open the database read-only (allows other processes to read the same file); the connection pool inherits this mode | +| `DAANSE_JDBC_SETTINGS` | *(unset)* | Comma separated DuckDB settings, each as `name=value` | + +## Source + +`https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot/duckdb` diff --git a/application/pivot/duckdb/container/Dockerfile b/application/pivot/duckdb/container/Dockerfile new file mode 100644 index 0000000..ae63f6e --- /dev/null +++ b/application/pivot/duckdb/container/Dockerfile @@ -0,0 +1,35 @@ +# DuckDB JDBC ships glibc natives — musl/Alpine is not supported +FROM eclipse-temurin:25-jre + +# Configurable UID/GID to avoid permission problems with bind mounts (e.g. rootless Podman) +ARG APP_UID=1000 +ARG APP_GID=1000 + +# Create non-root user for security. The Ubuntu based image may already ship a +# user with this UID — reuse it in that case and address it numerically below. +RUN getent group ${APP_GID} >/dev/null || groupadd --system --gid ${APP_GID} appgroup; \ + getent passwd ${APP_UID} >/dev/null || useradd --system --uid ${APP_UID} --gid ${APP_GID} appuser + +LABEL org.opencontainers.image.source="https://github.com/eclipse-daanse/org.eclipse.daanse.server" \ + org.opencontainers.image.licenses="EPL-2.0" \ + org.opencontainers.image.vendor="Eclipse Daanse" \ + org.opencontainers.image.description="Daanse OLAP (XMLA) server for DuckDB" + +WORKDIR /app + +# Copy application files +COPY --chown=${APP_UID}:${APP_GID} application/pivot/duckdb/target/daanse.pivot.duckdb.jar /app/ +COPY --chown=${APP_UID}:${APP_GID} application/pivot/duckdb/start /app/ +COPY --chown=${APP_UID}:${APP_GID} application/pivot/duckdb/logback.xml /app/ + +RUN mkdir -p /app/catalog /app/data /app/log \ + && chown -R ${APP_UID}:${APP_GID} /app \ + && chmod +x /app/start + +USER ${APP_UID}:${APP_GID} + +VOLUME ["/app/catalog"] + +EXPOSE 8080 + +ENTRYPOINT ["sh","-c","exec /app/start"] diff --git a/application/pivot/duckdb/daanse.pivot.duckdb.bndrun b/application/pivot/duckdb/daanse.pivot.duckdb.bndrun new file mode 100644 index 0000000..5a1fd1a --- /dev/null +++ b/application/pivot/duckdb/daanse.pivot.duckdb.bndrun @@ -0,0 +1,130 @@ +#******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# SmartCity Jena - initial +# Stefan Bischof (bipolis.org) - initial +#******************************************************************************* + +-include: ~${.}/../common/daanse.pivot.base.bndrun + +-runrequires.db: \ + bnd.identity;id='${project.artifactId}',\ + bnd.identity;id='org.eclipse.daanse.jdbc.datasource.duckdb',\ + bnd.identity;id='org.eclipse.daanse.sql.dialect.db.duckdb' + +-runbundles: \ + ai.starlake.transpiler;version='[1.8.0,1.8.1)',\ + ch.qos.logback.classic;version='[1.5.6,1.5.7)',\ + com.fasterxml.woodstox.woodstox-core;version='[7.1.1,7.1.2)',\ + com.github.ben-manes.caffeine;version='[3.1.8,3.1.9)',\ + com.zaxxer.HikariCP;version='[7.1.0,7.1.1)',\ + net.sf.jsqlparser;version='[5.3.167,5.3.168)',\ + org.apache.aries.spifly.dynamic.bundle;version='[1.3.7,1.3.8)',\ + org.apache.commons.commons-collections4;version='[4.5.0,4.5.1)',\ + org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ + org.apache.felix.http.jetty12;version='[2.0.4,2.0.5)',\ + org.apache.felix.http.servlet-api;version='[6.1.0,6.1.1)',\ + org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ + org.apache.felix.scr;version='[2.2.10,2.2.11)',\ + org.duckdb.duckdb_jdbc;version='[1.5.5,1.5.6)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.businessinformation;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.datatypes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.keysindexes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.behavioral;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.instance;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.resource.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jakarta.servlet.filter.cors;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.duckdb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.hikari;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.cfg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.spi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.util.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational.oledb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.mapping.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.duckdb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.duckdb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.an;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.bi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.cg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.edm;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.empty;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine200;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300_300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.exception;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.ext;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.io;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.mddataset;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.msxmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.multipleresults;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.mining;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.server;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.soap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.xmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.adapter.emf;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.roles;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.store.ldap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.whiteboard.servlet;version='[0.0.1,0.0.2)',\ + org.eclipse.emf.common;version='[2.44.0,2.44.1)',\ + org.eclipse.emf.ecore;version='[2.41.0,2.41.1)',\ + org.eclipse.emf.ecore.xmi;version='[2.39.0,2.39.1)',\ + org.eclipse.fennec.emf.osgi.component;version='[0.1.1,0.1.2)',\ + org.glassfish.hk2.osgi-resource-locator;version='[2.4.0,2.4.1)',\ + org.objectweb.asm;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.commons;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree.analysis;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.util;version='[9.9.0,9.9.1)',\ + org.osgi.service.cm;version='[1.6.1,1.6.2)',\ + org.osgi.service.component;version='[1.5.1,1.5.2)',\ + org.osgi.service.log;version='[1.4.0,1.4.1)',\ + org.osgi.util.converter;version='[1.0.9,1.0.10)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.3.0,1.3.1)',\ + org.osgi.util.pushstream;version='[1.1.0,1.1.1)',\ + stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file diff --git a/application/pivot/duckdb/logback.xml b/application/pivot/duckdb/logback.xml new file mode 100644 index 0000000..f6ebf80 --- /dev/null +++ b/application/pivot/duckdb/logback.xml @@ -0,0 +1,32 @@ + + + + + + + true + + + + + + %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + + diff --git a/application/pivot/duckdb/pom.xml b/application/pivot/duckdb/pom.xml new file mode 100644 index 0000000..e83535f --- /dev/null +++ b/application/pivot/duckdb/pom.xml @@ -0,0 +1,158 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot + ${revision} + + org.eclipse.daanse.server.application.pivot.duckdb + + + duckdb + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.duckdb + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.duckdb + 0.0.1-SNAPSHOT + runtime + + + + org.osgi + org.osgi.service.jdbc + 1.1.0 + runtime + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + test-jar + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + + + + biz.aQute.bnd + bnd-export-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + bnd.executablejar + true + false + + compile + runtime + + + + + + export + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + true + false + + compile + runtime + + + + + biz.aQute.bnd + bnd-run-maven-plugin + ${bnd.version} + + daanse.pivot.${pivot.db}.bndrun + true + + compile + runtime + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + + integration-test + verify + + + + + + + diff --git a/application/pivot/duckdb/src/main/java/org/eclipse/daanse/server/application/pivot/duckdb/DuckdbEnvConfigurator.java b/application/pivot/duckdb/src/main/java/org/eclipse/daanse/server/application/pivot/duckdb/DuckdbEnvConfigurator.java new file mode 100644 index 0000000..2388448 --- /dev/null +++ b/application/pivot/duckdb/src/main/java/org/eclipse/daanse/server/application/pivot/duckdb/DuckdbEnvConfigurator.java @@ -0,0 +1,92 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.duckdb; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.List; + +import org.eclipse.daanse.jdbc.datasource.duckdb.api.Constants; +import org.eclipse.daanse.jdbc.datasource.duckdb.api.ocd.DsConfig; +import org.eclipse.daanse.server.application.pivot.common.BasicContextConfigs; +import org.eclipse.daanse.server.application.pivot.common.EnvConfigMapper; +import org.eclipse.daanse.server.application.pivot.common.ServerConstants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.annotations.RequireConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.RequireServiceComponentRuntime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Registers the DuckDB DataSource from the {@code DAANSE_JDBC_*} environment + * variables and creates the BasicContext against it. Every attribute of + * {@link DsConfig} is supported. The database file defaults to + * {@code /app/data/database.duckdb} (mount it into the container) and is + * opened read-only unless {@code DAANSE_JDBC_READ_ONLY=false}. + */ +@Component(immediate = true) +@RequireConfigurationAdmin +@RequireServiceComponentRuntime +public class DuckdbEnvConfigurator { + + private static final Logger logger = LoggerFactory.getLogger(DuckdbEnvConfigurator.class); + + private static final String DIALECT_NAME = "DUCKDB"; + + private static final String DEFAULT_DATABASE_FILE = "/app/data/database.duckdb"; + + @Reference + ConfigurationAdmin ca; + + private Configuration confDataSource; + private List confPoolAndContext = List.of(); + + @Activate + public void activate() throws IOException { + Dictionary props = EnvConfigMapper.propsFromEnv(DsConfig.class); + if (props.get(Constants.DATASOURCE_PROPERTY_DATABASENAME) == null) { + props.put(Constants.DATASOURCE_PROPERTY_DATABASENAME, DEFAULT_DATABASE_FILE); + } + if (props.get(Constants.DATASOURCE_PROPERTY_READ_ONLY) == null) { + props.put(Constants.DATASOURCE_PROPERTY_READ_ONLY, true); + } + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_DATASOURCE); + + confDataSource = ca.getFactoryConfiguration(Constants.PID_DATASOURCE, ServerConstants.CONFIG_IDENT, "?"); + confDataSource.update(props); + + // DuckDB connections are fixed to the read-only mode of the DataSource; + // the pool must mark its connections the same way or the driver refuses. + confPoolAndContext = BasicContextConfigs.createEnvPoolAndContext(ca, DIALECT_NAME, + (Boolean) props.get(Constants.DATASOURCE_PROPERTY_READ_ONLY)); + + logger.info("DuckDB DataSource, connection pool and context configured from environment"); + } + + @Deactivate + public void deactivate() throws IOException { + if (confDataSource != null) { + confDataSource.delete(); + } + for (Configuration configuration : confPoolAndContext) { + configuration.delete(); + } + confPoolAndContext = List.of(); + } +} diff --git a/application/pivot/duckdb/src/main/java/org/eclipse/daanse/server/application/pivot/duckdb/package-info.java b/application/pivot/duckdb/src/main/java/org/eclipse/daanse/server/application/pivot/duckdb/package-info.java new file mode 100644 index 0000000..96f11ba --- /dev/null +++ b/application/pivot/duckdb/src/main/java/org/eclipse/daanse/server/application/pivot/duckdb/package-info.java @@ -0,0 +1,14 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.duckdb; diff --git a/application/pivot/duckdb/src/test/java/org/eclipse/daanse/server/application/pivot/duckdb/DuckdbPivotIT.java b/application/pivot/duckdb/src/test/java/org/eclipse/daanse/server/application/pivot/duckdb/DuckdbPivotIT.java new file mode 100644 index 0000000..fd2636f --- /dev/null +++ b/application/pivot/duckdb/src/test/java/org/eclipse/daanse/server/application/pivot/duckdb/DuckdbPivotIT.java @@ -0,0 +1,68 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.duckdb; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.server.application.pivot.common.test.PivotContainers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +/** + * End-to-end test: creates a DuckDB database file with the Fact table of the + * static test catalog, builds the daanse-pivot-duckdb image from its real + * Dockerfile, mounts database file and catalog and verifies via XMLA that the + * MDX sum over the VALUE column is queryable. + */ +@Testcontainers(disabledWithoutDocker = true) +class DuckdbPivotIT { + + @TempDir + static Path tempDir; + + @Test + void sumMeasureIsQueryableViaXmla() throws Exception { + Path databaseFile = tempDir.resolve("database.duckdb"); + try (Connection connection = DriverManager.getConnection("jdbc:duckdb:" + databaseFile); + Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE TABLE \"Fact\" (\"KEY\" VARCHAR, \"VALUE\" INTEGER)"); + statement.executeUpdate("INSERT INTO \"Fact\" VALUES ('A', 1), ('B', 2), ('C', 3)"); + } + assertTrue(Files.exists(databaseFile)); + + try (GenericContainer pivot = new GenericContainer<>(PivotContainers.pivotImage("duckdb")) + .withCopyFileToContainer(MountableFile.forClasspathResource("catalog.xmi"), + "/app/catalog/catalog.xmi") + .withCopyFileToContainer(MountableFile.forHostPath(databaseFile), "/app/data/database.duckdb") + .withExposedPorts(8080) + .waitingFor(Wait.forListeningPort())) { + pivot.start(); + + String response = PivotContainers.awaitMdxCell(pivot.getHost(), pivot.getMappedPort(8080), + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "6"); + + assertTrue(response.contains("6"), response); + } + } +} diff --git a/application/pivot/duckdb/src/test/resources/catalog.xmi b/application/pivot/duckdb/src/test/resources/catalog.xmi new file mode 100644 index 0000000..65db914 --- /dev/null +++ b/application/pivot/duckdb/src/test/resources/catalog.xmi @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/application/pivot/duckdb/start b/application/pivot/duckdb/start new file mode 100755 index 0000000..5a10b7b --- /dev/null +++ b/application/pivot/duckdb/start @@ -0,0 +1,19 @@ +#!/bin/sh + +JAVA_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/}java" +LOGBACK_CONFIG="./logback.xml" + +# The bundles declare osgi.ee=JavaSE-25; on an older JVM nothing resolves. +JAVA_MAJOR="$("$JAVA_EXEC" -version 2>&1 | awk -F'"' '/version/ { split($2, v, /[._]/); print (v[1] == "1" ? v[2] : v[1]); exit }')" +if [ "${JAVA_MAJOR:-0}" -lt 25 ]; then + echo "Java 25 is required, but $JAVA_EXEC is Java ${JAVA_MAJOR:-unknown}. Point JAVA_HOME at a Java 25 installation." + exit 1 +fi + +if [ ! -f "$LOGBACK_CONFIG" ]; then + echo "Logback-Configuration-File $LOGBACK_CONFIG not found." + exit 1 +fi + +echo "start Server" +"$JAVA_EXEC" -Dlogback.configurationFile=file:"$LOGBACK_CONFIG" -jar daanse.pivot.duckdb.jar diff --git a/application/pivot/h2/README.md b/application/pivot/h2/README.md new file mode 100644 index 0000000..96e4350 --- /dev/null +++ b/application/pivot/h2/README.md @@ -0,0 +1,42 @@ +# Daanse Pivot Server — H2 + +Production Daanse OLAP (XMLA) server image for **H2**. The image ships the +Daanse ROLAP engine, the XMLA endpoint, the H2 JDBC driver, the DataSource and +the matching SQL dialect. It is configured entirely through environment +variables. The H2 database is a file mounted into the container. + +Images: `docker.io/eclipsedaanse/daanse-pivot-h2:snapshot` and +`ghcr.io/eclipse-daanse/daanse-pivot-h2:snapshot` (linux/amd64, linux/arm64). + +## Quick start + +```bash +docker run --name daanse-pivot \ + -v ./catalog:/app/catalog:ro \ + -v ./database.mv.db:/app/data/database.mv.db \ + -p 8080:8080 \ + eclipsedaanse/daanse-pivot-h2:snapshot +``` + +The XMLA endpoint is then available at `http://localhost:8080/xmla`. + +## H2 environment variables + +There are no required variables — mount the database file at the default +location and the server starts. The database identifier is the file path +without the `.mv.db` suffix. + +| Variable | Default | Description | +|---|---|---| +| `DAANSE_JDBC_IDENTIFIER` | `/app/data/database` | Database identifier (path for the `file` filesystem) | +| `DAANSE_JDBC_PLUGABLE_FILESYSTEM` | `file` | H2 pluggable filesystem (`file`, `zip`, `nioMapped`, `async`, `memFS`, `memLZF`, `nioMemFS`, `nioMemLZF`) | +| `DAANSE_JDBC_USERNAME` | *(unset)* | Database user (note: derived from the `username` attribute, not `user`) | +| `DAANSE_JDBC_PASSWORD` | *(unset)* | Database password | +| `DAANSE_JDBC_DATABASE_TO_UPPER` | `true` | Fold unquoted identifiers to upper case | +| `DAANSE_JDBC_DB_CLOSE_DELAY` | `0` | Keep the database open after the last connection closes (seconds, -1 = until VM exit) | +| `DAANSE_JDBC_DEBUG` | `false` | H2 trace output | +| `DAANSE_JDBC_DESCRIPTION` | *(unset)* | Description of the DataSource | + +## Source + +`https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot/h2` diff --git a/application/pivot/h2/container/Dockerfile b/application/pivot/h2/container/Dockerfile new file mode 100644 index 0000000..8e722ed --- /dev/null +++ b/application/pivot/h2/container/Dockerfile @@ -0,0 +1,33 @@ +FROM eclipse-temurin:25-jre-alpine + +# Configurable UID/GID to avoid permission problems with bind mounts (e.g. rootless Podman) +ARG APP_UID=1000 +ARG APP_GID=1000 + +# Create non-root user for security +RUN addgroup -S -g ${APP_GID} appgroup \ + && adduser -S -u ${APP_UID} -G appgroup appuser + +LABEL org.opencontainers.image.source="https://github.com/eclipse-daanse/org.eclipse.daanse.server" \ + org.opencontainers.image.licenses="EPL-2.0" \ + org.opencontainers.image.vendor="Eclipse Daanse" \ + org.opencontainers.image.description="Daanse OLAP (XMLA) server for H2" + +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appgroup application/pivot/h2/target/daanse.pivot.h2.jar /app/ +COPY --chown=appuser:appgroup application/pivot/h2/start /app/ +COPY --chown=appuser:appgroup application/pivot/h2/logback.xml /app/ + +RUN mkdir -p /app/catalog /app/data /app/log \ + && chown -R appuser:appgroup /app \ + && chmod +x /app/start + +USER appuser + +VOLUME ["/app/catalog"] + +EXPOSE 8080 + +ENTRYPOINT ["sh","-c","exec /app/start"] diff --git a/application/pivot/h2/daanse.pivot.h2.bndrun b/application/pivot/h2/daanse.pivot.h2.bndrun new file mode 100644 index 0000000..f5f156e --- /dev/null +++ b/application/pivot/h2/daanse.pivot.h2.bndrun @@ -0,0 +1,130 @@ +#******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# SmartCity Jena - initial +# Stefan Bischof (bipolis.org) - initial +#******************************************************************************* + +-include: ~${.}/../common/daanse.pivot.base.bndrun + +-runrequires.db: \ + bnd.identity;id='${project.artifactId}',\ + bnd.identity;id='org.eclipse.daanse.jdbc.datasource.h2',\ + bnd.identity;id='org.eclipse.daanse.sql.dialect.db.h2' + +-runbundles: \ + ai.starlake.transpiler;version='[1.8.0,1.8.1)',\ + ch.qos.logback.classic;version='[1.5.6,1.5.7)',\ + com.fasterxml.woodstox.woodstox-core;version='[7.1.1,7.1.2)',\ + com.github.ben-manes.caffeine;version='[3.1.8,3.1.9)',\ + com.h2database;version='[2.3.230,2.3.231)',\ + com.zaxxer.HikariCP;version='[7.1.0,7.1.1)',\ + net.sf.jsqlparser;version='[5.3.167,5.3.168)',\ + org.apache.aries.spifly.dynamic.bundle;version='[1.3.7,1.3.8)',\ + org.apache.commons.commons-collections4;version='[4.5.0,4.5.1)',\ + org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ + org.apache.felix.http.jetty12;version='[2.0.4,2.0.5)',\ + org.apache.felix.http.servlet-api;version='[6.1.0,6.1.1)',\ + org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ + org.apache.felix.scr;version='[2.2.10,2.2.11)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.businessinformation;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.datatypes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.keysindexes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.behavioral;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.instance;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.resource.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jakarta.servlet.filter.cors;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.h2;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.hikari;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.cfg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.spi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.util.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational.oledb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.mapping.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.h2;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.h2;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.an;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.bi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.cg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.edm;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.empty;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine200;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300_300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.exception;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.ext;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.io;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.mddataset;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.msxmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.multipleresults;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.mining;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.server;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.soap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.xmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.adapter.emf;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.roles;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.store.ldap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.whiteboard.servlet;version='[0.0.1,0.0.2)',\ + org.eclipse.emf.common;version='[2.44.0,2.44.1)',\ + org.eclipse.emf.ecore;version='[2.41.0,2.41.1)',\ + org.eclipse.emf.ecore.xmi;version='[2.39.0,2.39.1)',\ + org.eclipse.fennec.emf.osgi.component;version='[0.1.1,0.1.2)',\ + org.glassfish.hk2.osgi-resource-locator;version='[2.4.0,2.4.1)',\ + org.objectweb.asm;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.commons;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree.analysis;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.util;version='[9.9.0,9.9.1)',\ + org.osgi.service.cm;version='[1.6.1,1.6.2)',\ + org.osgi.service.component;version='[1.5.1,1.5.2)',\ + org.osgi.service.log;version='[1.4.0,1.4.1)',\ + org.osgi.util.converter;version='[1.0.9,1.0.10)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.3.0,1.3.1)',\ + org.osgi.util.pushstream;version='[1.1.0,1.1.1)',\ + stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file diff --git a/application/pivot/h2/logback.xml b/application/pivot/h2/logback.xml new file mode 100644 index 0000000..f6ebf80 --- /dev/null +++ b/application/pivot/h2/logback.xml @@ -0,0 +1,32 @@ + + + + + + + true + + + + + + %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + + diff --git a/application/pivot/h2/pom.xml b/application/pivot/h2/pom.xml new file mode 100644 index 0000000..8fe30ec --- /dev/null +++ b/application/pivot/h2/pom.xml @@ -0,0 +1,158 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot + ${revision} + + org.eclipse.daanse.server.application.pivot.h2 + + + h2 + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.h2 + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.h2 + 0.0.1-SNAPSHOT + runtime + + + + org.osgi + org.osgi.service.jdbc + 1.1.0 + runtime + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + test-jar + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + + + + biz.aQute.bnd + bnd-export-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + bnd.executablejar + true + false + + compile + runtime + + + + + + export + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + true + false + + compile + runtime + + + + + biz.aQute.bnd + bnd-run-maven-plugin + ${bnd.version} + + daanse.pivot.${pivot.db}.bndrun + true + + compile + runtime + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + + integration-test + verify + + + + + + + diff --git a/application/pivot/h2/src/main/java/org/eclipse/daanse/server/application/pivot/h2/H2EnvConfigurator.java b/application/pivot/h2/src/main/java/org/eclipse/daanse/server/application/pivot/h2/H2EnvConfigurator.java new file mode 100644 index 0000000..2aac293 --- /dev/null +++ b/application/pivot/h2/src/main/java/org/eclipse/daanse/server/application/pivot/h2/H2EnvConfigurator.java @@ -0,0 +1,86 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.h2; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.List; + +import org.eclipse.daanse.jdbc.datasource.h2.api.Constants; +import org.eclipse.daanse.jdbc.datasource.h2.api.ocd.DsConfig; +import org.eclipse.daanse.server.application.pivot.common.BasicContextConfigs; +import org.eclipse.daanse.server.application.pivot.common.EnvConfigMapper; +import org.eclipse.daanse.server.application.pivot.common.ServerConstants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.annotations.RequireConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.RequireServiceComponentRuntime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Registers the H2 DataSource from the {@code DAANSE_JDBC_*} environment + * variables and creates the BasicContext against it. Every attribute of + * {@link DsConfig} is supported. The database identifier defaults to + * {@code /app/data/database} on the {@code file} filesystem (mount the + * database file into the container). + */ +@Component(immediate = true) +@RequireConfigurationAdmin +@RequireServiceComponentRuntime +public class H2EnvConfigurator { + + private static final Logger logger = LoggerFactory.getLogger(H2EnvConfigurator.class); + + private static final String DIALECT_NAME = "H2"; + + private static final String DEFAULT_IDENTIFIER = "/app/data/database"; + + @Reference + ConfigurationAdmin ca; + + private Configuration confDataSource; + private List confPoolAndContext = List.of(); + + @Activate + public void activate() throws IOException { + Dictionary props = EnvConfigMapper.propsFromEnv(DsConfig.class); + if (props.get(Constants.DATASOURCE_PROPERTY_IDENTIFIER) == null) { + props.put(Constants.DATASOURCE_PROPERTY_IDENTIFIER, DEFAULT_IDENTIFIER); + } + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_DATASOURCE); + + confDataSource = ca.getFactoryConfiguration(Constants.PID_DATASOURCE, ServerConstants.CONFIG_IDENT, "?"); + confDataSource.update(props); + + confPoolAndContext = BasicContextConfigs.createEnvPoolAndContext(ca, DIALECT_NAME); + + logger.info("H2 DataSource, connection pool and context configured from environment"); + } + + @Deactivate + public void deactivate() throws IOException { + if (confDataSource != null) { + confDataSource.delete(); + } + for (Configuration configuration : confPoolAndContext) { + configuration.delete(); + } + confPoolAndContext = List.of(); + } +} diff --git a/application/pivot/h2/src/main/java/org/eclipse/daanse/server/application/pivot/h2/package-info.java b/application/pivot/h2/src/main/java/org/eclipse/daanse/server/application/pivot/h2/package-info.java new file mode 100644 index 0000000..9504cb0 --- /dev/null +++ b/application/pivot/h2/src/main/java/org/eclipse/daanse/server/application/pivot/h2/package-info.java @@ -0,0 +1,14 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.h2; diff --git a/application/pivot/h2/src/test/java/org/eclipse/daanse/server/application/pivot/h2/H2PivotIT.java b/application/pivot/h2/src/test/java/org/eclipse/daanse/server/application/pivot/h2/H2PivotIT.java new file mode 100644 index 0000000..54347e1 --- /dev/null +++ b/application/pivot/h2/src/test/java/org/eclipse/daanse/server/application/pivot/h2/H2PivotIT.java @@ -0,0 +1,69 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.h2; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.server.application.pivot.common.test.PivotContainers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +/** + * End-to-end test: creates an H2 database file with the Fact table of the + * static test catalog, builds the daanse-pivot-h2 image from its real + * Dockerfile, mounts database file and catalog and verifies via XMLA that the + * MDX sum over the VALUE column is queryable. + */ +@Testcontainers(disabledWithoutDocker = true) +class H2PivotIT { + + @TempDir + static Path tempDir; + + @Test + void sumMeasureIsQueryableViaXmla() throws Exception { + Path databaseBase = tempDir.resolve("database"); + try (Connection connection = DriverManager.getConnection("jdbc:h2:file:" + databaseBase); + Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE TABLE \"Fact\" (\"KEY\" VARCHAR(100), \"VALUE\" INTEGER)"); + statement.executeUpdate("INSERT INTO \"Fact\" VALUES ('A', 1), ('B', 2), ('C', 3)"); + } + Path databaseFile = tempDir.resolve("database.mv.db"); + assertTrue(Files.exists(databaseFile)); + + try (GenericContainer pivot = new GenericContainer<>(PivotContainers.pivotImage("h2")) + .withCopyFileToContainer(MountableFile.forClasspathResource("catalog.xmi"), + "/app/catalog/catalog.xmi") + .withCopyFileToContainer(MountableFile.forHostPath(databaseFile), "/app/data/database.mv.db") + .withExposedPorts(8080) + .waitingFor(Wait.forListeningPort())) { + pivot.start(); + + String response = PivotContainers.awaitMdxCell(pivot.getHost(), pivot.getMappedPort(8080), + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "6"); + + assertTrue(response.contains("6"), response); + } + } +} diff --git a/application/pivot/h2/src/test/java/org/eclipse/daanse/server/application/pivot/h2/LdapAuthPivotIT.java b/application/pivot/h2/src/test/java/org/eclipse/daanse/server/application/pivot/h2/LdapAuthPivotIT.java new file mode 100644 index 0000000..4aaa514 --- /dev/null +++ b/application/pivot/h2/src/test/java/org/eclipse/daanse/server/application/pivot/h2/LdapAuthPivotIT.java @@ -0,0 +1,141 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.h2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.http.HttpResponse; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.server.application.pivot.common.test.PivotContainers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.containers.Container.ExecResult; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +/** + * End-to-end test of the LDAP backed basic authentication: an LLDAP side + * container is seeded by LLDAP's official bootstrap script from the JSON files + * shipped in {@code common/example/ldap/bootstrap}, exactly as the container + * README describes, and the pivot image authenticates against it. Verifies the + * documented behaviour: valid credentials answer, wrong credentials are + * rejected, anonymous requests are served by default and rejected once + * {@code DAANSE_AUTH_ANONYMOUS=false}. + */ +@Testcontainers(disabledWithoutDocker = true) +class LdapAuthPivotIT { + + private static final Path BOOTSTRAP_DIR = Path.of("../common/example/ldap/bootstrap"); + + @TempDir + static Path tempDir; + + @Test + void basicAuthAgainstLdapSideContainer() throws Exception { + Path databaseBase = tempDir.resolve("database"); + try (Connection connection = DriverManager.getConnection("jdbc:h2:file:" + databaseBase); + Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE TABLE \"Fact\" (\"KEY\" VARCHAR(100), \"VALUE\" INTEGER)"); + statement.executeUpdate("INSERT INTO \"Fact\" VALUES ('A', 1), ('B', 2), ('C', 3)"); + } + Path databaseFile = tempDir.resolve("database.mv.db"); + + try (Network network = Network.newNetwork(); + GenericContainer lldap = new GenericContainer<>("lldap/lldap:stable") + .withNetwork(network).withNetworkAliases("lldap") + .withEnv("LLDAP_LDAP_BASE_DN", "dc=example,dc=org") + .withEnv("LLDAP_LDAP_USER_PASS", "admin123") + .withEnv("LLDAP_KEY_SEED", "integration-test") + .withEnv("LLDAP_JWT_SECRET", "integration-test-jwt-secret-0123456789") + .withCopyFileToContainer(MountableFile.forHostPath(BOOTSTRAP_DIR), "/bootstrap") + .withExposedPorts(3890, 17170) + .waitingFor(Wait.forListeningPorts(3890, 17170))) { + lldap.start(); + + // LLDAP's own bootstrap script loads the example users and groups. + ExecResult bootstrap = lldap.execInContainer("/bin/sh", "-c", + "LLDAP_URL=http://localhost:17170 LLDAP_ADMIN_USERNAME=admin " + + "LLDAP_ADMIN_PASSWORD=admin123 DO_CLEANUP=false /app/bootstrap.sh"); + assertEquals(0, bootstrap.getExitCode(), bootstrap.getStdout() + bootstrap.getStderr()); + + try (GenericContainer pivot = pivotWithLdap(databaseFile, network)) { + pivot.start(); + String host = pivot.getHost(); + int port = pivot.getMappedPort(8080); + + // Polling with credentials warms up the OLAP context. + String authenticated = PivotContainers.awaitMdxCell(host, port, + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "6", "admin:admin123"); + assertTrue(authenticated.contains("6"), authenticated); + + // analyst from the bootstrap user configs works as well + HttpResponse analyst = PivotContainers.executeMdx(host, port, + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "analyst:analyst123"); + assertEquals(200, analyst.statusCode(), analyst.body()); + + // anonymous is accepted by default + HttpResponse anonymous = PivotContainers.executeMdx(host, port, + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, null); + assertEquals(200, anonymous.statusCode(), anonymous.body()); + + HttpResponse wrongPassword = PivotContainers.executeMdx(host, port, + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "admin:wrong"); + assertEquals(401, wrongPassword.statusCode(), wrongPassword.body()); + + // the role provider read the groups without complaining + assertFalse(pivot.getLogs().contains("could not read the groups"), pivot.getLogs()); + } + + try (GenericContainer pivot = pivotWithLdap(databaseFile, network) + .withEnv("DAANSE_AUTH_ANONYMOUS", "false")) { + pivot.start(); + String host = pivot.getHost(); + int port = pivot.getMappedPort(8080); + + PivotContainers.awaitMdxCell(host, port, PivotContainers.TEST_CATALOG, + PivotContainers.TEST_MDX, "6", "admin:admin123"); + + HttpResponse anonymous = PivotContainers.executeMdx(host, port, + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, null); + assertEquals(401, anonymous.statusCode(), anonymous.body()); + } + } + } + + private GenericContainer pivotWithLdap(Path databaseFile, Network network) { + return new GenericContainer<>(PivotContainers.pivotImage("h2")) + .withNetwork(network) + .withCopyFileToContainer(MountableFile.forClasspathResource("catalog.xmi"), + "/app/catalog/catalog.xmi") + .withCopyFileToContainer(MountableFile.forHostPath(databaseFile), "/app/data/database.mv.db") + .withEnv("DAANSE_LDAP_URL", "ldap://lldap:3890") + .withEnv("DAANSE_LDAP_TRANSPORT_SECURITY", "NONE") + .withEnv("DAANSE_LDAP_ALLOW_UNENCRYPTED", "true") + .withEnv("DAANSE_LDAP_USER_DN_PATTERN", "uid={0},ou=people,dc=example,dc=org") + .withEnv("DAANSE_LDAP_SERVICE_BIND_DN", "uid=admin,ou=people,dc=example,dc=org") + .withEnv("DAANSE_LDAP_SERVICE_BIND_PASSWORD", "admin123") + .withEnv("DAANSE_LDAP_GROUP_SEARCH_BASE", "ou=groups,dc=example,dc=org") + .withExposedPorts(8080) + .waitingFor(Wait.forListeningPort()); + } +} diff --git a/application/pivot/h2/src/test/resources/catalog.xmi b/application/pivot/h2/src/test/resources/catalog.xmi new file mode 100644 index 0000000..65db914 --- /dev/null +++ b/application/pivot/h2/src/test/resources/catalog.xmi @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/application/pivot/h2/start b/application/pivot/h2/start new file mode 100755 index 0000000..9be3319 --- /dev/null +++ b/application/pivot/h2/start @@ -0,0 +1,19 @@ +#!/bin/sh + +JAVA_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/}java" +LOGBACK_CONFIG="./logback.xml" + +# The bundles declare osgi.ee=JavaSE-25; on an older JVM nothing resolves. +JAVA_MAJOR="$("$JAVA_EXEC" -version 2>&1 | awk -F'"' '/version/ { split($2, v, /[._]/); print (v[1] == "1" ? v[2] : v[1]); exit }')" +if [ "${JAVA_MAJOR:-0}" -lt 25 ]; then + echo "Java 25 is required, but $JAVA_EXEC is Java ${JAVA_MAJOR:-unknown}. Point JAVA_HOME at a Java 25 installation." + exit 1 +fi + +if [ ! -f "$LOGBACK_CONFIG" ]; then + echo "Logback-Configuration-File $LOGBACK_CONFIG not found." + exit 1 +fi + +echo "start Server" +"$JAVA_EXEC" -Dlogback.configurationFile=file:"$LOGBACK_CONFIG" -jar daanse.pivot.h2.jar diff --git a/application/pivot/load/.keep b/application/pivot/load/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/application/pivot/log/.keep b/application/pivot/log/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/application/pivot/logback.xml b/application/pivot/logback.xml deleted file mode 100644 index 321cd86..0000000 --- a/application/pivot/logback.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - true - - - - - %-5level %logger{36} - %msg%n - - - - - log/log.log - - log/log.log-%d{yyyy-MM-dd}.%i.log.gz - - 10MB - - 30 - - 100MB - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - \ No newline at end of file diff --git a/application/pivot/mariadb/README.md b/application/pivot/mariadb/README.md new file mode 100644 index 0000000..f9e9d2f --- /dev/null +++ b/application/pivot/mariadb/README.md @@ -0,0 +1,49 @@ +# Daanse Pivot Server — MariaDB + +Production Daanse OLAP (XMLA) server image for **MariaDB**. The image ships the +Daanse ROLAP engine, the XMLA endpoint, the MariaDB JDBC driver, the DataSource +and the matching SQL dialect. It is configured entirely through environment +variables. + +Images: `docker.io/eclipsedaanse/daanse-pivot-mariadb:snapshot` and +`ghcr.io/eclipse-daanse/daanse-pivot-mariadb:snapshot` (linux/amd64, linux/arm64). + +## Quick start + +```bash +docker run --name daanse-pivot \ + -v ./catalog:/app/catalog:ro \ + -e DAANSE_JDBC_HOST=db.example.org \ + -e DAANSE_JDBC_DATABASE_NAME=warehouse \ + -e DAANSE_JDBC_USER=daanse \ + -e DAANSE_JDBC_PASSWORD=secret \ + -p 8080:8080 \ + eclipsedaanse/daanse-pivot-mariadb:snapshot +``` + +The XMLA endpoint is then available at `http://localhost:8080/xmla`. + +## MariaDB environment variables + +Required: `DAANSE_JDBC_DATABASE_NAME`, `DAANSE_JDBC_USER`, `DAANSE_JDBC_PASSWORD`. +Unset variables keep the defaults of the Daanse MariaDB DataSource bundle. + +| Variable | Default | Description | +|---|---|---| +| `DAANSE_JDBC_HOST` | `localhost` | Database host | +| `DAANSE_JDBC_PORT` | `3306` | Database port | +| `DAANSE_JDBC_DATABASE_NAME` | *(required)* | Database name | +| `DAANSE_JDBC_USER` | *(required)* | Database user | +| `DAANSE_JDBC_PASSWORD` | *(required)* | Database password | +| `DAANSE_JDBC_CONNECT_TIMEOUT` | `30000` | Connect timeout (milliseconds) | +| `DAANSE_JDBC_SOCKET_TIMEOUT` | `0` | Socket timeout (milliseconds, 0 = unlimited) | +| `DAANSE_JDBC_SSL_MODE` | `disable` | SSL mode (`disable`, `trust`, `verify-ca`, `verify-full`) | +| `DAANSE_JDBC_USE_SERVER_PREP_STMTS` | `false` | Server side prepared statements | +| `DAANSE_JDBC_ALLOW_LOCAL_INFILE` | `true` | Allow LOAD DATA LOCAL INFILE | +| `DAANSE_JDBC_USE_COMPRESSION` | `false` | Compress the connection | +| `DAANSE_JDBC_ALLOW_MULTI_QUERIES` | `false` | Allow multiple queries per statement | +| `DAANSE_JDBC_SESSION_VARIABLES` | *(unset)* | Session variables set on connect | + +## Source + +`https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot/mariadb` diff --git a/application/pivot/mariadb/container/Dockerfile b/application/pivot/mariadb/container/Dockerfile new file mode 100644 index 0000000..d06940c --- /dev/null +++ b/application/pivot/mariadb/container/Dockerfile @@ -0,0 +1,33 @@ +FROM eclipse-temurin:25-jre-alpine + +# Configurable UID/GID to avoid permission problems with bind mounts (e.g. rootless Podman) +ARG APP_UID=1000 +ARG APP_GID=1000 + +# Create non-root user for security +RUN addgroup -S -g ${APP_GID} appgroup \ + && adduser -S -u ${APP_UID} -G appgroup appuser + +LABEL org.opencontainers.image.source="https://github.com/eclipse-daanse/org.eclipse.daanse.server" \ + org.opencontainers.image.licenses="EPL-2.0" \ + org.opencontainers.image.vendor="Eclipse Daanse" \ + org.opencontainers.image.description="Daanse OLAP (XMLA) server for MariaDB" + +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appgroup application/pivot/mariadb/target/daanse.pivot.mariadb.jar /app/ +COPY --chown=appuser:appgroup application/pivot/mariadb/start /app/ +COPY --chown=appuser:appgroup application/pivot/mariadb/logback.xml /app/ + +RUN mkdir -p /app/catalog /app/data /app/log \ + && chown -R appuser:appgroup /app \ + && chmod +x /app/start + +USER appuser + +VOLUME ["/app/catalog"] + +EXPOSE 8080 + +ENTRYPOINT ["sh","-c","exec /app/start"] diff --git a/application/pivot/mariadb/daanse.pivot.mariadb.bndrun b/application/pivot/mariadb/daanse.pivot.mariadb.bndrun new file mode 100644 index 0000000..f5f3c29 --- /dev/null +++ b/application/pivot/mariadb/daanse.pivot.mariadb.bndrun @@ -0,0 +1,131 @@ +#******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# SmartCity Jena - initial +# Stefan Bischof (bipolis.org) - initial +#******************************************************************************* + +-include: ~${.}/../common/daanse.pivot.base.bndrun + +-runrequires.db: \ + bnd.identity;id='${project.artifactId}',\ + bnd.identity;id='org.eclipse.daanse.jdbc.datasource.mariadb',\ + bnd.identity;id='org.eclipse.daanse.sql.dialect.db.mariadb' + +-runbundles: \ + ai.starlake.transpiler;version='[1.8.0,1.8.1)',\ + ch.qos.logback.classic;version='[1.5.6,1.5.7)',\ + com.fasterxml.woodstox.woodstox-core;version='[7.1.1,7.1.2)',\ + com.github.ben-manes.caffeine;version='[3.1.8,3.1.9)',\ + com.zaxxer.HikariCP;version='[7.1.0,7.1.1)',\ + mariadb-java-client;version='[3.5.5,3.5.6)',\ + net.sf.jsqlparser;version='[5.3.167,5.3.168)',\ + org.apache.aries.spifly.dynamic.bundle;version='[1.3.7,1.3.8)',\ + org.apache.commons.commons-collections4;version='[4.5.0,4.5.1)',\ + org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ + org.apache.felix.http.jetty12;version='[2.0.4,2.0.5)',\ + org.apache.felix.http.servlet-api;version='[6.1.0,6.1.1)',\ + org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ + org.apache.felix.scr;version='[2.2.10,2.2.11)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.businessinformation;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.datatypes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.keysindexes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.behavioral;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.instance;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.resource.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jakarta.servlet.filter.cors;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.mariadb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.hikari;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.cfg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.spi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.util.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational.oledb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.mapping.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.mariadb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.mariadb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.mysql;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.an;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.bi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.cg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.edm;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.empty;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine200;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300_300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.exception;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.ext;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.io;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.mddataset;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.msxmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.multipleresults;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.mining;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.server;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.soap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.xmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.adapter.emf;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.roles;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.store.ldap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.whiteboard.servlet;version='[0.0.1,0.0.2)',\ + org.eclipse.emf.common;version='[2.44.0,2.44.1)',\ + org.eclipse.emf.ecore;version='[2.41.0,2.41.1)',\ + org.eclipse.emf.ecore.xmi;version='[2.39.0,2.39.1)',\ + org.eclipse.fennec.emf.osgi.component;version='[0.1.1,0.1.2)',\ + org.glassfish.hk2.osgi-resource-locator;version='[2.4.0,2.4.1)',\ + org.objectweb.asm;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.commons;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree.analysis;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.util;version='[9.9.0,9.9.1)',\ + org.osgi.service.cm;version='[1.6.1,1.6.2)',\ + org.osgi.service.component;version='[1.5.1,1.5.2)',\ + org.osgi.service.log;version='[1.4.0,1.4.1)',\ + org.osgi.util.converter;version='[1.0.9,1.0.10)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.3.0,1.3.1)',\ + org.osgi.util.pushstream;version='[1.1.0,1.1.1)',\ + stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file diff --git a/application/pivot/mariadb/logback.xml b/application/pivot/mariadb/logback.xml new file mode 100644 index 0000000..f6ebf80 --- /dev/null +++ b/application/pivot/mariadb/logback.xml @@ -0,0 +1,32 @@ + + + + + + + true + + + + + + %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + + diff --git a/application/pivot/mariadb/pom.xml b/application/pivot/mariadb/pom.xml new file mode 100644 index 0000000..b35e824 --- /dev/null +++ b/application/pivot/mariadb/pom.xml @@ -0,0 +1,165 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot + ${revision} + + org.eclipse.daanse.server.application.pivot.mariadb + + + mariadb + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.mariadb + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.mariadb + 0.0.1-SNAPSHOT + runtime + + + + org.osgi + org.osgi.service.jdbc + 1.1.0 + runtime + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + test-jar + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + org.testcontainers + mariadb + ${testcontainers.version} + test + + + + + + + biz.aQute.bnd + bnd-export-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + bnd.executablejar + true + false + + compile + runtime + + + + + + export + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + true + false + + compile + runtime + + + + + biz.aQute.bnd + bnd-run-maven-plugin + ${bnd.version} + + daanse.pivot.${pivot.db}.bndrun + true + + compile + runtime + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + + integration-test + verify + + + + + + + diff --git a/application/pivot/mariadb/src/main/java/org/eclipse/daanse/server/application/pivot/mariadb/MariadbEnvConfigurator.java b/application/pivot/mariadb/src/main/java/org/eclipse/daanse/server/application/pivot/mariadb/MariadbEnvConfigurator.java new file mode 100644 index 0000000..86d31e4 --- /dev/null +++ b/application/pivot/mariadb/src/main/java/org/eclipse/daanse/server/application/pivot/mariadb/MariadbEnvConfigurator.java @@ -0,0 +1,91 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.mariadb; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.List; + +import org.eclipse.daanse.jdbc.datasource.mariadb.api.Constants; +import org.eclipse.daanse.jdbc.datasource.mariadb.api.ocd.DsConfig; +import org.eclipse.daanse.server.application.pivot.common.BasicContextConfigs; +import org.eclipse.daanse.server.application.pivot.common.Env; +import org.eclipse.daanse.server.application.pivot.common.EnvConfigMapper; +import org.eclipse.daanse.server.application.pivot.common.ServerConstants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.annotations.RequireConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.RequireServiceComponentRuntime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Registers the MariaDB DataSource from the {@code DAANSE_JDBC_*} environment + * variables and creates the BasicContext against it. Every attribute of + * {@link DsConfig} is supported. + */ +@Component(immediate = true) +@RequireConfigurationAdmin +@RequireServiceComponentRuntime +public class MariadbEnvConfigurator { + + private static final Logger logger = LoggerFactory.getLogger(MariadbEnvConfigurator.class); + + private static final String DIALECT_NAME = "MARIADB"; + + private static final String ENV_USER = "DAANSE_JDBC_USER"; + private static final String ENV_PASSWORD = "DAANSE_JDBC_PASSWORD"; + private static final String ENV_DATABASE_NAME = "DAANSE_JDBC_DATABASE_NAME"; + + @Reference + ConfigurationAdmin ca; + + private Configuration confDataSource; + private List confPoolAndContext = List.of(); + + @Activate + public void activate() throws IOException { + List missing = Env.missing(ENV_USER, ENV_PASSWORD, ENV_DATABASE_NAME); + if (!missing.isEmpty()) { + logger.error("Not configuring the MariaDB DataSource, required environment variables are not set: {}", + missing); + return; + } + + Dictionary props = EnvConfigMapper.propsFromEnv(DsConfig.class); + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_DATASOURCE); + + confDataSource = ca.getFactoryConfiguration(Constants.PID_DATASOURCE, ServerConstants.CONFIG_IDENT, "?"); + confDataSource.update(props); + + confPoolAndContext = BasicContextConfigs.createEnvPoolAndContext(ca, DIALECT_NAME); + + logger.info("MariaDB DataSource, connection pool and context configured from environment"); + } + + @Deactivate + public void deactivate() throws IOException { + if (confDataSource != null) { + confDataSource.delete(); + } + for (Configuration configuration : confPoolAndContext) { + configuration.delete(); + } + confPoolAndContext = List.of(); + } +} diff --git a/application/pivot/mariadb/src/main/java/org/eclipse/daanse/server/application/pivot/mariadb/package-info.java b/application/pivot/mariadb/src/main/java/org/eclipse/daanse/server/application/pivot/mariadb/package-info.java new file mode 100644 index 0000000..adeaa63 --- /dev/null +++ b/application/pivot/mariadb/src/main/java/org/eclipse/daanse/server/application/pivot/mariadb/package-info.java @@ -0,0 +1,14 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.mariadb; diff --git a/application/pivot/mariadb/src/test/java/org/eclipse/daanse/server/application/pivot/mariadb/MariadbPivotIT.java b/application/pivot/mariadb/src/test/java/org/eclipse/daanse/server/application/pivot/mariadb/MariadbPivotIT.java new file mode 100644 index 0000000..023cd38 --- /dev/null +++ b/application/pivot/mariadb/src/test/java/org/eclipse/daanse/server/application/pivot/mariadb/MariadbPivotIT.java @@ -0,0 +1,74 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.mariadb; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.server.application.pivot.common.test.PivotContainers; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +/** + * End-to-end test: starts a MariaDB database, creates the Fact table of the + * static test catalog, builds the daanse-pivot-mariadb image from its real + * Dockerfile, mounts the catalog and verifies via XMLA that the MDX sum over + * the VALUE column is queryable. + */ +@Testcontainers(disabledWithoutDocker = true) +class MariadbPivotIT { + + static final Network NETWORK = Network.newNetwork(); + + @Container + static final MariaDBContainer DB = new MariaDBContainer<>("mariadb:11.4") + .withNetwork(NETWORK).withNetworkAliases("db"); + + @Test + void sumMeasureIsQueryableViaXmla() throws Exception { + try (Connection connection = DriverManager.getConnection(DB.getJdbcUrl(), DB.getUsername(), DB.getPassword()); + Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE TABLE `Fact` (`KEY` VARCHAR(100), `VALUE` INTEGER)"); + statement.executeUpdate("INSERT INTO `Fact` VALUES ('A', 1), ('B', 2), ('C', 3)"); + } + + try (GenericContainer pivot = new GenericContainer<>(PivotContainers.pivotImage("mariadb")) + .withNetwork(NETWORK) + .withCopyFileToContainer(MountableFile.forClasspathResource("catalog.xmi"), + "/app/catalog/catalog.xmi") + .withEnv("DAANSE_JDBC_HOST", "db") + .withEnv("DAANSE_JDBC_PORT", "3306") + .withEnv("DAANSE_JDBC_DATABASE_NAME", DB.getDatabaseName()) + .withEnv("DAANSE_JDBC_USER", DB.getUsername()) + .withEnv("DAANSE_JDBC_PASSWORD", DB.getPassword()) + .withExposedPorts(8080) + .waitingFor(Wait.forListeningPort())) { + pivot.start(); + + String response = PivotContainers.awaitMdxCell(pivot.getHost(), pivot.getMappedPort(8080), + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "6"); + + assertTrue(response.contains("6"), response); + } + } +} diff --git a/application/pivot/mariadb/src/test/resources/catalog.xmi b/application/pivot/mariadb/src/test/resources/catalog.xmi new file mode 100644 index 0000000..65db914 --- /dev/null +++ b/application/pivot/mariadb/src/test/resources/catalog.xmi @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/application/pivot/mariadb/start b/application/pivot/mariadb/start new file mode 100755 index 0000000..0023e73 --- /dev/null +++ b/application/pivot/mariadb/start @@ -0,0 +1,19 @@ +#!/bin/sh + +JAVA_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/}java" +LOGBACK_CONFIG="./logback.xml" + +# The bundles declare osgi.ee=JavaSE-25; on an older JVM nothing resolves. +JAVA_MAJOR="$("$JAVA_EXEC" -version 2>&1 | awk -F'"' '/version/ { split($2, v, /[._]/); print (v[1] == "1" ? v[2] : v[1]); exit }')" +if [ "${JAVA_MAJOR:-0}" -lt 25 ]; then + echo "Java 25 is required, but $JAVA_EXEC is Java ${JAVA_MAJOR:-unknown}. Point JAVA_HOME at a Java 25 installation." + exit 1 +fi + +if [ ! -f "$LOGBACK_CONFIG" ]; then + echo "Logback-Configuration-File $LOGBACK_CONFIG not found." + exit 1 +fi + +echo "start Server" +"$JAVA_EXEC" -Dlogback.configurationFile=file:"$LOGBACK_CONFIG" -jar daanse.pivot.mariadb.jar diff --git a/application/pivot/mssql/README.md b/application/pivot/mssql/README.md new file mode 100644 index 0000000..8cbde62 --- /dev/null +++ b/application/pivot/mssql/README.md @@ -0,0 +1,59 @@ +# Daanse Pivot Server — MS SQL Server + +Production Daanse OLAP (XMLA) server image for **Microsoft SQL Server**. The +image ships the Daanse ROLAP engine, the XMLA endpoint, the MS SQL Server JDBC +driver, the DataSource and the matching SQL dialect. It is configured entirely +through environment variables. + +Images: `docker.io/eclipsedaanse/daanse-pivot-mssql:snapshot` and +`ghcr.io/eclipse-daanse/daanse-pivot-mssql:snapshot` (linux/amd64, linux/arm64). + +## Quick start + +```bash +docker run --name daanse-pivot \ + -v ./catalog:/app/catalog:ro \ + -e DAANSE_JDBC_SERVER_NAME=db.example.org \ + -e DAANSE_JDBC_DATABASE_NAME=warehouse \ + -e DAANSE_JDBC_USER=daanse \ + -e DAANSE_JDBC_PASSWORD=secret \ + -p 8080:8080 \ + eclipsedaanse/daanse-pivot-mssql:snapshot +``` + +The XMLA endpoint is then available at `http://localhost:8080/xmla`. + +## MS SQL Server environment variables + +Required: `DAANSE_JDBC_DATABASE_NAME`, `DAANSE_JDBC_USER`, `DAANSE_JDBC_PASSWORD`. +Unset variables keep the defaults of the Daanse MS SQL Server DataSource bundle. + +| Variable | Default | Description | +|---|---|---| +| `DAANSE_JDBC_SERVER_NAME` | `localhost` | Database host | +| `DAANSE_JDBC_PORT_NUMBER` | `1433` | Database port | +| `DAANSE_JDBC_INSTANCE_NAME` | *(unset)* | Named instance | +| `DAANSE_JDBC_DATABASE_NAME` | *(required)* | Database name | +| `DAANSE_JDBC_USER` | *(required)* | Database user | +| `DAANSE_JDBC_PASSWORD` | *(required)* | Database password | +| `DAANSE_JDBC_ENCRYPT` | `true` | Encrypt the connection (`true`, `false`, `strict`) | +| `DAANSE_JDBC_TRUST_SERVER_CERTIFICATE` | `false` | Trust the server certificate without validation | +| `DAANSE_JDBC_INTEGRATED_SECURITY` | `false` | Use integrated security | +| `DAANSE_JDBC_AUTHENTICATION` | `NotSpecified` | Authentication method | +| `DAANSE_JDBC_APPLICATION_NAME` | *(unset)* | Application name reported to the server | +| `DAANSE_JDBC_APPLICATION_INTENT` | `readwrite` | `readwrite` or `readonly` | +| `DAANSE_JDBC_LOGIN_TIMEOUT` | `30` | Login timeout (seconds) | +| `DAANSE_JDBC_QUERY_TIMEOUT` | `-1` | Query timeout (seconds, -1 = unlimited) | +| `DAANSE_JDBC_SOCKET_TIMEOUT` | `0` | Socket timeout (milliseconds, 0 = unlimited) | +| `DAANSE_JDBC_LOCK_TIMEOUT` | `-1` | Lock timeout (milliseconds, -1 = server default) | +| `DAANSE_JDBC_RESPONSE_BUFFERING` | `adaptive` | `adaptive` or `full` | +| `DAANSE_JDBC_SELECT_METHOD` | `direct` | `direct` or `cursor` | +| `DAANSE_JDBC_MULTI_SUBNET_FAILOVER` | `false` | Multi-subnet failover | +| `DAANSE_JDBC_FAILOVER_PARTNER` | *(unset)* | Failover partner server | +| `DAANSE_JDBC_PACKET_SIZE` | `4096` | Network packet size | +| `DAANSE_JDBC_COLUMN_ENCRYPTION_SETTING` | `Disabled` | Always Encrypted setting | +| `DAANSE_JDBC_SEND_STRING_PARAMETERS_AS_UNICODE` | `true` | Send strings as Unicode | + +## Source + +`https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot/mssql` diff --git a/application/pivot/mssql/container/Dockerfile b/application/pivot/mssql/container/Dockerfile new file mode 100644 index 0000000..2390baf --- /dev/null +++ b/application/pivot/mssql/container/Dockerfile @@ -0,0 +1,33 @@ +FROM eclipse-temurin:25-jre-alpine + +# Configurable UID/GID to avoid permission problems with bind mounts (e.g. rootless Podman) +ARG APP_UID=1000 +ARG APP_GID=1000 + +# Create non-root user for security +RUN addgroup -S -g ${APP_GID} appgroup \ + && adduser -S -u ${APP_UID} -G appgroup appuser + +LABEL org.opencontainers.image.source="https://github.com/eclipse-daanse/org.eclipse.daanse.server" \ + org.opencontainers.image.licenses="EPL-2.0" \ + org.opencontainers.image.vendor="Eclipse Daanse" \ + org.opencontainers.image.description="Daanse OLAP (XMLA) server for Microsoft SQL Server" + +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appgroup application/pivot/mssql/target/daanse.pivot.mssql.jar /app/ +COPY --chown=appuser:appgroup application/pivot/mssql/start /app/ +COPY --chown=appuser:appgroup application/pivot/mssql/logback.xml /app/ + +RUN mkdir -p /app/catalog /app/data /app/log \ + && chown -R appuser:appgroup /app \ + && chmod +x /app/start + +USER appuser + +VOLUME ["/app/catalog"] + +EXPOSE 8080 + +ENTRYPOINT ["sh","-c","exec /app/start"] diff --git a/application/pivot/mssql/daanse.pivot.mssql.bndrun b/application/pivot/mssql/daanse.pivot.mssql.bndrun new file mode 100644 index 0000000..5581d9a --- /dev/null +++ b/application/pivot/mssql/daanse.pivot.mssql.bndrun @@ -0,0 +1,131 @@ +#******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# SmartCity Jena - initial +# Stefan Bischof (bipolis.org) - initial +#******************************************************************************* + +-include: ~${.}/../common/daanse.pivot.base.bndrun + +-runrequires.db: \ + bnd.identity;id='${project.artifactId}',\ + bnd.identity;id='org.eclipse.daanse.jdbc.datasource.mssqlserver',\ + bnd.identity;id='org.eclipse.daanse.sql.dialect.db.mssqlserver' + +-runbundles: \ + ai.starlake.transpiler;version='[1.8.0,1.8.1)',\ + ch.qos.logback.classic;version='[1.5.6,1.5.7)',\ + com.fasterxml.woodstox.woodstox-core;version='[7.1.1,7.1.2)',\ + com.github.ben-manes.caffeine;version='[3.1.8,3.1.9)',\ + com.microsoft.sqlserver.mssql-jdbc;version='[12.8.2,12.8.3)',\ + com.zaxxer.HikariCP;version='[7.1.0,7.1.1)',\ + net.sf.jsqlparser;version='[5.3.167,5.3.168)',\ + org.apache.aries.spifly.dynamic.bundle;version='[1.3.7,1.3.8)',\ + org.apache.commons.commons-collections4;version='[4.5.0,4.5.1)',\ + org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ + org.apache.felix.http.jetty12;version='[2.0.4,2.0.5)',\ + org.apache.felix.http.servlet-api;version='[6.1.0,6.1.1)',\ + org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ + org.apache.felix.scr;version='[2.2.10,2.2.11)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.businessinformation;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.datatypes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.keysindexes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.behavioral;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.instance;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.resource.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jakarta.servlet.filter.cors;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.mssqlserver;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.hikari;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.cfg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.spi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.util.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational.oledb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.mapping.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.mssql;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.mssqlserver;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.an;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.bi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.cg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.edm;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.empty;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine200;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300_300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.exception;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.ext;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.io;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.mddataset;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.msxmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.multipleresults;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.mining;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.server;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.soap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.xmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.adapter.emf;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.roles;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.store.ldap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.whiteboard.servlet;version='[0.0.1,0.0.2)',\ + org.eclipse.emf.common;version='[2.44.0,2.44.1)',\ + org.eclipse.emf.ecore;version='[2.41.0,2.41.1)',\ + org.eclipse.emf.ecore.xmi;version='[2.39.0,2.39.1)',\ + org.eclipse.fennec.emf.osgi.component;version='[0.1.1,0.1.2)',\ + org.glassfish.hk2.osgi-resource-locator;version='[2.4.0,2.4.1)',\ + org.objectweb.asm;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.commons;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree.analysis;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.util;version='[9.9.0,9.9.1)',\ + org.osgi.service.cm;version='[1.6.1,1.6.2)',\ + org.osgi.service.component;version='[1.5.1,1.5.2)',\ + org.osgi.service.jdbc;version='[1.1.0,1.1.1)',\ + org.osgi.service.log;version='[1.4.0,1.4.1)',\ + org.osgi.util.converter;version='[1.0.9,1.0.10)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.3.0,1.3.1)',\ + org.osgi.util.pushstream;version='[1.1.0,1.1.1)',\ + stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file diff --git a/application/pivot/mssql/logback.xml b/application/pivot/mssql/logback.xml new file mode 100644 index 0000000..f6ebf80 --- /dev/null +++ b/application/pivot/mssql/logback.xml @@ -0,0 +1,32 @@ + + + + + + + true + + + + + + %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + + diff --git a/application/pivot/mssql/pom.xml b/application/pivot/mssql/pom.xml new file mode 100644 index 0000000..8a4b118 --- /dev/null +++ b/application/pivot/mssql/pom.xml @@ -0,0 +1,165 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot + ${revision} + + org.eclipse.daanse.server.application.pivot.mssql + + + mssql + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.mssqlserver + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.mssqlserver + 0.0.1-SNAPSHOT + runtime + + + + org.osgi + org.osgi.service.jdbc + 1.1.0 + runtime + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + test-jar + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + org.testcontainers + mssqlserver + ${testcontainers.version} + test + + + + + + + biz.aQute.bnd + bnd-export-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + bnd.executablejar + true + false + + compile + runtime + + + + + + export + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + true + false + + compile + runtime + + + + + biz.aQute.bnd + bnd-run-maven-plugin + ${bnd.version} + + daanse.pivot.${pivot.db}.bndrun + true + + compile + runtime + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + + integration-test + verify + + + + + + + diff --git a/application/pivot/mssql/src/main/java/org/eclipse/daanse/server/application/pivot/mssql/MssqlEnvConfigurator.java b/application/pivot/mssql/src/main/java/org/eclipse/daanse/server/application/pivot/mssql/MssqlEnvConfigurator.java new file mode 100644 index 0000000..e6fd174 --- /dev/null +++ b/application/pivot/mssql/src/main/java/org/eclipse/daanse/server/application/pivot/mssql/MssqlEnvConfigurator.java @@ -0,0 +1,92 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.mssql; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.List; + +import org.eclipse.daanse.jdbc.datasource.mssqlserver.api.Constants; +import org.eclipse.daanse.jdbc.datasource.mssqlserver.api.ocd.DsConfig; +import org.eclipse.daanse.server.application.pivot.common.BasicContextConfigs; +import org.eclipse.daanse.server.application.pivot.common.Env; +import org.eclipse.daanse.server.application.pivot.common.EnvConfigMapper; +import org.eclipse.daanse.server.application.pivot.common.ServerConstants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.annotations.RequireConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.RequireServiceComponentRuntime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Registers the MS SQL Server DataSource from the {@code DAANSE_JDBC_*} + * environment variables and creates the BasicContext against it. Every + * attribute of {@link DsConfig} is supported. + */ +@Component(immediate = true) +@RequireConfigurationAdmin +@RequireServiceComponentRuntime +public class MssqlEnvConfigurator { + + private static final Logger logger = LoggerFactory.getLogger(MssqlEnvConfigurator.class); + + private static final String DIALECT_NAME = "MSSQL"; + + private static final String ENV_USER = "DAANSE_JDBC_USER"; + private static final String ENV_PASSWORD = "DAANSE_JDBC_PASSWORD"; + private static final String ENV_DATABASE_NAME = "DAANSE_JDBC_DATABASE_NAME"; + + @Reference + ConfigurationAdmin ca; + + private Configuration confDataSource; + private List confPoolAndContext = List.of(); + + @Activate + public void activate() throws IOException { + List missing = Env.missing(ENV_USER, ENV_PASSWORD, ENV_DATABASE_NAME); + if (!missing.isEmpty()) { + logger.error( + "Not configuring the MS SQL Server DataSource, required environment variables are not set: {}", + missing); + return; + } + + Dictionary props = EnvConfigMapper.propsFromEnv(DsConfig.class); + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_DATASOURCE); + + confDataSource = ca.getFactoryConfiguration(Constants.PID_DATASOURCE, ServerConstants.CONFIG_IDENT, "?"); + confDataSource.update(props); + + confPoolAndContext = BasicContextConfigs.createEnvPoolAndContext(ca, DIALECT_NAME); + + logger.info("MS SQL Server DataSource, connection pool and context configured from environment"); + } + + @Deactivate + public void deactivate() throws IOException { + if (confDataSource != null) { + confDataSource.delete(); + } + for (Configuration configuration : confPoolAndContext) { + configuration.delete(); + } + confPoolAndContext = List.of(); + } +} diff --git a/application/pivot/mssql/src/main/java/org/eclipse/daanse/server/application/pivot/mssql/package-info.java b/application/pivot/mssql/src/main/java/org/eclipse/daanse/server/application/pivot/mssql/package-info.java new file mode 100644 index 0000000..56da395 --- /dev/null +++ b/application/pivot/mssql/src/main/java/org/eclipse/daanse/server/application/pivot/mssql/package-info.java @@ -0,0 +1,14 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.mssql; diff --git a/application/pivot/mssql/src/test/java/org/eclipse/daanse/server/application/pivot/mssql/MssqlPivotIT.java b/application/pivot/mssql/src/test/java/org/eclipse/daanse/server/application/pivot/mssql/MssqlPivotIT.java new file mode 100644 index 0000000..d658ad0 --- /dev/null +++ b/application/pivot/mssql/src/test/java/org/eclipse/daanse/server/application/pivot/mssql/MssqlPivotIT.java @@ -0,0 +1,76 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.mssql; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.server.application.pivot.common.test.PivotContainers; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +/** + * End-to-end test: starts a MS SQL Server database, creates the Fact table of + * the static test catalog, builds the daanse-pivot-mssql image from its real + * Dockerfile, mounts the catalog and verifies via XMLA that the MDX sum over + * the VALUE column is queryable. + */ +@Testcontainers(disabledWithoutDocker = true) +class MssqlPivotIT { + + static final Network NETWORK = Network.newNetwork(); + + @Container + static final MSSQLServerContainer DB = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-latest").acceptLicense() + .withNetwork(NETWORK).withNetworkAliases("db"); + + @Test + void sumMeasureIsQueryableViaXmla() throws Exception { + try (Connection connection = DriverManager.getConnection(DB.getJdbcUrl(), DB.getUsername(), DB.getPassword()); + Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE TABLE \"Fact\" (\"KEY\" VARCHAR(100), \"VALUE\" INTEGER)"); + statement.executeUpdate("INSERT INTO \"Fact\" VALUES ('A', 1), ('B', 2), ('C', 3)"); + } + + try (GenericContainer pivot = new GenericContainer<>(PivotContainers.pivotImage("mssql")) + .withNetwork(NETWORK) + .withCopyFileToContainer(MountableFile.forClasspathResource("catalog.xmi"), + "/app/catalog/catalog.xmi") + .withEnv("DAANSE_JDBC_SERVER_NAME", "db") + .withEnv("DAANSE_JDBC_PORT_NUMBER", "1433") + .withEnv("DAANSE_JDBC_DATABASE_NAME", "master") + .withEnv("DAANSE_JDBC_USER", DB.getUsername()) + .withEnv("DAANSE_JDBC_PASSWORD", DB.getPassword()) + .withEnv("DAANSE_JDBC_ENCRYPT", "false") + .withExposedPorts(8080) + .waitingFor(Wait.forListeningPort())) { + pivot.start(); + + String response = PivotContainers.awaitMdxCell(pivot.getHost(), pivot.getMappedPort(8080), + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "6"); + + assertTrue(response.contains("6"), response); + } + } +} diff --git a/application/pivot/mssql/src/test/resources/catalog.xmi b/application/pivot/mssql/src/test/resources/catalog.xmi new file mode 100644 index 0000000..65db914 --- /dev/null +++ b/application/pivot/mssql/src/test/resources/catalog.xmi @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/application/pivot/mssql/start b/application/pivot/mssql/start new file mode 100755 index 0000000..515f4bc --- /dev/null +++ b/application/pivot/mssql/start @@ -0,0 +1,19 @@ +#!/bin/sh + +JAVA_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/}java" +LOGBACK_CONFIG="./logback.xml" + +# The bundles declare osgi.ee=JavaSE-25; on an older JVM nothing resolves. +JAVA_MAJOR="$("$JAVA_EXEC" -version 2>&1 | awk -F'"' '/version/ { split($2, v, /[._]/); print (v[1] == "1" ? v[2] : v[1]); exit }')" +if [ "${JAVA_MAJOR:-0}" -lt 25 ]; then + echo "Java 25 is required, but $JAVA_EXEC is Java ${JAVA_MAJOR:-unknown}. Point JAVA_HOME at a Java 25 installation." + exit 1 +fi + +if [ ! -f "$LOGBACK_CONFIG" ]; then + echo "Logback-Configuration-File $LOGBACK_CONFIG not found." + exit 1 +fi + +echo "start Server" +"$JAVA_EXEC" -Dlogback.configurationFile=file:"$LOGBACK_CONFIG" -jar daanse.pivot.mssql.jar diff --git a/application/pivot/mysql/README.md b/application/pivot/mysql/README.md new file mode 100644 index 0000000..b9351b9 --- /dev/null +++ b/application/pivot/mysql/README.md @@ -0,0 +1,57 @@ +# Daanse Pivot Server — MySQL + +Production Daanse OLAP (XMLA) server image for **MySQL**. The image ships the +Daanse ROLAP engine, the XMLA endpoint, the MySQL JDBC driver, the DataSource +and the matching SQL dialect. It is configured entirely through environment +variables. + +Images: `docker.io/eclipsedaanse/daanse-pivot-mysql:snapshot` and +`ghcr.io/eclipse-daanse/daanse-pivot-mysql:snapshot` (linux/amd64, linux/arm64). + +## Quick start + +```bash +docker run --name daanse-pivot \ + -v ./catalog:/app/catalog:ro \ + -e DAANSE_JDBC_HOST=db.example.org \ + -e DAANSE_JDBC_DATABASE_NAME=warehouse \ + -e DAANSE_JDBC_USER=daanse \ + -e DAANSE_JDBC_PASSWORD=secret \ + -p 8080:8080 \ + eclipsedaanse/daanse-pivot-mysql:snapshot +``` + +The XMLA endpoint is then available at `http://localhost:8080/xmla`. + +## MySQL environment variables + +Required: `DAANSE_JDBC_DATABASE_NAME`, `DAANSE_JDBC_USER`, `DAANSE_JDBC_PASSWORD`. +Unset variables keep the defaults of the Daanse MySQL DataSource bundle. + +| Variable | Default | Description | +|---|---|---| +| `DAANSE_JDBC_HOST` | `localhost` | Database host | +| `DAANSE_JDBC_PORT` | `3306` | Database port | +| `DAANSE_JDBC_DATABASE_NAME` | *(required)* | Database name | +| `DAANSE_JDBC_USER` | *(required)* | Database user | +| `DAANSE_JDBC_PASSWORD` | *(required)* | Database password | +| `DAANSE_JDBC_AUTO_RECONNECT` | `false` | Auto reconnect | +| `DAANSE_JDBC_CONNECT_TIMEOUT` | `0` | Connect timeout (milliseconds, 0 = unlimited) | +| `DAANSE_JDBC_SOCKET_TIMEOUT` | `0` | Socket timeout (milliseconds, 0 = unlimited) | +| `DAANSE_JDBC_USE_SSL` | `false` | Use SSL | +| `DAANSE_JDBC_CHARACTER_ENCODING` | `UTF-8` | Character encoding | +| `DAANSE_JDBC_USE_SERVER_PREP_STMTS` | `false` | Server side prepared statements | +| `DAANSE_JDBC_CACHE_PREP_STMTS` | `false` | Cache prepared statements | +| `DAANSE_JDBC_PREP_STMT_CACHE_SIZE` | `25` | Prepared statement cache size | +| `DAANSE_JDBC_USE_COMPRESSION` | `false` | Compress the connection | +| `DAANSE_JDBC_ZERO_DATE_TIME_BEHAVIOR` | `exception` | Behavior for zero datetime values | +| `DAANSE_JDBC_SERVER_TIMEZONE` | *(unset)* | Server timezone override | +| `DAANSE_JDBC_USE_AFFECTED_ROWS` | `false` | Report affected instead of found rows | +| `DAANSE_JDBC_ALLOW_MULTI_QUERIES` | `false` | Allow multiple queries per statement | +| `DAANSE_JDBC_REWRITE_BATCHED_STATEMENTS` | `false` | Rewrite batched statements | +| `DAANSE_JDBC_USE_INFORMATION_SCHEMA` | `true` | Use INFORMATION_SCHEMA for metadata | +| `DAANSE_JDBC_LOGGER` | *(unset)* | Driver logger class | + +## Source + +`https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot/mysql` diff --git a/application/pivot/mysql/container/Dockerfile b/application/pivot/mysql/container/Dockerfile new file mode 100644 index 0000000..49624db --- /dev/null +++ b/application/pivot/mysql/container/Dockerfile @@ -0,0 +1,33 @@ +FROM eclipse-temurin:25-jre-alpine + +# Configurable UID/GID to avoid permission problems with bind mounts (e.g. rootless Podman) +ARG APP_UID=1000 +ARG APP_GID=1000 + +# Create non-root user for security +RUN addgroup -S -g ${APP_GID} appgroup \ + && adduser -S -u ${APP_UID} -G appgroup appuser + +LABEL org.opencontainers.image.source="https://github.com/eclipse-daanse/org.eclipse.daanse.server" \ + org.opencontainers.image.licenses="EPL-2.0" \ + org.opencontainers.image.vendor="Eclipse Daanse" \ + org.opencontainers.image.description="Daanse OLAP (XMLA) server for MySQL" + +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appgroup application/pivot/mysql/target/daanse.pivot.mysql.jar /app/ +COPY --chown=appuser:appgroup application/pivot/mysql/start /app/ +COPY --chown=appuser:appgroup application/pivot/mysql/logback.xml /app/ + +RUN mkdir -p /app/catalog /app/data /app/log \ + && chown -R appuser:appgroup /app \ + && chmod +x /app/start + +USER appuser + +VOLUME ["/app/catalog"] + +EXPOSE 8080 + +ENTRYPOINT ["sh","-c","exec /app/start"] diff --git a/application/pivot/mysql/daanse.pivot.mysql.bndrun b/application/pivot/mysql/daanse.pivot.mysql.bndrun new file mode 100644 index 0000000..8ad1eaa --- /dev/null +++ b/application/pivot/mysql/daanse.pivot.mysql.bndrun @@ -0,0 +1,130 @@ +#******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# SmartCity Jena - initial +# Stefan Bischof (bipolis.org) - initial +#******************************************************************************* + +-include: ~${.}/../common/daanse.pivot.base.bndrun + +-runrequires.db: \ + bnd.identity;id='${project.artifactId}',\ + bnd.identity;id='org.eclipse.daanse.jdbc.datasource.mysql',\ + bnd.identity;id='org.eclipse.daanse.sql.dialect.db.mysql' + +-runbundles: \ + ai.starlake.transpiler;version='[1.8.0,1.8.1)',\ + ch.qos.logback.classic;version='[1.5.6,1.5.7)',\ + com.fasterxml.woodstox.woodstox-core;version='[7.1.1,7.1.2)',\ + com.github.ben-manes.caffeine;version='[3.1.8,3.1.9)',\ + com.mysql.cj;version='[9.0.0,9.0.1)',\ + com.zaxxer.HikariCP;version='[7.1.0,7.1.1)',\ + net.sf.jsqlparser;version='[5.3.167,5.3.168)',\ + org.apache.aries.spifly.dynamic.bundle;version='[1.3.7,1.3.8)',\ + org.apache.commons.commons-collections4;version='[4.5.0,4.5.1)',\ + org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ + org.apache.felix.http.jetty12;version='[2.0.4,2.0.5)',\ + org.apache.felix.http.servlet-api;version='[6.1.0,6.1.1)',\ + org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ + org.apache.felix.scr;version='[2.2.10,2.2.11)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.businessinformation;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.datatypes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.keysindexes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.behavioral;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.instance;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.resource.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jakarta.servlet.filter.cors;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.mysql;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.hikari;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.cfg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.spi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.util.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational.oledb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.mapping.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.mysql;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.mysql;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.an;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.bi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.cg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.edm;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.empty;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine200;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300_300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.exception;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.ext;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.io;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.mddataset;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.msxmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.multipleresults;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.mining;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.server;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.soap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.xmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.adapter.emf;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.roles;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.store.ldap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.whiteboard.servlet;version='[0.0.1,0.0.2)',\ + org.eclipse.emf.common;version='[2.44.0,2.44.1)',\ + org.eclipse.emf.ecore;version='[2.41.0,2.41.1)',\ + org.eclipse.emf.ecore.xmi;version='[2.39.0,2.39.1)',\ + org.eclipse.fennec.emf.osgi.component;version='[0.1.1,0.1.2)',\ + org.glassfish.hk2.osgi-resource-locator;version='[2.4.0,2.4.1)',\ + org.objectweb.asm;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.commons;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree.analysis;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.util;version='[9.9.0,9.9.1)',\ + org.osgi.service.cm;version='[1.6.1,1.6.2)',\ + org.osgi.service.component;version='[1.5.1,1.5.2)',\ + org.osgi.service.log;version='[1.4.0,1.4.1)',\ + org.osgi.util.converter;version='[1.0.9,1.0.10)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.3.0,1.3.1)',\ + org.osgi.util.pushstream;version='[1.1.0,1.1.1)',\ + stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file diff --git a/application/pivot/mysql/logback.xml b/application/pivot/mysql/logback.xml new file mode 100644 index 0000000..f6ebf80 --- /dev/null +++ b/application/pivot/mysql/logback.xml @@ -0,0 +1,32 @@ + + + + + + + true + + + + + + %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + + diff --git a/application/pivot/mysql/pom.xml b/application/pivot/mysql/pom.xml new file mode 100644 index 0000000..004de45 --- /dev/null +++ b/application/pivot/mysql/pom.xml @@ -0,0 +1,165 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot + ${revision} + + org.eclipse.daanse.server.application.pivot.mysql + + + mysql + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.mysql + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.mysql + 0.0.1-SNAPSHOT + runtime + + + + org.osgi + org.osgi.service.jdbc + 1.1.0 + runtime + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + test-jar + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + org.testcontainers + mysql + ${testcontainers.version} + test + + + + + + + biz.aQute.bnd + bnd-export-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + bnd.executablejar + true + false + + compile + runtime + + + + + + export + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + true + false + + compile + runtime + + + + + biz.aQute.bnd + bnd-run-maven-plugin + ${bnd.version} + + daanse.pivot.${pivot.db}.bndrun + true + + compile + runtime + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + + integration-test + verify + + + + + + + diff --git a/application/pivot/mysql/src/main/java/org/eclipse/daanse/server/application/pivot/mysql/MysqlEnvConfigurator.java b/application/pivot/mysql/src/main/java/org/eclipse/daanse/server/application/pivot/mysql/MysqlEnvConfigurator.java new file mode 100644 index 0000000..acfc4ef --- /dev/null +++ b/application/pivot/mysql/src/main/java/org/eclipse/daanse/server/application/pivot/mysql/MysqlEnvConfigurator.java @@ -0,0 +1,91 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.mysql; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.List; + +import org.eclipse.daanse.jdbc.datasource.mysql.api.Constants; +import org.eclipse.daanse.jdbc.datasource.mysql.api.ocd.DsConfig; +import org.eclipse.daanse.server.application.pivot.common.BasicContextConfigs; +import org.eclipse.daanse.server.application.pivot.common.Env; +import org.eclipse.daanse.server.application.pivot.common.EnvConfigMapper; +import org.eclipse.daanse.server.application.pivot.common.ServerConstants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.annotations.RequireConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.RequireServiceComponentRuntime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Registers the MySQL DataSource from the {@code DAANSE_JDBC_*} environment + * variables and creates the BasicContext against it. Every attribute of + * {@link DsConfig} is supported. + */ +@Component(immediate = true) +@RequireConfigurationAdmin +@RequireServiceComponentRuntime +public class MysqlEnvConfigurator { + + private static final Logger logger = LoggerFactory.getLogger(MysqlEnvConfigurator.class); + + private static final String DIALECT_NAME = "MYSQL"; + + private static final String ENV_USER = "DAANSE_JDBC_USER"; + private static final String ENV_PASSWORD = "DAANSE_JDBC_PASSWORD"; + private static final String ENV_DATABASE_NAME = "DAANSE_JDBC_DATABASE_NAME"; + + @Reference + ConfigurationAdmin ca; + + private Configuration confDataSource; + private List confPoolAndContext = List.of(); + + @Activate + public void activate() throws IOException { + List missing = Env.missing(ENV_USER, ENV_PASSWORD, ENV_DATABASE_NAME); + if (!missing.isEmpty()) { + logger.error("Not configuring the MySQL DataSource, required environment variables are not set: {}", + missing); + return; + } + + Dictionary props = EnvConfigMapper.propsFromEnv(DsConfig.class); + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_DATASOURCE); + + confDataSource = ca.getFactoryConfiguration(Constants.PID_DATASOURCE, ServerConstants.CONFIG_IDENT, "?"); + confDataSource.update(props); + + confPoolAndContext = BasicContextConfigs.createEnvPoolAndContext(ca, DIALECT_NAME); + + logger.info("MySQL DataSource, connection pool and context configured from environment"); + } + + @Deactivate + public void deactivate() throws IOException { + if (confDataSource != null) { + confDataSource.delete(); + } + for (Configuration configuration : confPoolAndContext) { + configuration.delete(); + } + confPoolAndContext = List.of(); + } +} diff --git a/application/pivot/mysql/src/main/java/org/eclipse/daanse/server/application/pivot/mysql/package-info.java b/application/pivot/mysql/src/main/java/org/eclipse/daanse/server/application/pivot/mysql/package-info.java new file mode 100644 index 0000000..ca12d50 --- /dev/null +++ b/application/pivot/mysql/src/main/java/org/eclipse/daanse/server/application/pivot/mysql/package-info.java @@ -0,0 +1,14 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.mysql; diff --git a/application/pivot/mysql/src/test/java/org/eclipse/daanse/server/application/pivot/mysql/MysqlPivotIT.java b/application/pivot/mysql/src/test/java/org/eclipse/daanse/server/application/pivot/mysql/MysqlPivotIT.java new file mode 100644 index 0000000..de53199 --- /dev/null +++ b/application/pivot/mysql/src/test/java/org/eclipse/daanse/server/application/pivot/mysql/MysqlPivotIT.java @@ -0,0 +1,74 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.mysql; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.server.application.pivot.common.test.PivotContainers; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +/** + * End-to-end test: starts a MySQL database, creates the Fact table of the + * static test catalog, builds the daanse-pivot-mysql image from its real + * Dockerfile, mounts the catalog and verifies via XMLA that the MDX sum over + * the VALUE column is queryable. + */ +@Testcontainers(disabledWithoutDocker = true) +class MysqlPivotIT { + + static final Network NETWORK = Network.newNetwork(); + + @Container + static final MySQLContainer DB = new MySQLContainer<>("mysql:8.4") + .withNetwork(NETWORK).withNetworkAliases("db"); + + @Test + void sumMeasureIsQueryableViaXmla() throws Exception { + try (Connection connection = DriverManager.getConnection(DB.getJdbcUrl(), DB.getUsername(), DB.getPassword()); + Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE TABLE `Fact` (`KEY` VARCHAR(100), `VALUE` INTEGER)"); + statement.executeUpdate("INSERT INTO `Fact` VALUES ('A', 1), ('B', 2), ('C', 3)"); + } + + try (GenericContainer pivot = new GenericContainer<>(PivotContainers.pivotImage("mysql")) + .withNetwork(NETWORK) + .withCopyFileToContainer(MountableFile.forClasspathResource("catalog.xmi"), + "/app/catalog/catalog.xmi") + .withEnv("DAANSE_JDBC_HOST", "db") + .withEnv("DAANSE_JDBC_PORT", "3306") + .withEnv("DAANSE_JDBC_DATABASE_NAME", DB.getDatabaseName()) + .withEnv("DAANSE_JDBC_USER", DB.getUsername()) + .withEnv("DAANSE_JDBC_PASSWORD", DB.getPassword()) + .withExposedPorts(8080) + .waitingFor(Wait.forListeningPort())) { + pivot.start(); + + String response = PivotContainers.awaitMdxCell(pivot.getHost(), pivot.getMappedPort(8080), + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "6"); + + assertTrue(response.contains("6"), response); + } + } +} diff --git a/application/pivot/mysql/src/test/resources/catalog.xmi b/application/pivot/mysql/src/test/resources/catalog.xmi new file mode 100644 index 0000000..65db914 --- /dev/null +++ b/application/pivot/mysql/src/test/resources/catalog.xmi @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/application/pivot/mysql/start b/application/pivot/mysql/start new file mode 100755 index 0000000..a7f304d --- /dev/null +++ b/application/pivot/mysql/start @@ -0,0 +1,19 @@ +#!/bin/sh + +JAVA_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/}java" +LOGBACK_CONFIG="./logback.xml" + +# The bundles declare osgi.ee=JavaSE-25; on an older JVM nothing resolves. +JAVA_MAJOR="$("$JAVA_EXEC" -version 2>&1 | awk -F'"' '/version/ { split($2, v, /[._]/); print (v[1] == "1" ? v[2] : v[1]); exit }')" +if [ "${JAVA_MAJOR:-0}" -lt 25 ]; then + echo "Java 25 is required, but $JAVA_EXEC is Java ${JAVA_MAJOR:-unknown}. Point JAVA_HOME at a Java 25 installation." + exit 1 +fi + +if [ ! -f "$LOGBACK_CONFIG" ]; then + echo "Logback-Configuration-File $LOGBACK_CONFIG not found." + exit 1 +fi + +echo "start Server" +"$JAVA_EXEC" -Dlogback.configurationFile=file:"$LOGBACK_CONFIG" -jar daanse.pivot.mysql.jar diff --git a/application/pivot/oracle/README.md b/application/pivot/oracle/README.md new file mode 100644 index 0000000..87ba783 --- /dev/null +++ b/application/pivot/oracle/README.md @@ -0,0 +1,44 @@ +# Daanse Pivot Server — Oracle + +Production Daanse OLAP (XMLA) server image for **Oracle Database**. The image +ships the Daanse ROLAP engine, the XMLA endpoint, the Oracle JDBC driver +(embedded in the DataSource bundle), the DataSource and the matching SQL +dialect. It is configured entirely through environment variables. + +Images: `docker.io/eclipsedaanse/daanse-pivot-oracle:snapshot` and +`ghcr.io/eclipse-daanse/daanse-pivot-oracle:snapshot` (linux/amd64, linux/arm64). + +## Quick start + +```bash +docker run --name daanse-pivot \ + -v ./catalog:/app/catalog:ro \ + -e DAANSE_JDBC_SERVER_NAME=db.example.org \ + -e DAANSE_JDBC_SERVICE_NAME=freepdb1 \ + -e DAANSE_JDBC_USER=daanse \ + -e DAANSE_JDBC_PASSWORD=secret \ + -p 8080:8080 \ + eclipsedaanse/daanse-pivot-oracle:snapshot +``` + +The XMLA endpoint is then available at `http://localhost:8080/xmla`. + +## Oracle environment variables + +Required: `DAANSE_JDBC_USER`, `DAANSE_JDBC_PASSWORD` and either +`DAANSE_JDBC_SERVICE_NAME` (service name) or `DAANSE_JDBC_DATABASE_NAME` (SID). +Unset variables keep the defaults of the Daanse Oracle DataSource bundle. + +| Variable | Default | Description | +|---|---|---| +| `DAANSE_JDBC_SERVER_NAME` | `localhost` | Database host | +| `DAANSE_JDBC_PORT_NUMBER` | `1521` | Database port | +| `DAANSE_JDBC_SERVICE_NAME` | *(unset)* | Service name (preferred) | +| `DAANSE_JDBC_DATABASE_NAME` | *(unset)* | SID (alternative to the service name) | +| `DAANSE_JDBC_USER` | *(required)* | Database user | +| `DAANSE_JDBC_PASSWORD` | *(required)* | Database password | +| `DAANSE_JDBC_DEFAULT_ROW_PREFETCH` | `0` | Rows prefetched per round trip (0 = driver default) | + +## Source + +`https://github.com/eclipse-daanse/org.eclipse.daanse.server/tree/main/application/pivot/oracle` diff --git a/application/pivot/oracle/container/Dockerfile b/application/pivot/oracle/container/Dockerfile new file mode 100644 index 0000000..8cf6e87 --- /dev/null +++ b/application/pivot/oracle/container/Dockerfile @@ -0,0 +1,33 @@ +FROM eclipse-temurin:25-jre-alpine + +# Configurable UID/GID to avoid permission problems with bind mounts (e.g. rootless Podman) +ARG APP_UID=1000 +ARG APP_GID=1000 + +# Create non-root user for security +RUN addgroup -S -g ${APP_GID} appgroup \ + && adduser -S -u ${APP_UID} -G appgroup appuser + +LABEL org.opencontainers.image.source="https://github.com/eclipse-daanse/org.eclipse.daanse.server" \ + org.opencontainers.image.licenses="EPL-2.0" \ + org.opencontainers.image.vendor="Eclipse Daanse" \ + org.opencontainers.image.description="Daanse OLAP (XMLA) server for Oracle Database" + +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appgroup application/pivot/oracle/target/daanse.pivot.oracle.jar /app/ +COPY --chown=appuser:appgroup application/pivot/oracle/start /app/ +COPY --chown=appuser:appgroup application/pivot/oracle/logback.xml /app/ + +RUN mkdir -p /app/catalog /app/data /app/log \ + && chown -R appuser:appgroup /app \ + && chmod +x /app/start + +USER appuser + +VOLUME ["/app/catalog"] + +EXPOSE 8080 + +ENTRYPOINT ["sh","-c","exec /app/start"] diff --git a/application/pivot/oracle/daanse.pivot.oracle.bndrun b/application/pivot/oracle/daanse.pivot.oracle.bndrun new file mode 100644 index 0000000..750665a --- /dev/null +++ b/application/pivot/oracle/daanse.pivot.oracle.bndrun @@ -0,0 +1,129 @@ +#******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# SmartCity Jena - initial +# Stefan Bischof (bipolis.org) - initial +#******************************************************************************* + +-include: ~${.}/../common/daanse.pivot.base.bndrun + +-runrequires.db: \ + bnd.identity;id='${project.artifactId}',\ + bnd.identity;id='org.eclipse.daanse.jdbc.datasource.oracle',\ + bnd.identity;id='org.eclipse.daanse.sql.dialect.db.oracle' + +-runbundles: \ + ai.starlake.transpiler;version='[1.8.0,1.8.1)',\ + ch.qos.logback.classic;version='[1.5.6,1.5.7)',\ + com.fasterxml.woodstox.woodstox-core;version='[7.1.1,7.1.2)',\ + com.github.ben-manes.caffeine;version='[3.1.8,3.1.9)',\ + com.zaxxer.HikariCP;version='[7.1.0,7.1.1)',\ + net.sf.jsqlparser;version='[5.3.167,5.3.168)',\ + org.apache.aries.spifly.dynamic.bundle;version='[1.3.7,1.3.8)',\ + org.apache.commons.commons-collections4;version='[4.5.0,4.5.1)',\ + org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ + org.apache.felix.http.jetty12;version='[2.0.4,2.0.5)',\ + org.apache.felix.http.servlet-api;version='[6.1.0,6.1.1)',\ + org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ + org.apache.felix.scr;version='[2.2.10,2.2.11)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.businessinformation;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.datatypes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.keysindexes;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.behavioral;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.objectmodel.instance;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.cwm.model.cwm.resource.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.dmv.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jakarta.servlet.filter.cors;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.oracle;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.jdbc.datasource.pools.hikari;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.lcid.cfg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.model.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.mdx.parser.ccc;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.spi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.util.format;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.olap.xmla.connector.relational.oledb;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.rolap.mapping.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.server.application.pivot.oracle;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.oracle;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.statement.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.an;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.bi;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.cg;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.csdl.v2.edm;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.empty;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine200;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.engine300_300;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.exception;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.ext;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.io;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.mddataset;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.msxmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.multipleresults;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.core;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.mining;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.multidimensional;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.relational;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.rowset.server;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.soap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.model.xmla;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.adapter.emf;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.basic;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.roles;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.auth.store.ldap;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.xmla.server.whiteboard.servlet;version='[0.0.1,0.0.2)',\ + org.eclipse.emf.common;version='[2.44.0,2.44.1)',\ + org.eclipse.emf.ecore;version='[2.41.0,2.41.1)',\ + org.eclipse.emf.ecore.xmi;version='[2.39.0,2.39.1)',\ + org.eclipse.fennec.emf.osgi.component;version='[0.1.1,0.1.2)',\ + org.glassfish.hk2.osgi-resource-locator;version='[2.4.0,2.4.1)',\ + org.objectweb.asm;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.commons;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.tree.analysis;version='[9.9.0,9.9.1)',\ + org.objectweb.asm.util;version='[9.9.0,9.9.1)',\ + org.osgi.service.cm;version='[1.6.1,1.6.2)',\ + org.osgi.service.component;version='[1.5.1,1.5.2)',\ + org.osgi.service.log;version='[1.4.0,1.4.1)',\ + org.osgi.util.converter;version='[1.0.9,1.0.10)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.3.0,1.3.1)',\ + org.osgi.util.pushstream;version='[1.1.0,1.1.1)',\ + stax2-api;version='[4.2.2,4.2.3)' \ No newline at end of file diff --git a/application/pivot/oracle/logback.xml b/application/pivot/oracle/logback.xml new file mode 100644 index 0000000..f6ebf80 --- /dev/null +++ b/application/pivot/oracle/logback.xml @@ -0,0 +1,32 @@ + + + + + + + true + + + + + + %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + + diff --git a/application/pivot/oracle/pom.xml b/application/pivot/oracle/pom.xml new file mode 100644 index 0000000..dca5874 --- /dev/null +++ b/application/pivot/oracle/pom.xml @@ -0,0 +1,165 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot + ${revision} + + org.eclipse.daanse.server.application.pivot.oracle + + + oracle + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.oracle + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.oracle + 0.0.1-SNAPSHOT + runtime + + + + org.osgi + org.osgi.service.jdbc + 1.1.0 + runtime + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + test-jar + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + org.testcontainers + oracle-free + ${testcontainers.version} + test + + + + + + + biz.aQute.bnd + bnd-export-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + bnd.executablejar + true + false + + compile + runtime + + + + + + export + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + true + false + + compile + runtime + + + + + biz.aQute.bnd + bnd-run-maven-plugin + ${bnd.version} + + daanse.pivot.${pivot.db}.bndrun + true + + compile + runtime + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + + integration-test + verify + + + + + + + diff --git a/application/pivot/oracle/src/main/java/org/eclipse/daanse/server/application/pivot/oracle/OracleEnvConfigurator.java b/application/pivot/oracle/src/main/java/org/eclipse/daanse/server/application/pivot/oracle/OracleEnvConfigurator.java new file mode 100644 index 0000000..1a08d20 --- /dev/null +++ b/application/pivot/oracle/src/main/java/org/eclipse/daanse/server/application/pivot/oracle/OracleEnvConfigurator.java @@ -0,0 +1,97 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.oracle; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.List; + +import org.eclipse.daanse.jdbc.datasource.oracle.api.Constants; +import org.eclipse.daanse.jdbc.datasource.oracle.api.ocd.DsConfig; +import org.eclipse.daanse.server.application.pivot.common.BasicContextConfigs; +import org.eclipse.daanse.server.application.pivot.common.Env; +import org.eclipse.daanse.server.application.pivot.common.EnvConfigMapper; +import org.eclipse.daanse.server.application.pivot.common.ServerConstants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.annotations.RequireConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.RequireServiceComponentRuntime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Registers the Oracle DataSource from the {@code DAANSE_JDBC_*} environment + * variables and creates the BasicContext against it. Every attribute of + * {@link DsConfig} is supported. + */ +@Component(immediate = true) +@RequireConfigurationAdmin +@RequireServiceComponentRuntime +public class OracleEnvConfigurator { + + private static final Logger logger = LoggerFactory.getLogger(OracleEnvConfigurator.class); + + private static final String DIALECT_NAME = "ORACLE"; + + private static final String ENV_USER = "DAANSE_JDBC_USER"; + private static final String ENV_PASSWORD = "DAANSE_JDBC_PASSWORD"; + private static final String ENV_SERVICE_NAME = "DAANSE_JDBC_SERVICE_NAME"; + private static final String ENV_DATABASE_NAME = "DAANSE_JDBC_DATABASE_NAME"; + + @Reference + ConfigurationAdmin ca; + + private Configuration confDataSource; + private List confPoolAndContext = List.of(); + + @Activate + public void activate() throws IOException { + List missing = Env.missing(ENV_USER, ENV_PASSWORD); + if (!missing.isEmpty()) { + logger.error("Not configuring the Oracle DataSource, required environment variables are not set: {}", + missing); + return; + } + if (Env.get(ENV_SERVICE_NAME).isEmpty() && Env.get(ENV_DATABASE_NAME).isEmpty()) { + logger.error("Not configuring the Oracle DataSource, neither {} (service name) nor {} (SID) is set", + ENV_SERVICE_NAME, ENV_DATABASE_NAME); + return; + } + + Dictionary props = EnvConfigMapper.propsFromEnv(DsConfig.class); + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_DATASOURCE); + + confDataSource = ca.getFactoryConfiguration(Constants.PID_DATASOURCE, ServerConstants.CONFIG_IDENT, "?"); + confDataSource.update(props); + + confPoolAndContext = BasicContextConfigs.createEnvPoolAndContext(ca, DIALECT_NAME); + + logger.info("Oracle DataSource, connection pool and context configured from environment"); + } + + @Deactivate + public void deactivate() throws IOException { + if (confDataSource != null) { + confDataSource.delete(); + } + for (Configuration configuration : confPoolAndContext) { + configuration.delete(); + } + confPoolAndContext = List.of(); + } +} diff --git a/application/pivot/oracle/src/main/java/org/eclipse/daanse/server/application/pivot/oracle/package-info.java b/application/pivot/oracle/src/main/java/org/eclipse/daanse/server/application/pivot/oracle/package-info.java new file mode 100644 index 0000000..a73757c --- /dev/null +++ b/application/pivot/oracle/src/main/java/org/eclipse/daanse/server/application/pivot/oracle/package-info.java @@ -0,0 +1,14 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.oracle; diff --git a/application/pivot/oracle/src/test/java/org/eclipse/daanse/server/application/pivot/oracle/OraclePivotIT.java b/application/pivot/oracle/src/test/java/org/eclipse/daanse/server/application/pivot/oracle/OraclePivotIT.java new file mode 100644 index 0000000..a8d6518 --- /dev/null +++ b/application/pivot/oracle/src/test/java/org/eclipse/daanse/server/application/pivot/oracle/OraclePivotIT.java @@ -0,0 +1,76 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.oracle; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.server.application.pivot.common.test.PivotContainers; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.oracle.OracleContainer; +import org.testcontainers.utility.MountableFile; + +/** + * End-to-end test: starts an Oracle Free database, creates the Fact table of + * the static test catalog, builds the daanse-pivot-oracle image from its real + * Dockerfile, mounts the catalog and verifies via XMLA that the MDX sum over + * the VALUE column is queryable. + */ +@Testcontainers(disabledWithoutDocker = true) +class OraclePivotIT { + + static final Network NETWORK = Network.newNetwork(); + + @Container + static final OracleContainer DB = new OracleContainer("gvenzl/oracle-free:23-slim-faststart") + .withNetwork(NETWORK).withNetworkAliases("db"); + + @Test + void sumMeasureIsQueryableViaXmla() throws Exception { + try (Connection connection = DriverManager.getConnection(DB.getJdbcUrl(), DB.getUsername(), DB.getPassword()); + Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE TABLE \"Fact\" (\"KEY\" VARCHAR2(100), \"VALUE\" NUMBER(10))"); + statement.executeUpdate("INSERT INTO \"Fact\" VALUES ('A', 1)"); + statement.executeUpdate("INSERT INTO \"Fact\" VALUES ('B', 2)"); + statement.executeUpdate("INSERT INTO \"Fact\" VALUES ('C', 3)"); + } + + try (GenericContainer pivot = new GenericContainer<>(PivotContainers.pivotImage("oracle")) + .withNetwork(NETWORK) + .withCopyFileToContainer(MountableFile.forClasspathResource("catalog.xmi"), + "/app/catalog/catalog.xmi") + .withEnv("DAANSE_JDBC_SERVER_NAME", "db") + .withEnv("DAANSE_JDBC_PORT_NUMBER", "1521") + .withEnv("DAANSE_JDBC_SERVICE_NAME", DB.getDatabaseName()) + .withEnv("DAANSE_JDBC_USER", DB.getUsername()) + .withEnv("DAANSE_JDBC_PASSWORD", DB.getPassword()) + .withExposedPorts(8080) + .waitingFor(Wait.forListeningPort())) { + pivot.start(); + + String response = PivotContainers.awaitMdxCell(pivot.getHost(), pivot.getMappedPort(8080), + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "6"); + + assertTrue(response.contains("6"), response); + } + } +} diff --git a/application/pivot/oracle/src/test/resources/catalog.xmi b/application/pivot/oracle/src/test/resources/catalog.xmi new file mode 100644 index 0000000..65db914 --- /dev/null +++ b/application/pivot/oracle/src/test/resources/catalog.xmi @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/application/pivot/oracle/start b/application/pivot/oracle/start new file mode 100755 index 0000000..fe3507a --- /dev/null +++ b/application/pivot/oracle/start @@ -0,0 +1,19 @@ +#!/bin/sh + +JAVA_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/}java" +LOGBACK_CONFIG="./logback.xml" + +# The bundles declare osgi.ee=JavaSE-25; on an older JVM nothing resolves. +JAVA_MAJOR="$("$JAVA_EXEC" -version 2>&1 | awk -F'"' '/version/ { split($2, v, /[._]/); print (v[1] == "1" ? v[2] : v[1]); exit }')" +if [ "${JAVA_MAJOR:-0}" -lt 25 ]; then + echo "Java 25 is required, but $JAVA_EXEC is Java ${JAVA_MAJOR:-unknown}. Point JAVA_HOME at a Java 25 installation." + exit 1 +fi + +if [ ! -f "$LOGBACK_CONFIG" ]; then + echo "Logback-Configuration-File $LOGBACK_CONFIG not found." + exit 1 +fi + +echo "start Server" +"$JAVA_EXEC" -Dlogback.configurationFile=file:"$LOGBACK_CONFIG" -jar daanse.pivot.oracle.jar diff --git a/application/pivot/pom.xml b/application/pivot/pom.xml index cd1752f..aaa0833 100644 --- a/application/pivot/pom.xml +++ b/application/pivot/pom.xml @@ -1,7 +1,7 @@ + + + + + true + + + + + + %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + + diff --git a/application/pivot/postgres/pom.xml b/application/pivot/postgres/pom.xml new file mode 100644 index 0000000..6a8f0d8 --- /dev/null +++ b/application/pivot/postgres/pom.xml @@ -0,0 +1,165 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot + ${revision} + + org.eclipse.daanse.server.application.pivot.postgres + + + postgres + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + compile + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.postgresql + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.postgresql + 0.0.1-SNAPSHOT + runtime + + + + org.osgi + org.osgi.service.jdbc + 1.1.0 + runtime + + + + + org.eclipse.daanse + org.eclipse.daanse.server.application.pivot.common + ${project.version} + test-jar + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + org.testcontainers + postgresql + ${testcontainers.version} + test + + + + + + + biz.aQute.bnd + bnd-export-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + bnd.executablejar + true + false + + compile + runtime + + + + + + export + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + ${bnd.version} + + false + + daanse.pivot.${pivot.db}.bndrun + + true + false + + compile + runtime + + + + + biz.aQute.bnd + bnd-run-maven-plugin + ${bnd.version} + + daanse.pivot.${pivot.db}.bndrun + true + + compile + runtime + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + + integration-test + verify + + + + + + + diff --git a/application/pivot/postgres/src/main/java/org/eclipse/daanse/server/application/pivot/postgres/PostgresEnvConfigurator.java b/application/pivot/postgres/src/main/java/org/eclipse/daanse/server/application/pivot/postgres/PostgresEnvConfigurator.java new file mode 100644 index 0000000..6cb43f1 --- /dev/null +++ b/application/pivot/postgres/src/main/java/org/eclipse/daanse/server/application/pivot/postgres/PostgresEnvConfigurator.java @@ -0,0 +1,91 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.postgres; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.List; + +import org.eclipse.daanse.jdbc.datasource.postgresql.api.Constants; +import org.eclipse.daanse.jdbc.datasource.postgresql.api.ocd.DsConfig; +import org.eclipse.daanse.server.application.pivot.common.BasicContextConfigs; +import org.eclipse.daanse.server.application.pivot.common.Env; +import org.eclipse.daanse.server.application.pivot.common.EnvConfigMapper; +import org.eclipse.daanse.server.application.pivot.common.ServerConstants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.annotations.RequireConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.RequireServiceComponentRuntime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Registers the PostgreSQL DataSource from the {@code DAANSE_JDBC_*} + * environment variables and creates the BasicContext against it. Every + * attribute of {@link DsConfig} is supported. + */ +@Component(immediate = true) +@RequireConfigurationAdmin +@RequireServiceComponentRuntime +public class PostgresEnvConfigurator { + + private static final Logger logger = LoggerFactory.getLogger(PostgresEnvConfigurator.class); + + private static final String DIALECT_NAME = "POSTGRESQL"; + + private static final String ENV_USER = "DAANSE_JDBC_USER"; + private static final String ENV_PASSWORD = "DAANSE_JDBC_PASSWORD"; + private static final String ENV_DBNAME = "DAANSE_JDBC_DBNAME"; + + @Reference + ConfigurationAdmin ca; + + private Configuration confDataSource; + private List confPoolAndContext = List.of(); + + @Activate + public void activate() throws IOException { + List missing = Env.missing(ENV_USER, ENV_PASSWORD, ENV_DBNAME); + if (!missing.isEmpty()) { + logger.error("Not configuring the PostgreSQL DataSource, required environment variables are not set: {}", + missing); + return; + } + + Dictionary props = EnvConfigMapper.propsFromEnv(DsConfig.class); + props.put(ServerConstants.PROP_IDENT, ServerConstants.IDENT_DATASOURCE); + + confDataSource = ca.getFactoryConfiguration(Constants.PID_DATASOURCE, ServerConstants.CONFIG_IDENT, "?"); + confDataSource.update(props); + + confPoolAndContext = BasicContextConfigs.createEnvPoolAndContext(ca, DIALECT_NAME); + + logger.info("PostgreSQL DataSource, connection pool and context configured from environment"); + } + + @Deactivate + public void deactivate() throws IOException { + if (confDataSource != null) { + confDataSource.delete(); + } + for (Configuration configuration : confPoolAndContext) { + configuration.delete(); + } + confPoolAndContext = List.of(); + } +} diff --git a/application/pivot/postgres/src/main/java/org/eclipse/daanse/server/application/pivot/postgres/package-info.java b/application/pivot/postgres/src/main/java/org/eclipse/daanse/server/application/pivot/postgres/package-info.java new file mode 100644 index 0000000..ecce510 --- /dev/null +++ b/application/pivot/postgres/src/main/java/org/eclipse/daanse/server/application/pivot/postgres/package-info.java @@ -0,0 +1,14 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.postgres; diff --git a/application/pivot/postgres/src/test/java/org/eclipse/daanse/server/application/pivot/postgres/PostgresPivotIT.java b/application/pivot/postgres/src/test/java/org/eclipse/daanse/server/application/pivot/postgres/PostgresPivotIT.java new file mode 100644 index 0000000..5c93d94 --- /dev/null +++ b/application/pivot/postgres/src/test/java/org/eclipse/daanse/server/application/pivot/postgres/PostgresPivotIT.java @@ -0,0 +1,74 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.server.application.pivot.postgres; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.server.application.pivot.common.test.PivotContainers; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +/** + * End-to-end test: starts a PostgreSQL database, creates the Fact table of the + * static test catalog, builds the daanse-pivot-postgres image from its real + * Dockerfile, mounts the catalog and verifies via XMLA that the MDX sum over + * the VALUE column is queryable. + */ +@Testcontainers(disabledWithoutDocker = true) +class PostgresPivotIT { + + static final Network NETWORK = Network.newNetwork(); + + @Container + static final PostgreSQLContainer DB = new PostgreSQLContainer<>("postgres:17-alpine") + .withNetwork(NETWORK).withNetworkAliases("db"); + + @Test + void sumMeasureIsQueryableViaXmla() throws Exception { + try (Connection connection = DriverManager.getConnection(DB.getJdbcUrl(), DB.getUsername(), DB.getPassword()); + Statement statement = connection.createStatement()) { + statement.executeUpdate("CREATE TABLE \"Fact\" (\"KEY\" VARCHAR(100), \"VALUE\" INTEGER)"); + statement.executeUpdate("INSERT INTO \"Fact\" VALUES ('A', 1), ('B', 2), ('C', 3)"); + } + + try (GenericContainer pivot = new GenericContainer<>(PivotContainers.pivotImage("postgres")) + .withNetwork(NETWORK) + .withCopyFileToContainer(MountableFile.forClasspathResource("catalog.xmi"), + "/app/catalog/catalog.xmi") + .withEnv("DAANSE_JDBC_HOST", "db") + .withEnv("DAANSE_JDBC_PORT", "5432") + .withEnv("DAANSE_JDBC_DBNAME", DB.getDatabaseName()) + .withEnv("DAANSE_JDBC_USER", DB.getUsername()) + .withEnv("DAANSE_JDBC_PASSWORD", DB.getPassword()) + .withExposedPorts(8080) + .waitingFor(Wait.forListeningPort())) { + pivot.start(); + + String response = PivotContainers.awaitMdxCell(pivot.getHost(), pivot.getMappedPort(8080), + PivotContainers.TEST_CATALOG, PivotContainers.TEST_MDX, "6"); + + assertTrue(response.contains("6"), response); + } + } +} diff --git a/application/pivot/postgres/src/test/resources/catalog.xmi b/application/pivot/postgres/src/test/resources/catalog.xmi new file mode 100644 index 0000000..65db914 --- /dev/null +++ b/application/pivot/postgres/src/test/resources/catalog.xmi @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/application/pivot/postgres/start b/application/pivot/postgres/start new file mode 100755 index 0000000..bf35bfc --- /dev/null +++ b/application/pivot/postgres/start @@ -0,0 +1,19 @@ +#!/bin/sh + +JAVA_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/}java" +LOGBACK_CONFIG="./logback.xml" + +# The bundles declare osgi.ee=JavaSE-25; on an older JVM nothing resolves. +JAVA_MAJOR="$("$JAVA_EXEC" -version 2>&1 | awk -F'"' '/version/ { split($2, v, /[._]/); print (v[1] == "1" ? v[2] : v[1]); exit }')" +if [ "${JAVA_MAJOR:-0}" -lt 25 ]; then + echo "Java 25 is required, but $JAVA_EXEC is Java ${JAVA_MAJOR:-unknown}. Point JAVA_HOME at a Java 25 installation." + exit 1 +fi + +if [ ! -f "$LOGBACK_CONFIG" ]; then + echo "Logback-Configuration-File $LOGBACK_CONFIG not found." + exit 1 +fi + +echo "start Server" +"$JAVA_EXEC" -Dlogback.configurationFile=file:"$LOGBACK_CONFIG" -jar daanse.pivot.postgres.jar diff --git a/application/pivot/start b/application/pivot/start deleted file mode 100644 index 7e86fb4..0000000 --- a/application/pivot/start +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -# Path to Java-Binary -JAVA_EXEC="java" - -# Logback-Konfiguration -LOGBACK_CONFIG="./logback.xml" - -# Check if java -version returns output successfully -if ! "$JAVA_EXEC" -version >/dev/null 2>&1; then - echo "Java is installed but not working properly (java -version failed)." - exit 1 -fi - - -if [[ ! -f "$LOGBACK_CONFIG" ]]; then - echo "Logback-Configuration-File $LOGBACK_CONFIG not found." - exit 1 -fi - -# start Server -echo "start Server" -"$JAVA_EXEC" -Dlogback.configurationFile=file:"$LOGBACK_CONFIG" -jar daanse.pivot.jar - diff --git a/application/pivot/start.bat b/application/pivot/start.bat deleted file mode 100644 index d5981ab..0000000 --- a/application/pivot/start.bat +++ /dev/null @@ -1,28 +0,0 @@ -@echo off -setlocal - -REM Path to Java-Binary -set "JAVA_EXEC=java" - -REM Logback-Configuration-File -set "LOGBACK_CONFIG=.\logback.xml" - - -REM Check if java -version works -"%JAVA_EXEC%" -version >nul 2>&1 -if errorlevel 1 ( - echo Java is installed but not working properly (java -version failed). - exit /b 1 -) - -REM Check Logback configuration file exists -if not exist "%LOGBACK_CONFIG%" ( - echo Logback configuration file "%LOGBACK_CONFIG%" not found. - exit /b 1 -) - -REM Start Server -echo Starting server... -"%JAVA_EXEC%" -Dlogback.configurationFile=file:"%LOGBACK_CONFIG%" -jar daanse.pivot.jar - -endlocal diff --git a/application/pivot/zip_assembly.xml b/application/pivot/zip_assembly.xml deleted file mode 100644 index 9b8a6be..0000000 --- a/application/pivot/zip_assembly.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - bundle - - zip - - false - - - - ${project.build.directory}/daanse.pivot.jar - / - - - - ${project.basedir}/start.bat - / - - - - ${project.basedir}/start - / - - - - ${project.basedir}/logback.xml - / - - - - - - ${project.basedir}/catalog - catalog - - **/* - - - - ${project.basedir}/log - log - - **/* - - - - ${project.basedir}/load - catalogs - - **/* - - - - \ No newline at end of file diff --git a/application/playground/daanse.playground.bndrun b/application/playground/daanse.playground.bndrun index 3ec7863..18ff681 100644 --- a/application/playground/daanse.playground.bndrun +++ b/application/playground/daanse.playground.bndrun @@ -89,6 +89,7 @@ org.apache.felix.metatype;version='[1.2.4,1.2.5)',\ org.apache.felix.scr;version='[2.2.10,2.2.11)',\ org.apache.felix.webconsole;version='[5.0.12,5.0.13)',\ + org.eclipse.daanse.cwm.model.cwm.foundation.businessinformation;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.cwm.model.cwm.foundation.datatypes;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.cwm.model.cwm.foundation.keysindexes;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.cwm.model.cwm.objectmodel.behavioral;version='[0.0.1,0.0.2)',\ diff --git a/application/playground/pom.xml b/application/playground/pom.xml index 8c29275..0eedff4 100644 --- a/application/playground/pom.xml +++ b/application/playground/pom.xml @@ -139,6 +139,22 @@ runtime + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.datasource.h2 + 0.0.1-SNAPSHOT + runtime + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.h2 + 0.0.1-SNAPSHOT + runtime + + org.eclipse.daanse org.eclipse.daanse.jdbc.datasource.mariadb diff --git a/application/pom.xml b/application/pom.xml index 8179b86..7ad4936 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -73,9 +73,8 @@ + connector above is only the dispatch: without these three the + server refuses every request type as unknown. --> org.eclipse.daanse org.eclipse.daanse.olap.xmla.connector.relational @@ -125,6 +124,20 @@ compile + + org.eclipse.daanse + org.eclipse.daanse.xmla.server.auth.basic + 0.0.1-SNAPSHOT + compile + + + + org.eclipse.daanse + org.eclipse.daanse.xmla.server.auth.store.ldap + 0.0.1-SNAPSHOT + compile + + org.apache.felix org.apache.felix.http.jetty12 diff --git a/application/probe/src/main/java/org/eclipse/daanse/server/application/probe/ProbeFileListener.java b/application/probe/src/main/java/org/eclipse/daanse/server/application/probe/ProbeFileListener.java index 21a1f83..d07b35e 100644 --- a/application/probe/src/main/java/org/eclipse/daanse/server/application/probe/ProbeFileListener.java +++ b/application/probe/src/main/java/org/eclipse/daanse/server/application/probe/ProbeFileListener.java @@ -222,9 +222,8 @@ private void createContext(Path path, String matcherKey) throws IOException { Dictionary props = new Hashtable<>(); props.put(BASIC_CONTEXT_REF_NAME_CONNECTION_POOL + TARGET_EXT, filterOfMatcherKey(matcherKey)); - // Named, although DuckDB is the only dialect in the bundle set: the context - // binds whichever factory answers first, and one added later would silently - // take over the catalog. This once handed H2 a DuckDB statement. + // Name the dialect explicitly: the context binds whichever factory answers + // first, so a dialect bundle added later would silently take over. props.put(BASIC_CONTEXT_REF_NAME_DIALECT_FACTORY + TARGET_EXT, "(org.eclipse.daanse.dialect.name=DUCKDB)"); props.put(BASIC_CONTEXT_REF_NAME_CATALOG_MAPPING_SUPPLIER + TARGET_EXT, filterOfMatcherKey(matcherKey)); @@ -258,20 +257,12 @@ private void createCsvDatabaseImporter(Path path, String matcherKey) throws IOEx catalogFolderConfigsCSV.put(path, configCsv); } - /** - * The in-memory database this catalog folder is loaded into: DuckDB. - *

- * Not a choice any more. Measured on FoodMart in this repository, against H2 as - * the deployment it replaced: a crossjoin answered in 0.06 s where H2 took - * 1077 s, and the catalog's CSV files read in 0.67 s where row-by-row inserts - * needed 3.2 minutes. - */ + /** The in-memory DuckDB database this catalog folder is loaded into. */ private void createDataSource(Path path, String matcherKey) throws IOException { Configuration config = ca.getFactoryConfiguration(PID_DATASOURCE, UUID.randomUUID().toString(), "?"); Dictionary props = new Hashtable<>(); - // In memory. The DataSource keeps the connection that owns the database and hands - // out duplicates of it, which is what makes one in-memory database reachable from - // the importer and the pool alike. + // The DataSource keeps the owning connection and hands out duplicates, + // which makes one in-memory database reachable from importer and pool alike. props.put(DATASOURCE_PROPERTY_DATABASENAME, ":memory:"); props.put(DATASOURCE_PROPERTY_SETTINGS, new String[] { "threads=4" }); props.put(KEY_FILE_CONTEXT_MATCHER, matcherKey);