From 44d473756deb9dc16edbb1ee321ef072c7a96607 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Mon, 23 Mar 2026 12:05:46 +0100
Subject: [PATCH 01/45] Scaffold Quarkus backend, Maven wrapper, and Nix dev
shell~
---
.gitignore | 1 +
.mvn/wrapper/maven-wrapper.properties | 3 +
backend-quarkus/README.md | 40 +++
backend-quarkus/pom.xml | 118 ++++++
.../conquery/quarkus/api/PingResource.java | 23 ++
.../src/main/resources/application.properties | 5 +
.../quarkus/api/PingResourceTest.java | 21 ++
flake.lock | 61 ++++
flake.nix | 33 ++
mvnw | 338 ++++++++++++++++++
mvnw.cmd | 206 +++++++++++
pom.xml | 3 +-
12 files changed, 851 insertions(+), 1 deletion(-)
create mode 100644 .mvn/wrapper/maven-wrapper.properties
create mode 100644 backend-quarkus/README.md
create mode 100644 backend-quarkus/pom.xml
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/PingResource.java
create mode 100644 backend-quarkus/src/main/resources/application.properties
create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/PingResourceTest.java
create mode 100644 flake.lock
create mode 100644 flake.nix
create mode 100755 mvnw
create mode 100644 mvnw.cmd
diff --git a/.gitignore b/.gitignore
index 443ace390c..59811b6afb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,3 +77,4 @@ tutorial/mimic_iii_demo/data/**
/node_modules
cypress/screenshots/
cypress/videos/
+
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000000..7caaae64bb
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,3 @@
+distributionType=bin
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar
diff --git a/backend-quarkus/README.md b/backend-quarkus/README.md
new file mode 100644
index 0000000000..90103b693e
--- /dev/null
+++ b/backend-quarkus/README.md
@@ -0,0 +1,40 @@
+# Conquery Quarkus Backend (Migration Scaffold)
+
+This module is the starting point for migrating the Dropwizard backend to Quarkus in small, safe increments.
+
+## Current state
+
+- Maven module: `backend-quarkus`
+- Quarkus REST runtime with JSON support
+- Health and OpenAPI extensions wired
+- Baseline endpoint: `GET /api/ping`
+
+## Run locally
+
+From repository root:
+
+```bash
+mvn -pl backend-quarkus quarkus:dev
+```
+
+Default port is `8090`.
+
+## Migration strategy
+
+1. Keep `backend` (Dropwizard) running while we port feature slices.
+2. Move framework-neutral logic first (services, models, utility layers).
+3. Port HTTP resources incrementally:
+ - `resources/api/*`
+ - `resources/admin/rest/*`
+4. Replace Dropwizard-specific primitives with Quarkus equivalents:
+ - `@Auth` + Dropwizard Auth -> CDI + Quarkus Security/JAX-RS filters
+ - Dropwizard `Task` -> scheduled jobs/admin endpoints
+ - `Managed` lifecycle hooks -> Quarkus startup/shutdown events
+ - Freemarker views -> keep server-side templates via Quarkus Qute/Freemarker extension or move to frontend
+5. Port CLI commands (`ShardCommand`, `PreprocessorCommand`, etc.) to Quarkus CLI/Picocli entrypoints.
+
+## Immediate next slices
+
+- Add one real API resource from `backend/src/main/java/com/bakdata/conquery/resources/api`.
+- Introduce shared core module for code that should be reused by both runtimes.
+- Add compatibility tests that hit both old and new endpoints for parity.
diff --git a/backend-quarkus/pom.xml b/backend-quarkus/pom.xml
new file mode 100644
index 0000000000..a46b717c60
--- /dev/null
+++ b/backend-quarkus/pom.xml
@@ -0,0 +1,118 @@
+
+ 4.0.0
+
+
+ com.bakdata.conquery
+ parent
+ ${revision}
+
+
+ backend-quarkus
+ Conquery Backend (Quarkus)
+
+
+ io.quarkus.platform
+ quarkus-bom
+ 3.23.4
+ true
+
+
+
+
+
+ ${quarkus.platform.group-id}
+ ${quarkus.platform.artifact-id}
+ ${quarkus.platform.version}
+ pom
+ import
+
+
+ org.jboss.logging
+ jboss-logging
+ 3.6.1.Final
+
+
+
+
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ io.quarkus
+ quarkus-rest
+
+
+ io.quarkus
+ quarkus-rest-jackson
+
+
+ io.quarkus
+ quarkus-smallrye-openapi
+
+
+ io.quarkus
+ quarkus-smallrye-health
+
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ ${quarkus.platform.group-id}
+ quarkus-maven-plugin
+ ${quarkus.platform.version}
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+
+
+
+
+
+ maven-surefire-plugin
+
+
+ org.jboss.logmanager.LogManager
+
+
+
+
+ maven-failsafe-plugin
+ 3.2.5
+
+
+
+ integration-test
+ verify
+
+
+
+ ${project.build.directory}/${project.build.finalName}-runner
+ org.jboss.logmanager.LogManager
+
+
+
+
+
+
+
+
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/PingResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/PingResource.java
new file mode 100644
index 0000000000..1ae1d9b6c3
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/PingResource.java
@@ -0,0 +1,23 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/api/ping")
+@Produces(MediaType.APPLICATION_JSON)
+public class PingResource {
+
+ @GET
+ public Map ping() {
+ return Map.of(
+ "service", "conquery-backend-quarkus",
+ "status", "ok",
+ "timestamp", OffsetDateTime.now().toString()
+ );
+ }
+}
diff --git a/backend-quarkus/src/main/resources/application.properties b/backend-quarkus/src/main/resources/application.properties
new file mode 100644
index 0000000000..b68052b5bc
--- /dev/null
+++ b/backend-quarkus/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+quarkus.http.port=8090
+quarkus.http.root-path=/
+quarkus.smallrye-openapi.path=/q/openapi
+quarkus.swagger-ui.always-include=true
+quarkus.swagger-ui.path=/q/swagger-ui
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/PingResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/PingResourceTest.java
new file mode 100644
index 0000000000..e9bbd7e756
--- /dev/null
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/PingResourceTest.java
@@ -0,0 +1,21 @@
+package com.bakdata.conquery.quarkus.api;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+
+import io.quarkus.test.junit.QuarkusTest;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class PingResourceTest {
+
+ @Test
+ void pingEndpointResponds() {
+ given()
+ .when().get("/api/ping")
+ .then()
+ .statusCode(200)
+ .body("status", equalTo("ok"))
+ .body("service", equalTo("conquery-backend-quarkus"));
+ }
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000000..db686c56b7
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1767313136,
+ "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-25.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000000..899bc46869
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,33 @@
+{
+ description = "Local dev shell for Conquery";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+
+ outputs = { self, nixpkgs, flake-utils }:
+ flake-utils.lib.eachDefaultSystem (system:
+ let
+ pkgs = import nixpkgs { inherit system; };
+ in
+ {
+ devShells.default = pkgs.mkShell {
+ packages = with pkgs; [
+ jdk21
+ maven
+ nodejs_20
+ git
+ curl
+ jq
+ ripgrep
+ ];
+
+ shellHook = ''
+ export JAVA_HOME=${pkgs.jdk21}
+ export PATH="$JAVA_HOME/bin:$PATH"
+ echo "Conquery dev shell ready: java=$(java -version 2>&1 | head -n 1), node=$(node -v)"
+ '';
+ };
+ });
+}
diff --git a/mvnw b/mvnw
new file mode 100755
index 0000000000..df1a80ac19
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,338 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version @@project.version@@
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ]; then
+
+ if [ -f /usr/local/etc/mavenrc ]; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ]; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ]; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false
+darwin=false
+mingw=false
+case "$(uname)" in
+CYGWIN*) cygwin=true ;;
+MINGW*) mingw=true ;;
+Darwin*)
+ darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"
+ export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"
+ export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ]; then
+ if [ -r /etc/gentoo-release ]; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] \
+ && JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] \
+ && CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \
+ && JAVA_HOME="$(
+ cd "$JAVA_HOME" || (
+ echo "cannot cd into $JAVA_HOME." >&2
+ exit 1
+ )
+ pwd
+ )"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin; then
+ javaHome="$(dirname "$javaExecutable")"
+ javaExecutable="$(cd "$javaHome" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "$javaExecutable")"
+ fi
+ javaHome="$(dirname "$javaExecutable")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ]; then
+ if [ -n "$JAVA_HOME" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(
+ \unset -f command 2>/dev/null
+ \command -v java
+ )"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ]; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ echo "Warning: JAVA_HOME environment variable is not set." >&2
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]; then
+ echo "Path not specified to find_maven_basedir" >&2
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ]; do
+ if [ -d "$wdir"/.mvn ]; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(
+ cd "$wdir/.." || exit 1
+ pwd
+ )
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(
+ cd "$basedir" || exit 1
+ pwd
+ )"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' <"$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/@@project.version@@/maven-wrapper-@@project.version@@.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/@@project.version@@/maven-wrapper-@@project.version@@.jar"
+ fi
+ while IFS="=" read -r key value; do
+ case "$key" in wrapperUrl)
+ wrapperUrl=$(trim "${value-}")
+ break
+ ;;
+ esac
+ done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget >/dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget ${QUIET:+"$QUIET"} "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget ${QUIET:+"$QUIET"} --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl >/dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl ${QUIET:+"$QUIET"} -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl ${QUIET:+"$QUIET"} --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in wrapperSha256Sum)
+ wrapperSha256Sum=$(trim "${value-}")
+ break
+ ;;
+ esac
+done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum >/dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c --strict - >/dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] \
+ && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] \
+ && CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] \
+ && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000000..97fff17750
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,206 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version @@project.version@@
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo. >&2
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo. >&2
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo. >&2
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo. >&2
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/@@project.version@@/maven-wrapper-@@project.version@@.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/@@project.version@@/maven-wrapper-@@project.version@@.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/pom.xml b/pom.xml
index 860dda8e2f..14cedadf54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,7 @@
backend
+ backend-quarkus
executable
autodoc
@@ -129,4 +130,4 @@
-
\ No newline at end of file
+
From ae0b68ab9e74b9c6b9575ad9ce08728513b4bcbe Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Mon, 23 Mar 2026 15:00:07 +0100
Subject: [PATCH 02/45] adds frontend config api
---
README.md | 27 ++++++
backend-quarkus/pom.xml | 4 +
.../api/config/FrontendConfigResource.java | 91 +++++++++++++++++++
.../config/FrontendConfigurationResponse.java | 45 +++++++++
.../api/config/FrontendRuntimeConfig.java | 59 ++++++++++++
.../main/resources/application-dev.properties | 2 +
.../config/FrontendConfigResourceTest.java | 26 ++++++
7 files changed, 254 insertions(+)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResource.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigurationResponse.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java
create mode 100644 backend-quarkus/src/main/resources/application-dev.properties
create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResourceTest.java
diff --git a/README.md b/README.md
index 11ea1960a9..7903f5e974 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,33 @@ configurations, such as:
## Development
+### Backend Development
+
+#### Dropwizard backend (current production backend)
+
+From the repository root:
+
+```bash
+./mvnw -pl backend -am test
+./mvnw -pl executable -am package
+java -jar executable/target/conquery.jar standalone
+```
+
+#### Quarkus backend (migration target)
+
+From the repository root:
+
+```bash
+./mvnw -pl backend-quarkus quarkus:dev
+```
+
+Useful endpoints while running:
+
+- `http://localhost:8090/api/ping`
+- `http://localhost:8090/api/config/frontend`
+- `http://localhost:8090/q/health`
+- `http://localhost:8090/q/swagger-ui`
+
### Testing
#### Integration Tests
diff --git a/backend-quarkus/pom.xml b/backend-quarkus/pom.xml
index a46b717c60..931657b979 100644
--- a/backend-quarkus/pom.xml
+++ b/backend-quarkus/pom.xml
@@ -53,6 +53,10 @@
io.quarkus
quarkus-smallrye-openapi
+
+ io.quarkus
+ quarkus-info
+
io.quarkus
quarkus-smallrye-health
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResource.java
new file mode 100644
index 0000000000..369a18b968
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResource.java
@@ -0,0 +1,91 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import java.time.LocalDate;
+import java.time.Year;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+import io.quarkus.info.BuildInfo;
+import jakarta.enterprise.inject.Instance;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/api/config")
+@Produces(MediaType.APPLICATION_JSON)
+public class FrontendConfigResource {
+ @Inject
+ Instance buildInfo;
+
+ @Inject
+ FrontendRuntimeConfig frontendConfig;
+
+ @GET
+ @Path("/frontend")
+ public FrontendConfigurationResponse getFrontendConfig() {
+
+ FrontendRuntimeConfig.Currency currencyConfig = frontendConfig.currency();
+ FrontendConfigurationResponse.CurrencyConfigResponse currency =
+ new FrontendConfigurationResponse.CurrencyConfigResponse(
+ currencyConfig.unit(),
+ currencyConfig.thousandSeparator(),
+ currencyConfig.decimalSeparator(),
+ currencyConfig.decimalScale()
+ );
+
+
+ FrontendRuntimeConfig.QueryUpload queryUploadConfig = frontendConfig.queryUpload();
+ List configuredIds =
+ queryUploadConfig.ids()
+ .orElse(List.of())
+ .stream()
+ .map(id -> new FrontendConfigurationResponse.ColumnConfigResponse(
+ id.name(),
+ id.field(),
+ Optional.ofNullable(id.label()).orElse(Map.of(Locale.ROOT.toString(), "result")),
+ id.primaryId(),
+ id.print()
+ ))
+ .toList();
+
+ FrontendConfigurationResponse.IdColumnConfigResponse queryUpload =
+ new FrontendConfigurationResponse.IdColumnConfigResponse(
+ queryUploadConfig.table(),
+ configuredIds.isEmpty()
+ ? List.of(
+ new FrontendConfigurationResponse.ColumnConfigResponse(
+ "ID",
+ "pid",
+ Map.of("und", "result"),
+ true,
+ true
+ )
+ )
+ : configuredIds
+ );
+
+ List versions = List.of(
+ new FrontendConfigurationResponse.VersionContainerResponse("Backend", getBackendVersion(), null)
+ );
+
+ return new FrontendConfigurationResponse(
+ versions,
+ currency,
+ queryUpload,
+ frontendConfig.manualUrl().orElse(null),
+ frontendConfig.contactEmail().orElse(null),
+ Year.now().minusYears(frontendConfig.observationPeriodYears()).atDay(1)
+ );
+ }
+
+ private String getBackendVersion() {
+ if (buildInfo != null && buildInfo.isResolvable()) {
+ return buildInfo.get().version();
+ }
+ return "0.0.0-SNAPSHOT";
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigurationResponse.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigurationResponse.java
new file mode 100644
index 0000000000..c5d48992c3
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigurationResponse.java
@@ -0,0 +1,45 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+public record FrontendConfigurationResponse(
+ List versions,
+ CurrencyConfigResponse currency,
+ IdColumnConfigResponse queryUpload,
+ String manualUrl,
+ String contactEmail,
+ LocalDate observationPeriodStart
+) {
+
+ public record VersionContainerResponse(
+ String name,
+ String version,
+ String error
+ ) {
+ }
+
+ public record CurrencyConfigResponse(
+ String unit,
+ String thousandSeparator,
+ String decimalSeparator,
+ int decimalScale
+ ) {
+ }
+
+ public record IdColumnConfigResponse(
+ String table,
+ List ids
+ ) {
+ }
+
+ public record ColumnConfigResponse(
+ String name,
+ String field,
+ Map label,
+ boolean primaryId,
+ boolean print
+ ) {
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java
new file mode 100644
index 0000000000..a9f1a8564b
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java
@@ -0,0 +1,59 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigMapping(prefix = "conquery.frontend")
+public interface FrontendRuntimeConfig {
+
+ Currency currency();
+ QueryUpload queryUpload();
+
+ @WithDefault("6")
+ int observationPeriodYears();
+
+ Optional manualUrl();
+
+ Optional contactEmail();
+
+ interface Currency {
+ @WithDefault("€")
+ String unit();
+
+ @WithDefault(".")
+ String thousandSeparator();
+
+ @WithDefault(",")
+ String decimalSeparator();
+
+ @WithDefault("2")
+ int decimalScale();
+ }
+
+ interface QueryUpload {
+ @WithDefault("entities")
+ String table();
+
+ Optional> ids();
+ }
+
+ interface IdColumn {
+ @WithDefault("ID")
+ String name();
+
+ @WithDefault("pid")
+ String field();
+
+ Map label();
+
+ @WithDefault("true")
+ boolean primaryId();
+
+ @WithDefault("true")
+ boolean print();
+ }
+}
diff --git a/backend-quarkus/src/main/resources/application-dev.properties b/backend-quarkus/src/main/resources/application-dev.properties
new file mode 100644
index 0000000000..c845f6df4c
--- /dev/null
+++ b/backend-quarkus/src/main/resources/application-dev.properties
@@ -0,0 +1,2 @@
+quarkus.http.cors.enabled=true
+quarkus.http.cors.origins=/.*/
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResourceTest.java
new file mode 100644
index 0000000000..356f777f14
--- /dev/null
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResourceTest.java
@@ -0,0 +1,26 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+
+import io.quarkus.test.junit.QuarkusTest;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class FrontendConfigResourceTest {
+
+ @Test
+ void frontendConfigEndpointResponds() {
+ given()
+ .when().get("/api/config/frontend")
+ .then()
+ .statusCode(200)
+ .body("currency.unit", notNullValue())
+ .body("currency.decimalScale", notNullValue())
+ .body("queryUpload.table", equalTo("entities"))
+ .body("queryUpload.ids[0].name", equalTo("ID"))
+ .body("versions[0].name", equalTo("Backend"))
+ .body("observationPeriodStart", notNullValue());
+ }
+}
From 3e88c4646f1b46b724b05c3fb31822cbd41e409e Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Mon, 23 Mar 2026 15:30:47 +0100
Subject: [PATCH 03/45] adds me resource
---
backend-quarkus/pom.xml | 5 ++
.../conquery/quarkus/api/MeResource.java | 66 +++++++++++++++++++
.../conquery/quarkus/api/MeResourceTest.java | 38 +++++++++++
3 files changed, 109 insertions(+)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/MeResource.java
create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/MeResourceTest.java
diff --git a/backend-quarkus/pom.xml b/backend-quarkus/pom.xml
index 931657b979..77fa66324c 100644
--- a/backend-quarkus/pom.xml
+++ b/backend-quarkus/pom.xml
@@ -67,6 +67,11 @@
quarkus-junit5
test
+
+ io.quarkus
+ quarkus-test-security
+ test
+
io.rest-assured
rest-assured
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/MeResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/MeResource.java
new file mode 100644
index 0000000000..c5cabb1092
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/MeResource.java
@@ -0,0 +1,66 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.util.List;
+import java.util.Map;
+
+import io.quarkus.security.identity.SecurityIdentity;
+import jakarta.enterprise.inject.Instance;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/api/me")
+@Produces(MediaType.APPLICATION_JSON)
+public class MeResource {
+ @Inject
+ Instance identity;
+
+ @GET
+ public MeResponse me() {
+ String userName = resolveUserName(identity);
+
+ return new MeResponse(
+ userName,
+ true,
+ Map.of(),
+ List.of()
+ );
+ }
+
+ private static String resolveUserName(Instance identityInstance) {
+ if (identityInstance.isResolvable()) {
+ SecurityIdentity identity = identityInstance.get();
+ if (identity != null && !identity.isAnonymous() && identity.getPrincipal() != null) {
+ String principalName = identity.getPrincipal().getName();
+ if (principalName != null && !principalName.isBlank()) {
+ return principalName;
+ }
+ }
+ }
+
+ return "anonymous";
+ }
+
+ public record MeResponse(
+ String userName,
+ boolean hideLogoutButton,
+ Map datasetAbilities,
+ List groups
+ ) {
+ }
+
+ public record PermissionFlags(
+ boolean canUpload,
+ boolean canViewEntityPreview,
+ boolean canViewQueryPreview
+ ) {
+ }
+
+ public record GroupInfo(
+ String id,
+ String label
+ ) {
+ }
+}
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/MeResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/MeResourceTest.java
new file mode 100644
index 0000000000..d38cf5b766
--- /dev/null
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/MeResourceTest.java
@@ -0,0 +1,38 @@
+package com.bakdata.conquery.quarkus.api;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class MeResourceTest {
+
+ @Test
+ void meEndpointRespondsWithExpectedShape() {
+ given()
+ .when().get("/api/me")
+ .then()
+ .statusCode(200)
+ .body("userName", equalTo("anonymous"))
+ .body("hideLogoutButton", equalTo(true))
+ .body("datasetAbilities", notNullValue())
+ .body("groups", notNullValue());
+ }
+
+ @Test
+ @TestSecurity(user = "testUser")
+ void authneticatedMeEndpointRespondsWithExpectedShape() {
+ given()
+ .when().get("/api/me")
+ .then()
+ .statusCode(200)
+ .body("userName", equalTo("testUser"))
+ .body("hideLogoutButton", equalTo(true))
+ .body("datasetAbilities", notNullValue())
+ .body("groups", notNullValue());
+ }
+}
From d7b3b5606b0b2ab6cafa24743b47c652955a1087 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Mon, 23 Mar 2026 15:57:45 +0100
Subject: [PATCH 04/45] adds dataset endpoint
---
.../quarkus/api/DatasetsResource.java | 32 +++++++++++++++++++
.../api/config/DatasetsRuntimeConfig.java | 16 ++++++++++
.../main/resources/application-dev.properties | 11 +++++++
.../quarkus/api/DatasetsResourceTest.java | 23 +++++++++++++
4 files changed, 82 insertions(+)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java
create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
new file mode 100644
index 0000000000..424b53da33
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -0,0 +1,32 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.util.List;
+
+import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/api/datasets")
+@Produces(MediaType.APPLICATION_JSON)
+public class DatasetsResource {
+
+ @Inject
+ DatasetsRuntimeConfig datasetsConfig;
+
+ @GET
+ public List getDatasets() {
+ return datasetsConfig.datasets()
+ .stream()
+ .map(entry -> new DatasetResponse(entry.id(), entry.label()))
+ .toList();
+ }
+
+ public record DatasetResponse(
+ String id,
+ String label
+ ) {
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java
new file mode 100644
index 0000000000..232980e749
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java
@@ -0,0 +1,16 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import java.util.List;
+
+import io.smallrye.config.ConfigMapping;
+
+@ConfigMapping(prefix = "conquery")
+public interface DatasetsRuntimeConfig {
+ List datasets();
+
+ interface DatasetEntry {
+ String id();
+
+ String label();
+ }
+}
diff --git a/backend-quarkus/src/main/resources/application-dev.properties b/backend-quarkus/src/main/resources/application-dev.properties
index c845f6df4c..bf9a1eee0e 100644
--- a/backend-quarkus/src/main/resources/application-dev.properties
+++ b/backend-quarkus/src/main/resources/application-dev.properties
@@ -1,2 +1,13 @@
quarkus.http.cors.enabled=true
quarkus.http.cors.origins=/.*/
+
+conquery.frontend.query-upload.ids[0].name=ID
+conquery.frontend.query-upload.ids[0].field=pid
+conquery.frontend.query-upload.ids[0].label=result
+conquery.frontend.query-upload.ids[0].primary-id=true
+conquery.frontend.query-upload.ids[0].print=true
+
+conquery.datasets[0].id=imdb
+conquery.datasets[0].label=IMDb
+conquery.datasets[1].id=empty-set
+conquery.datasets[1].label=Empty Dataset
\ No newline at end of file
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
new file mode 100644
index 0000000000..2614c9295f
--- /dev/null
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
@@ -0,0 +1,23 @@
+package com.bakdata.conquery.quarkus.api;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+
+import io.quarkus.test.junit.QuarkusTest;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class DatasetsResourceTest {
+
+ @Test
+ void datasetsEndpointRespondsWithConfiguredDatasets() {
+ given()
+ .when().get("/api/datasets")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThanOrEqualTo(1))
+ .body("[0].id", equalTo("imdb"))
+ .body("[0].label", equalTo("IMDb"));
+ }
+}
From 9a09a7a1fd26ea974877f27b21b16ae217a625ee Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 24 Mar 2026 07:55:14 +0100
Subject: [PATCH 05/45] adds entity-preview endpoint
---
.../quarkus/api/DatasetsResource.java | 33 ++++++++++++++++++-
.../quarkus/api/EntityPreviewResponse.java | 19 +++++++++++
.../config/EntityPreviewRuntimeConfig.java | 27 +++++++++++++++
.../src/main/resources/application.properties | 16 +++++++++
.../quarkus/api/DatasetsResourceTest.java | 23 +++++++++++++
5 files changed, 117 insertions(+), 1 deletion(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/EntityPreviewResponse.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index 424b53da33..9bbbf0849b 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -1,21 +1,27 @@
package com.bakdata.conquery.quarkus.api;
import java.util.List;
+import java.util.stream.Stream;
import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
+import com.bakdata.conquery.quarkus.api.config.EntityPreviewRuntimeConfig;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
+import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/api/datasets")
@Produces(MediaType.APPLICATION_JSON)
public class DatasetsResource {
-
@Inject
DatasetsRuntimeConfig datasetsConfig;
+ @Inject
+ EntityPreviewRuntimeConfig entityPreviewConfig;
+
@GET
public List getDatasets() {
return datasetsConfig.datasets()
@@ -24,6 +30,31 @@ public List getDatasets() {
.toList();
}
+ @GET
+ @Path("/{datasetId}/entity-preview")
+ public EntityPreviewResponse getEntityPreview(@PathParam("datasetId") String datasetId) {
+ boolean datasetExists = datasetsConfig.datasets().stream().anyMatch(dataset -> dataset.id().equals(datasetId));
+ if (!datasetExists) {
+ throw new NotFoundException("Unknown dataset: " + datasetId);
+ }
+
+ List allSources =
+ entityPreviewConfig.allSources().stream().map(source -> new EntityPreviewResponse.LabeledSource(source.name(), source.label())).toList();
+
+ List defaultSources =
+ entityPreviewConfig.defaultSources().stream().map(source -> new EntityPreviewResponse.LabeledSource(source.name(), source.label())).toList();
+
+ List searchFilters = entityPreviewConfig.searchFilters()
+ .map(value -> Stream.of(value.split(","))
+ .map(String::trim)
+ .filter(filter -> !filter.isEmpty())
+ .toList())
+ .orElse(List.of());
+ String searchConcept = entityPreviewConfig.searchConcept().orElse(null);
+
+ return new EntityPreviewResponse(allSources, defaultSources, searchFilters, searchConcept);
+ }
+
public record DatasetResponse(
String id,
String label
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/EntityPreviewResponse.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/EntityPreviewResponse.java
new file mode 100644
index 0000000000..dd3386da05
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/EntityPreviewResponse.java
@@ -0,0 +1,19 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public record EntityPreviewResponse(
+ List all,
+ @JsonProperty("default")
+ List defaultSources,
+ List searchFilters,
+ String searchConcept
+) {
+ public record LabeledSource(
+ String name,
+ String label
+ ) {
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java
new file mode 100644
index 0000000000..d7755e354c
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java
@@ -0,0 +1,27 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithName;
+
+@ConfigMapping(prefix = "conquery.entity-preview")
+public interface EntityPreviewRuntimeConfig {
+
+ @WithName("all")
+ List allSources();
+
+ @WithName("default")
+ List defaultSources();
+
+ Optional searchConcept();
+
+ Optional searchFilters();
+
+ interface Source {
+ String name();
+
+ String label();
+ }
+}
diff --git a/backend-quarkus/src/main/resources/application.properties b/backend-quarkus/src/main/resources/application.properties
index b68052b5bc..3dddf7d1aa 100644
--- a/backend-quarkus/src/main/resources/application.properties
+++ b/backend-quarkus/src/main/resources/application.properties
@@ -3,3 +3,19 @@ quarkus.http.root-path=/
quarkus.smallrye-openapi.path=/q/openapi
quarkus.swagger-ui.always-include=true
quarkus.swagger-ui.path=/q/swagger-ui
+
+conquery.datasets[0].id=imdb
+conquery.datasets[0].label=IMDb
+conquery.datasets[1].id=empty-set
+conquery.datasets[1].label=Empty Dataset
+
+conquery.entity-preview.all[0].name=imdb:entities
+conquery.entity-preview.all[0].label=Entities
+conquery.entity-preview.default[0].name=imdb:entities
+conquery.entity-preview.default[0].label=Entities
+
+conquery.frontend.query-upload.ids[0].name=ID
+conquery.frontend.query-upload.ids[0].field=pid
+conquery.frontend.query-upload.ids[0].label.und=result
+conquery.frontend.query-upload.ids[0].primary-id=true
+conquery.frontend.query-upload.ids[0].print=true
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
index 2614c9295f..7219ab81eb 100644
--- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
@@ -3,6 +3,7 @@
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.nullValue;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
@@ -20,4 +21,26 @@ void datasetsEndpointRespondsWithConfiguredDatasets() {
.body("[0].id", equalTo("imdb"))
.body("[0].label", equalTo("IMDb"));
}
+
+ @Test
+ void entityPreviewEndpointRespondsWithConfiguredDefaults() {
+ given()
+ .when().get("/api/datasets/imdb/entity-preview")
+ .then()
+ .statusCode(200)
+ .body("all[0].name", equalTo("imdb:entities"))
+ .body("all[0].label", equalTo("Entities"))
+ .body("default[0].name", equalTo("imdb:entities"))
+ .body("default[0].label", equalTo("Entities"))
+ .body("searchConcept", nullValue())
+ .body("searchFilters.size()", equalTo(0));
+ }
+
+ @Test
+ void entityPreviewEndpointReturns404ForUnknownDataset() {
+ given()
+ .when().get("/api/datasets/unknown/entity-preview")
+ .then()
+ .statusCode(404);
+ }
}
From df3d00247fbb24a26bc841871d7e3ee0be47527b Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 24 Mar 2026 08:34:53 +0100
Subject: [PATCH 06/45] adds concepts endpoint
---
.../quarkus/api/ConceptsResponse.java | 34 +++++++++++++++
.../quarkus/api/DatasetsResource.java | 41 +++++++++++++++++--
.../quarkus/api/DatasetsResourceTest.java | 20 +++++++++
3 files changed, 91 insertions(+), 4 deletions(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptsResponse.java
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptsResponse.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptsResponse.java
new file mode 100644
index 0000000000..3cbc05a87b
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptsResponse.java
@@ -0,0 +1,34 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
+
+public record ConceptsResponse(
+ List secondaryIds,
+ Map concepts
+) {
+ public record SecondaryIdResponse(
+ String id,
+ String label,
+ String description
+ ) {
+ }
+
+ public record ConceptSummaryResponse(
+ String label,
+ String description,
+ @Schema(description = "Whether this node can be dragged/selected in the query editor. "
+ + "When detailsAvailable is false (folder/structure node), this should be false.")
+ Boolean active,
+ List children,
+ Long matchingEntries,
+ Long matchingEntities,
+ @Schema(description = "Whether detailed concept contents are available via /api/concepts/{conceptId}. "
+ + "If false, this node is a folder/structure node and should not be draggable.")
+ Boolean detailsAvailable,
+ Boolean codeListResolvable
+ ) {
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index 9bbbf0849b..8ca6950797 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -12,6 +12,7 @@
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.openapi.annotations.Operation;
@Path("/api/datasets")
@Produces(MediaType.APPLICATION_JSON)
@@ -33,10 +34,7 @@ public List getDatasets() {
@GET
@Path("/{datasetId}/entity-preview")
public EntityPreviewResponse getEntityPreview(@PathParam("datasetId") String datasetId) {
- boolean datasetExists = datasetsConfig.datasets().stream().anyMatch(dataset -> dataset.id().equals(datasetId));
- if (!datasetExists) {
- throw new NotFoundException("Unknown dataset: " + datasetId);
- }
+ requireDataset(datasetId);
List allSources =
entityPreviewConfig.allSources().stream().map(source -> new EntityPreviewResponse.LabeledSource(source.name(), source.label())).toList();
@@ -55,6 +53,41 @@ public EntityPreviewResponse getEntityPreview(@PathParam("datasetId") String dat
return new EntityPreviewResponse(allSources, defaultSources, searchFilters, searchConcept);
}
+ @GET
+ @Path("/{datasetId}/concepts")
+ @Operation(
+ summary = "Get root concepts for a dataset",
+ description = "Nodes with detailsAvailable=false represent folder/structure nodes and should not be draggable. "
+ + "Their active flag should be false."
+ )
+ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
+ DatasetsRuntimeConfig.DatasetEntry dataset = requireDataset(datasetId);
+
+ ConceptsResponse.ConceptSummaryResponse rootConcept = new ConceptsResponse.ConceptSummaryResponse(
+ dataset.label(),
+ null,
+ true,
+ List.of(),
+ 0L,
+ 0L,
+ false,
+ false
+ );
+
+ return new ConceptsResponse(
+ List.of(),
+ java.util.Map.of(dataset.id(), rootConcept)
+ );
+ }
+
+ private DatasetsRuntimeConfig.DatasetEntry requireDataset(String datasetId) {
+ return datasetsConfig.datasets()
+ .stream()
+ .filter(dataset -> dataset.id().equals(datasetId))
+ .findFirst()
+ .orElseThrow(() -> new NotFoundException("Unknown dataset: " + datasetId));
+ }
+
public record DatasetResponse(
String id,
String label
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
index 7219ab81eb..5550be2e95 100644
--- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
@@ -43,4 +43,24 @@ void entityPreviewEndpointReturns404ForUnknownDataset() {
.then()
.statusCode(404);
}
+
+ @Test
+ void conceptsEndpointRespondsWithFrontendCompatibleShape() {
+ given()
+ .when().get("/api/datasets/imdb/concepts")
+ .then()
+ .statusCode(200)
+ .body("secondaryIds.size()", equalTo(0))
+ .body("concepts.imdb.label", equalTo("IMDb"))
+ .body("concepts.imdb.detailsAvailable", equalTo(false))
+ .body("concepts.imdb.children.size()", equalTo(0));
+ }
+
+ @Test
+ void conceptsEndpointReturns404ForUnknownDataset() {
+ given()
+ .when().get("/api/datasets/unknown/concepts")
+ .then()
+ .statusCode(404);
+ }
}
From 14c6e6f3d7d80c3e56e2fac666ee0f6350d18631 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 24 Mar 2026 09:05:27 +0100
Subject: [PATCH 07/45] adds form-config endpoint
---
.../quarkus/api/DatasetsResource.java | 36 +++++++++++++++++
.../api/config/FormQueriesRuntimeConfig.java | 12 ++++++
.../forms/export_form.frontend_conf.json | 40 +++++++++++++++++++
.../table_export_form.frontend_conf.json | 40 +++++++++++++++++++
.../quarkus/api/DatasetsResourceTest.java | 19 +++++++++
5 files changed, 147 insertions(+)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java
create mode 100644 backend-quarkus/src/main/resources/forms/export_form.frontend_conf.json
create mode 100644 backend-quarkus/src/main/resources/forms/table_export_form.frontend_conf.json
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index 8ca6950797..b2707da23e 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -1,10 +1,16 @@
package com.bakdata.conquery.quarkus.api;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
import java.util.List;
import java.util.stream.Stream;
import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
import com.bakdata.conquery.quarkus.api.config.EntityPreviewRuntimeConfig;
+import com.bakdata.conquery.quarkus.api.config.FormQueriesRuntimeConfig;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
@@ -23,6 +29,12 @@ public class DatasetsResource {
@Inject
EntityPreviewRuntimeConfig entityPreviewConfig;
+ @Inject
+ FormQueriesRuntimeConfig formQueriesConfig;
+
+ @Inject
+ ObjectMapper objectMapper;
+
@GET
public List getDatasets() {
return datasetsConfig.datasets()
@@ -80,6 +92,17 @@ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
);
}
+ @GET
+ @Path("/{datasetId}/form-queries")
+ @Operation(
+ summary = "Get form configurations for a dataset",
+ description = "Returns raw frontend form configuration objects."
+ )
+ public List getFormQueries(@PathParam("datasetId") String datasetId) {
+ requireDataset(datasetId);
+ return formQueriesConfig.resources().stream().map(this::loadFormResource).toList();
+ }
+
private DatasetsRuntimeConfig.DatasetEntry requireDataset(String datasetId) {
return datasetsConfig.datasets()
.stream()
@@ -88,6 +111,19 @@ private DatasetsRuntimeConfig.DatasetEntry requireDataset(String datasetId) {
.orElseThrow(() -> new NotFoundException("Unknown dataset: " + datasetId));
}
+ private JsonNode loadFormResource(String path) {
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ try (InputStream input = classLoader.getResourceAsStream(path)) {
+ if (input == null) {
+ throw new IllegalStateException("Configured form resource does not exist: " + path);
+ }
+ return objectMapper.readTree(input);
+ }
+ catch (IOException e) {
+ throw new UncheckedIOException("Failed to parse form resource: " + path, e);
+ }
+ }
+
public record DatasetResponse(
String id,
String label
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java
new file mode 100644
index 0000000000..9cfdecdaf7
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java
@@ -0,0 +1,12 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import java.util.List;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigMapping(prefix = "conquery.form-queries")
+public interface FormQueriesRuntimeConfig {
+ @WithDefault("forms/export_form.frontend_conf.json,forms/table_export_form.frontend_conf.json")
+ List resources();
+}
diff --git a/backend-quarkus/src/main/resources/forms/export_form.frontend_conf.json b/backend-quarkus/src/main/resources/forms/export_form.frontend_conf.json
new file mode 100644
index 0000000000..247d105569
--- /dev/null
+++ b/backend-quarkus/src/main/resources/forms/export_form.frontend_conf.json
@@ -0,0 +1,40 @@
+{
+ "type": "EXPORT_FORM",
+ "title": {
+ "de": "Export Formular",
+ "en": "Export Form"
+ },
+ "description": {
+ "de": "Beispiel-Formular fuer die Quarkus Migration.",
+ "en": "Example form for the Quarkus migration."
+ },
+ "fields": [
+ {
+ "type": "HEADLINE",
+ "label": {
+ "de": "Ergebnis",
+ "en": "Result"
+ }
+ },
+ {
+ "name": "resultGroup",
+ "type": "RESULT_GROUP",
+ "label": {
+ "de": "Ergebnisgruppe",
+ "en": "Result Group"
+ },
+ "dropzoneLabel": {
+ "de": "Ziehe eine vorherige Abfrage hierher",
+ "en": "Drop a previous query here"
+ }
+ },
+ {
+ "name": "dataset",
+ "type": "DATASET_SELECT",
+ "label": {
+ "de": "Datensatz",
+ "en": "Dataset"
+ }
+ }
+ ]
+}
diff --git a/backend-quarkus/src/main/resources/forms/table_export_form.frontend_conf.json b/backend-quarkus/src/main/resources/forms/table_export_form.frontend_conf.json
new file mode 100644
index 0000000000..5e5f1d3abc
--- /dev/null
+++ b/backend-quarkus/src/main/resources/forms/table_export_form.frontend_conf.json
@@ -0,0 +1,40 @@
+{
+ "type": "FULL_EXPORT_FORM",
+ "title": {
+ "de": "Tabellenexport",
+ "en": "Table Export"
+ },
+ "description": {
+ "de": "Minimale Konfiguration fuer den Formular-Tab.",
+ "en": "Minimal configuration for the forms tab."
+ },
+ "fields": [
+ {
+ "type": "HEADLINE",
+ "label": {
+ "de": "Zeitraum",
+ "en": "Date Range"
+ }
+ },
+ {
+ "name": "dateRange",
+ "type": "DATE_RANGE",
+ "label": {
+ "de": "Zeitraum",
+ "en": "Date Range"
+ }
+ },
+ {
+ "name": "resultGroup",
+ "type": "RESULT_GROUP",
+ "label": {
+ "de": "Ergebnisgruppe",
+ "en": "Result Group"
+ },
+ "dropzoneLabel": {
+ "de": "Ziehe eine vorherige Abfrage hierher",
+ "en": "Drop a previous query here"
+ }
+ }
+ ]
+}
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
index 5550be2e95..a5ffb115fb 100644
--- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
@@ -63,4 +63,23 @@ void conceptsEndpointReturns404ForUnknownDataset() {
.then()
.statusCode(404);
}
+
+ @Test
+ void formQueriesEndpointReturnsForms() {
+ given()
+ .when().get("/api/datasets/imdb/form-queries")
+ .then()
+ .statusCode(200)
+ .body("size()", greaterThanOrEqualTo(1))
+ .body("[0].type", equalTo("EXPORT_FORM"))
+ .body("[0].fields.size()", greaterThanOrEqualTo(1));
+ }
+
+ @Test
+ void formQueriesEndpointReturns404ForUnknownDataset() {
+ given()
+ .when().get("/api/datasets/unknown/form-queries")
+ .then()
+ .statusCode(404);
+ }
}
From 3211dd644283a28d21c8a67183df838c4d8a41c8 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 24 Mar 2026 09:16:31 +0100
Subject: [PATCH 08/45] adds queries endpoint
---
.../quarkus/api/DatasetsResource.java | 35 +++++++++++++++++++
.../quarkus/api/DatasetsResourceTest.java | 17 +++++++++
2 files changed, 52 insertions(+)
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index b2707da23e..93e36d864d 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -103,6 +103,17 @@ public List getFormQueries(@PathParam("datasetId") String datasetId) {
return formQueriesConfig.resources().stream().map(this::loadFormResource).toList();
}
+ @GET
+ @Path("/{datasetId}/queries")
+ @Operation(
+ summary = "List queries for a dataset",
+ description = "Returns the query history list for the given dataset."
+ )
+ public List getQueries(@PathParam("datasetId") String datasetId) {
+ requireDataset(datasetId);
+ return List.of();
+ }
+
private DatasetsRuntimeConfig.DatasetEntry requireDataset(String datasetId) {
return datasetsConfig.datasets()
.stream()
@@ -124,6 +135,30 @@ private JsonNode loadFormResource(String path) {
}
}
+ public record QuerySummaryResponse(
+ String id,
+ String label,
+ Long numberOfResults,
+ String createdAt,
+ List tags,
+ boolean own,
+ String ownerName,
+ boolean system,
+ List resultUrls,
+ boolean shared,
+ boolean canExpand,
+ String queryType,
+ String secondaryId,
+ boolean containsDates
+ ) {
+ }
+
+ public record ResultUrlResponse(
+ String label,
+ String url
+ ) {
+ }
+
public record DatasetResponse(
String id,
String label
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
index a5ffb115fb..58b2557a5d 100644
--- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
@@ -82,4 +82,21 @@ void formQueriesEndpointReturns404ForUnknownDataset() {
.then()
.statusCode(404);
}
+
+ @Test
+ void queriesEndpointReturnsEmptyList() {
+ given()
+ .when().get("/api/datasets/imdb/queries")
+ .then()
+ .statusCode(200)
+ .body("size()", equalTo(0));
+ }
+
+ @Test
+ void queriesEndpointReturns404ForUnknownDataset() {
+ given()
+ .when().get("/api/datasets/unknown/queries")
+ .then()
+ .statusCode(404);
+ }
}
From 82c5e75d4ef638e96719eed717c4185931d4bf52 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 24 Mar 2026 09:28:43 +0100
Subject: [PATCH 09/45] adds endpoint for stored query
---
.../conquery/quarkus/api/QueryResource.java | 79 +++++++++++++++++++
.../quarkus/api/QueryResourceTest.java | 25 ++++++
2 files changed, 104 insertions(+)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/QueryResourceTest.java
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
new file mode 100644
index 0000000000..6ba8d32898
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
@@ -0,0 +1,79 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.time.Instant;
+import java.util.List;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.openapi.annotations.Operation;
+
+@Path("/api/queries")
+@Produces(MediaType.APPLICATION_JSON)
+public class QueryResource {
+
+ @GET
+ @Path("/{queryId}")
+ @Operation(
+ summary = "Get a query by id",
+ description = "Returns the current query execution status and metadata."
+ )
+ public QueryResponse getQuery(@PathParam("queryId") String queryId) {
+ return new QueryResponse(
+ queryId,
+ "Query " + queryId,
+ Instant.now().toString(),
+ true,
+ false,
+ false,
+ List.of(),
+ null,
+ null,
+ "anonymous",
+ List.of(),
+ false,
+ List.of(),
+ "NEW",
+ null,
+ null,
+ null,
+ null,
+ "CONCEPT_QUERY",
+ 0L,
+ false
+ );
+ }
+
+ public record QueryResponse(
+ String id,
+ String label,
+ String createdAt,
+ boolean own,
+ boolean shared,
+ boolean system,
+ List tags,
+ Object query,
+ String secondaryId,
+ String ownerName,
+ List groups,
+ boolean canExpand,
+ List availableSecondaryIds,
+ String status,
+ Double progress,
+ String error,
+ Long numberOfResults,
+ List resultUrls,
+ String queryType,
+ long requiredTime,
+ boolean containsDates
+ ) {
+ }
+
+ public record ResultUrlResponse(
+ String label,
+ String url
+ ) {
+ }
+}
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/QueryResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/QueryResourceTest.java
new file mode 100644
index 0000000000..e8e6504b02
--- /dev/null
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/QueryResourceTest.java
@@ -0,0 +1,25 @@
+package com.bakdata.conquery.quarkus.api;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+
+import io.quarkus.test.junit.QuarkusTest;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class QueryResourceTest {
+
+ @Test
+ void queryEndpointRespondsWithNewStatus() {
+ given()
+ .when().get("/api/queries/example-query")
+ .then()
+ .statusCode(200)
+ .body("id", equalTo("example-query"))
+ .body("label", equalTo("Query example-query"))
+ .body("status", equalTo("NEW"))
+ .body("queryType", equalTo("CONCEPT_QUERY"))
+ .body("createdAt", notNullValue());
+ }
+}
From a5f3443712a47f75835b56a168b9de5051bd11ae Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 24 Mar 2026 09:42:28 +0100
Subject: [PATCH 10/45] adds post query endpoint
---
.../quarkus/api/DatasetsResource.java | 17 ++++++++++++++
.../quarkus/api/DatasetsResourceTest.java | 22 +++++++++++++++++++
2 files changed, 39 insertions(+)
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index 93e36d864d..2bfd8f4636 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -13,6 +13,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@@ -114,6 +115,17 @@ public List getQueries(@PathParam("datasetId") String data
return List.of();
}
+ @POST
+ @Path("/{datasetId}/queries")
+ @Operation(
+ summary = "Create a query for a dataset",
+ description = "Accepts a query payload and returns the created query id."
+ )
+ public StartQueryResponse postQueries(@PathParam("datasetId") String datasetId, JsonNode payload) {
+ requireDataset(datasetId);
+ return new StartQueryResponse(java.util.UUID.randomUUID().toString());
+ }
+
private DatasetsRuntimeConfig.DatasetEntry requireDataset(String datasetId) {
return datasetsConfig.datasets()
.stream()
@@ -159,6 +171,11 @@ public record ResultUrlResponse(
) {
}
+ public record StartQueryResponse(
+ String id
+ ) {
+ }
+
public record DatasetResponse(
String id,
String label
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
index 58b2557a5d..c05d6b59df 100644
--- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
@@ -3,6 +3,7 @@
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import io.quarkus.test.junit.QuarkusTest;
@@ -99,4 +100,25 @@ void queriesEndpointReturns404ForUnknownDataset() {
.then()
.statusCode(404);
}
+
+ @Test
+ void postQueriesEndpointReturnsCreatedQueryId() {
+ given()
+ .contentType("application/json")
+ .body("{\"type\":\"CONCEPT_QUERY\"}")
+ .when().post("/api/datasets/imdb/queries")
+ .then()
+ .statusCode(200)
+ .body("id", notNullValue());
+ }
+
+ @Test
+ void postQueriesEndpointReturns404ForUnknownDataset() {
+ given()
+ .contentType("application/json")
+ .body("{\"type\":\"CONCEPT_QUERY\"}")
+ .when().post("/api/datasets/unknown/queries")
+ .then()
+ .statusCode(404);
+ }
}
From 94a0117d2269fde6dc05d8cc0b6af49586fdf8cb Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 24 Mar 2026 10:12:49 +0100
Subject: [PATCH 11/45] adds query cancel endpoint
---
.../bakdata/conquery/quarkus/api/QueryResource.java | 11 +++++++++++
.../conquery/quarkus/api/QueryResourceTest.java | 8 ++++++++
2 files changed, 19 insertions(+)
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
index 6ba8d32898..d6199adfa4 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
@@ -4,6 +4,7 @@
import java.util.List;
import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@@ -46,6 +47,16 @@ public QueryResponse getQuery(@PathParam("queryId") String queryId) {
);
}
+ @POST
+ @Path("/{queryId}/cancel")
+ @Operation(
+ summary = "Cancel a query",
+ description = "Cancels a running query."
+ )
+ public void cancelQuery(@PathParam("queryId") String queryId) {
+ // Stub endpoint for migration progress. Query cancellation wiring follows later.
+ }
+
public record QueryResponse(
String id,
String label,
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/QueryResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/QueryResourceTest.java
index e8e6504b02..2c9a53b082 100644
--- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/QueryResourceTest.java
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/QueryResourceTest.java
@@ -22,4 +22,12 @@ void queryEndpointRespondsWithNewStatus() {
.body("queryType", equalTo("CONCEPT_QUERY"))
.body("createdAt", notNullValue());
}
+
+ @Test
+ void cancelQueryEndpointResponds() {
+ given()
+ .when().post("/api/queries/example-query/cancel")
+ .then()
+ .statusCode(204);
+ }
}
From aa1cc6b87142708d1cdd52eeedfd708f92d29791 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 24 Mar 2026 11:32:58 +0100
Subject: [PATCH 12/45] adds concept endpoint
---
.../conquery/quarkus/api/ConceptResource.java | 124 ++++++++++++++++++
.../quarkus/api/DatasetsResource.java | 5 +-
.../quarkus/api/ConceptResourceTest.java | 31 +++++
.../quarkus/api/DatasetsResourceTest.java | 2 +-
frontend/.env.example | 25 ----
frontend/package-lock.json | 16 ++-
6 files changed, 171 insertions(+), 32 deletions(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/ConceptResourceTest.java
delete mode 100644 frontend/.env.example
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
new file mode 100644
index 0000000000..6859cb53c6
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
@@ -0,0 +1,124 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.util.List;
+import java.util.Map;
+
+import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.openapi.annotations.Operation;
+
+@Path("/api/concepts")
+@Produces(MediaType.APPLICATION_JSON)
+public class ConceptResource {
+
+ @Inject
+ DatasetsRuntimeConfig datasetsConfig;
+
+ @GET
+ @Path("/{conceptId}")
+ @Operation(
+ summary = "Get concept details",
+ description = "Returns a concept map keyed by concept id, compatible with frontend tree loading."
+ )
+ public Map getConcept(@PathParam("conceptId") String conceptId) {
+ DatasetsRuntimeConfig.DatasetEntry dataset = datasetsConfig.datasets()
+ .stream()
+ .filter(entry -> entry.id().equals(conceptId))
+ .findFirst()
+ .orElseThrow(() -> new NotFoundException("Unknown concept: " + conceptId));
+
+ ConceptNodeResponse node = new ConceptNodeResponse(
+ dataset.label(),
+ null,
+ true,
+ List.of(),
+ 0L,
+ 0L,
+ true,
+ false,
+ List.of(),
+ List.of()
+ );
+
+ return Map.of(conceptId, node);
+ }
+
+ public record ConceptNodeResponse(
+ String label,
+ String description,
+ Boolean active,
+ List children,
+ Long matchingEntries,
+ Long matchingEntities,
+ Boolean detailsAvailable,
+ Boolean codeListResolvable,
+ List tables,
+ List selects
+ ) {
+ }
+
+ public record TableResponse(
+ String id,
+ String connectorId,
+ String label,
+ Boolean exclude,
+ @JsonProperty("default")
+ Boolean defaultSelected,
+ List filters,
+ List selects,
+ List supportedSecondaryIds,
+ DateColumnResponse dateColumn
+ ) {
+ }
+
+ public record SelectResponse(
+ String id,
+ String label,
+ String description,
+ @JsonProperty("default")
+ Boolean defaultSelected,
+ SelectResultTypeResponse resultType
+ ) {
+ }
+
+ public record SelectResultTypeResponse(
+ String type,
+ ElementTypeResponse elementType
+ ) {
+ }
+
+ public record ElementTypeResponse(
+ String type
+ ) {
+ }
+
+ public record FilterResponse(
+ String id,
+ String label,
+ String description,
+ String tooltip,
+ String type
+ ) {
+ }
+
+ public record DateColumnResponse(
+ List options,
+ String defaultValue,
+ String value,
+ String tooltip
+ ) {
+ }
+
+ public record ValueResponse(
+ String value,
+ String label
+ ) {
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index 2bfd8f4636..3c4f4ce580 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -70,8 +70,7 @@ public EntityPreviewResponse getEntityPreview(@PathParam("datasetId") String dat
@Path("/{datasetId}/concepts")
@Operation(
summary = "Get root concepts for a dataset",
- description = "Nodes with detailsAvailable=false represent folder/structure nodes and should not be draggable. "
- + "Their active flag should be false."
+ description = "Returns top-level concept nodes. Nodes with detailsAvailable=false represent folder/structure nodes."
)
public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
DatasetsRuntimeConfig.DatasetEntry dataset = requireDataset(datasetId);
@@ -83,7 +82,7 @@ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
List.of(),
0L,
0L,
- false,
+ true,
false
);
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/ConceptResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/ConceptResourceTest.java
new file mode 100644
index 0000000000..6e00999b55
--- /dev/null
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/ConceptResourceTest.java
@@ -0,0 +1,31 @@
+package com.bakdata.conquery.quarkus.api;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.equalTo;
+
+import io.quarkus.test.junit.QuarkusTest;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class ConceptResourceTest {
+
+ @Test
+ void conceptEndpointReturnsConceptMap() {
+ given()
+ .when().get("/api/concepts/imdb")
+ .then()
+ .statusCode(200)
+ .body("imdb.label", equalTo("IMDb"))
+ .body("imdb.detailsAvailable", equalTo(true))
+ .body("imdb.tables.size()", equalTo(0))
+ .body("imdb.selects.size()", equalTo(0));
+ }
+
+ @Test
+ void conceptEndpointReturns404ForUnknownConcept() {
+ given()
+ .when().get("/api/concepts/unknown")
+ .then()
+ .statusCode(404);
+ }
+}
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
index c05d6b59df..983ae2a3cf 100644
--- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/DatasetsResourceTest.java
@@ -53,7 +53,7 @@ void conceptsEndpointRespondsWithFrontendCompatibleShape() {
.statusCode(200)
.body("secondaryIds.size()", equalTo(0))
.body("concepts.imdb.label", equalTo("IMDb"))
- .body("concepts.imdb.detailsAvailable", equalTo(false))
+ .body("concepts.imdb.detailsAvailable", equalTo(true))
.body("concepts.imdb.children.size()", equalTo(0));
}
diff --git a/frontend/.env.example b/frontend/.env.example
deleted file mode 100644
index f3763b5599..0000000000
--- a/frontend/.env.example
+++ /dev/null
@@ -1,25 +0,0 @@
-########################################
-# If you add / remove env variables, make sure to also adjust
-# - the Dockerfile
-# - the ./scripts/replace-env-at-runtime.sh script
-# - the ./src/js/environment/index.ts file
-########################################
-
-PORT=80
-
-REACT_APP_API_URL=http://localhost:8001
-REACT_APP_DISABLE_LOGIN=false
-
-# to host the app under a relative path
-REACT_APP_BASENAME=
-
-# also possible: en
-REACT_APP_LANG=de
-
-# Using keycloak as the Identity Provider (IDP)
-REACT_APP_IDP_ENABLE=false
-REACT_APP_IDP_URL=http://localhost:8080/auth
-# NOTE: You will need to create this realm in keycloak
-REACT_APP_IDP_REALM=Myrealm
-# NOTE: You will need to create this client in keycloak
-REACT_APP_IDP_CLIENT_ID=frontend
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 906e790200..3a1ea97dd7 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -3230,7 +3230,7 @@
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
"integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==",
- "devOptional": true,
+ "dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -3272,6 +3272,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -3288,6 +3289,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -3304,6 +3306,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3320,6 +3323,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -3336,6 +3340,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -3352,6 +3357,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -3368,6 +3374,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -3384,6 +3391,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -3400,6 +3408,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -3416,6 +3425,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -3429,7 +3439,7 @@
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
@@ -3445,7 +3455,7 @@
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
From 9feeb77ffd084d46fa7f0eb120b2a9f32f72c3f7 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 7 Apr 2026 09:42:39 +0200
Subject: [PATCH 13/45] adds discriminators for polymorph query nodes
---
.../quarkus/api/DatasetsResource.java | 2 +-
.../conquery/quarkus/api/MeResource.java | 2 +-
.../conquery/quarkus/api/QueryResource.java | 296 ++++++++++++++++--
.../quarkus/api/QuerySubmissionPayload.java | 216 +++++++++++++
4 files changed, 482 insertions(+), 34 deletions(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QuerySubmissionPayload.java
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index 3c4f4ce580..e943cceb79 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -120,7 +120,7 @@ public List getQueries(@PathParam("datasetId") String data
summary = "Create a query for a dataset",
description = "Accepts a query payload and returns the created query id."
)
- public StartQueryResponse postQueries(@PathParam("datasetId") String datasetId, JsonNode payload) {
+ public StartQueryResponse postQueries(@PathParam("datasetId") String datasetId, QuerySubmissionPayload payload) {
requireDataset(datasetId);
return new StartQueryResponse(java.util.UUID.randomUUID().toString());
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/MeResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/MeResource.java
index c5cabb1092..7a007e80df 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/MeResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/MeResource.java
@@ -24,7 +24,7 @@ public MeResponse me() {
return new MeResponse(
userName,
true,
- Map.of(),
+ Map.of("imdb", new PermissionFlags(true, true, true)),
List.of()
);
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
index d6199adfa4..3c19d3ebe5 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
@@ -3,6 +3,8 @@
import java.time.Instant;
import java.util.List;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@@ -10,6 +12,8 @@
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
+import org.eclipse.microprofile.openapi.annotations.media.DiscriminatorMapping;
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
@Path("/api/queries")
@Produces(MediaType.APPLICATION_JSON)
@@ -22,7 +26,7 @@ public class QueryResource {
description = "Returns the current query execution status and metadata."
)
public QueryResponse getQuery(@PathParam("queryId") String queryId) {
- return new QueryResponse(
+ return new NewQueryResponse(
queryId,
"Query " + queryId,
Instant.now().toString(),
@@ -35,15 +39,7 @@ public QueryResponse getQuery(@PathParam("queryId") String queryId) {
"anonymous",
List.of(),
false,
- List.of(),
- "NEW",
- null,
- null,
- null,
- null,
- "CONCEPT_QUERY",
- 0L,
- false
+ List.of()
);
}
@@ -57,28 +53,264 @@ public void cancelQuery(@PathParam("queryId") String queryId) {
// Stub endpoint for migration progress. Query cancellation wiring follows later.
}
- public record QueryResponse(
- String id,
- String label,
- String createdAt,
- boolean own,
- boolean shared,
- boolean system,
- List tags,
- Object query,
- String secondaryId,
- String ownerName,
- List groups,
- boolean canExpand,
- List availableSecondaryIds,
- String status,
- Double progress,
- String error,
- Long numberOfResults,
- List resultUrls,
- String queryType,
- long requiredTime,
- boolean containsDates
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "status", visible = true)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = NewQueryResponse.class, name = "NEW"),
+ @JsonSubTypes.Type(value = RunningQueryResponse.class, name = "RUNNING"),
+ @JsonSubTypes.Type(value = DoneQueryResponse.class, name = "DONE"),
+ @JsonSubTypes.Type(value = FailedQueryResponse.class, name = "FAILED"),
+ @JsonSubTypes.Type(value = CanceledQueryResponse.class, name = "CANCELED")
+ })
+ @Schema(
+ description = "Query response with status-based polymorphism.",
+ discriminatorProperty = "status",
+ oneOf = {
+ NewQueryResponse.class,
+ RunningQueryResponse.class,
+ DoneQueryResponse.class,
+ FailedQueryResponse.class,
+ CanceledQueryResponse.class
+ },
+ discriminatorMapping = {
+ @DiscriminatorMapping(value = "NEW", schema = NewQueryResponse.class),
+ @DiscriminatorMapping(value = "RUNNING", schema = RunningQueryResponse.class),
+ @DiscriminatorMapping(value = "DONE", schema = DoneQueryResponse.class),
+ @DiscriminatorMapping(value = "FAILED", schema = FailedQueryResponse.class),
+ @DiscriminatorMapping(value = "CANCELED", schema = CanceledQueryResponse.class)
+ }
+ )
+ public abstract static class QueryResponse {
+ public final String id;
+ public final String label;
+ public final String createdAt;
+ public final boolean own;
+ public final boolean shared;
+ public final boolean system;
+ public final List tags;
+ public final QueryDefinition query;
+ public final String secondaryId;
+ public final String ownerName;
+ public final List groups;
+ public final boolean canExpand;
+ public final List availableSecondaryIds;
+ public final QueryStatus status;
+
+ protected QueryResponse(
+ String id,
+ String label,
+ String createdAt,
+ boolean own,
+ boolean shared,
+ boolean system,
+ List tags,
+ QueryDefinition query,
+ String secondaryId,
+ String ownerName,
+ List groups,
+ boolean canExpand,
+ List availableSecondaryIds,
+ QueryStatus status
+ ) {
+ this.id = id;
+ this.label = label;
+ this.createdAt = createdAt;
+ this.own = own;
+ this.shared = shared;
+ this.system = system;
+ this.tags = tags;
+ this.query = query;
+ this.secondaryId = secondaryId;
+ this.ownerName = ownerName;
+ this.groups = groups;
+ this.canExpand = canExpand;
+ this.availableSecondaryIds = availableSecondaryIds;
+ this.status = status;
+ }
+ }
+
+ public static final class NewQueryResponse extends QueryResponse {
+ public NewQueryResponse(
+ String id,
+ String label,
+ String createdAt,
+ boolean own,
+ boolean shared,
+ boolean system,
+ List tags,
+ QueryDefinition query,
+ String secondaryId,
+ String ownerName,
+ List groups,
+ boolean canExpand,
+ List availableSecondaryIds
+ ) {
+ super(id, label, createdAt, own, shared, system, tags, query, secondaryId, ownerName, groups, canExpand, availableSecondaryIds, QueryStatus.NEW);
+ }
+ }
+
+ public static final class RunningQueryResponse extends QueryResponse {
+ public final Double progress;
+
+ public RunningQueryResponse(
+ String id,
+ String label,
+ String createdAt,
+ boolean own,
+ boolean shared,
+ boolean system,
+ List tags,
+ QueryDefinition query,
+ String secondaryId,
+ String ownerName,
+ List groups,
+ boolean canExpand,
+ List availableSecondaryIds,
+ Double progress
+ ) {
+ super(id, label, createdAt, own, shared, system, tags, query, secondaryId, ownerName, groups, canExpand, availableSecondaryIds, QueryStatus.RUNNING);
+ this.progress = progress;
+ }
+ }
+
+ public static final class DoneQueryResponse extends QueryResponse {
+ public final Long numberOfResults;
+ public final List resultUrls;
+ public final QueryType queryType;
+ public final long requiredTime;
+ public final boolean containsDates;
+
+ public DoneQueryResponse(
+ String id,
+ String label,
+ String createdAt,
+ boolean own,
+ boolean shared,
+ boolean system,
+ List tags,
+ QueryDefinition query,
+ String secondaryId,
+ String ownerName,
+ List groups,
+ boolean canExpand,
+ List availableSecondaryIds,
+ Long numberOfResults,
+ List resultUrls,
+ QueryType queryType,
+ long requiredTime,
+ boolean containsDates
+ ) {
+ super(id, label, createdAt, own, shared, system, tags, query, secondaryId, ownerName, groups, canExpand, availableSecondaryIds, QueryStatus.DONE);
+ this.numberOfResults = numberOfResults;
+ this.resultUrls = resultUrls;
+ this.queryType = queryType;
+ this.requiredTime = requiredTime;
+ this.containsDates = containsDates;
+ }
+ }
+
+ public static final class FailedQueryResponse extends QueryResponse {
+ public final ErrorResponse error;
+
+ public FailedQueryResponse(
+ String id,
+ String label,
+ String createdAt,
+ boolean own,
+ boolean shared,
+ boolean system,
+ List tags,
+ QueryDefinition query,
+ String secondaryId,
+ String ownerName,
+ List groups,
+ boolean canExpand,
+ List availableSecondaryIds,
+ ErrorResponse error
+ ) {
+ super(id, label, createdAt, own, shared, system, tags, query, secondaryId, ownerName, groups, canExpand, availableSecondaryIds, QueryStatus.FAILED);
+ this.error = error;
+ }
+ }
+
+ public static final class CanceledQueryResponse extends QueryResponse {
+ public final ErrorResponse error;
+
+ public CanceledQueryResponse(
+ String id,
+ String label,
+ String createdAt,
+ boolean own,
+ boolean shared,
+ boolean system,
+ List tags,
+ QueryDefinition query,
+ String secondaryId,
+ String ownerName,
+ List groups,
+ boolean canExpand,
+ List availableSecondaryIds,
+ ErrorResponse error
+ ) {
+ super(id, label, createdAt, own, shared, system, tags, query, secondaryId, ownerName, groups, canExpand, availableSecondaryIds, QueryStatus.CANCELED);
+ this.error = error;
+ }
+ }
+
+ public enum QueryStatus {
+ NEW,
+ RUNNING,
+ DONE,
+ FAILED,
+ CANCELED
+ }
+
+ public enum QueryType {
+ CONCEPT_QUERY,
+ SECONDARY_ID_QUERY
+ }
+
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = ConceptQueryDefinition.class, name = "CONCEPT_QUERY"),
+ @JsonSubTypes.Type(value = SecondaryIdQueryDefinition.class, name = "SECONDARY_ID_QUERY")
+ })
+ @Schema(
+ description = "Submitted query definition, discriminated by `type`.",
+ discriminatorProperty = "type",
+ oneOf = {ConceptQueryDefinition.class, SecondaryIdQueryDefinition.class},
+ discriminatorMapping = {
+ @DiscriminatorMapping(value = "CONCEPT_QUERY", schema = ConceptQueryDefinition.class),
+ @DiscriminatorMapping(value = "SECONDARY_ID_QUERY", schema = SecondaryIdQueryDefinition.class)
+ }
+ )
+ public abstract static class QueryDefinition {
+ public final QueryType type;
+ public final QuerySubmissionPayload.QueryNode root;
+
+ protected QueryDefinition(QueryType type, QuerySubmissionPayload.QueryNode root) {
+ this.type = type;
+ this.root = root;
+ }
+ }
+
+ public static final class ConceptQueryDefinition extends QueryDefinition {
+ public ConceptQueryDefinition(QuerySubmissionPayload.QueryNode root) {
+ super(QueryType.CONCEPT_QUERY, root);
+ }
+ }
+
+ public static final class SecondaryIdQueryDefinition extends QueryDefinition {
+ public final String secondaryId;
+
+ public SecondaryIdQueryDefinition(QuerySubmissionPayload.QueryNode root, String secondaryId) {
+ super(QueryType.SECONDARY_ID_QUERY, root);
+ this.secondaryId = secondaryId;
+ }
+ }
+
+ public record ErrorResponse(
+ String message,
+ String code
) {
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QuerySubmissionPayload.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QuerySubmissionPayload.java
new file mode 100644
index 0000000000..603ade0346
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QuerySubmissionPayload.java
@@ -0,0 +1,216 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.eclipse.microprofile.openapi.annotations.media.DiscriminatorMapping;
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
+
+public class QuerySubmissionPayload {
+ public String type;
+ public String secondaryId;
+ @Schema(description = "Query root node. Polymorphic and discriminated by field `type`.")
+ public QueryNode root;
+ public Map values;
+
+ private final Map additionalProperties = new HashMap<>();
+
+ @JsonAnySetter
+ public void setAdditionalProperty(String name, Object value) {
+ additionalProperties.put(name, value);
+ }
+
+ @JsonAnyGetter
+ public Map getAdditionalProperties() {
+ return additionalProperties;
+ }
+
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = AndNode.class, name = "AND"),
+ @JsonSubTypes.Type(value = OrNode.class, name = "OR"),
+ @JsonSubTypes.Type(value = NegationNode.class, name = "NEGATION"),
+ @JsonSubTypes.Type(value = DateRestrictionNode.class, name = "DATE_RESTRICTION"),
+ @JsonSubTypes.Type(value = ConceptNode.class, name = "CONCEPT"),
+ @JsonSubTypes.Type(value = SavedQueryNode.class, name = "SAVED_QUERY"),
+ @JsonSubTypes.Type(value = TemporalNode.class, name = "TEMPORAL"),
+ @JsonSubTypes.Type(value = ExternalResolvedNode.class, name = "EXTERNAL_RESOLVED")
+ })
+ @Schema(
+ description = "Base query node. Concrete schema is selected via discriminator `type`.",
+ discriminatorProperty = "type",
+ oneOf = {
+ AndNode.class,
+ OrNode.class,
+ NegationNode.class,
+ DateRestrictionNode.class,
+ ConceptNode.class,
+ SavedQueryNode.class,
+ TemporalNode.class,
+ ExternalResolvedNode.class
+ },
+ discriminatorMapping = {
+ @DiscriminatorMapping(value = "AND", schema = AndNode.class),
+ @DiscriminatorMapping(value = "OR", schema = OrNode.class),
+ @DiscriminatorMapping(value = "NEGATION", schema = NegationNode.class),
+ @DiscriminatorMapping(value = "DATE_RESTRICTION", schema = DateRestrictionNode.class),
+ @DiscriminatorMapping(value = "CONCEPT", schema = ConceptNode.class),
+ @DiscriminatorMapping(value = "SAVED_QUERY", schema = SavedQueryNode.class),
+ @DiscriminatorMapping(value = "TEMPORAL", schema = TemporalNode.class),
+ @DiscriminatorMapping(value = "EXTERNAL_RESOLVED", schema = ExternalResolvedNode.class)
+ }
+ )
+ public abstract static class QueryNode {
+ }
+
+ @Schema(name = "AndQueryNode")
+ public static class AndNode extends QueryNode {
+ public List children;
+ }
+
+ @Schema(name = "OrQueryNode")
+ public static class OrNode extends QueryNode {
+ public List children;
+ }
+
+ @Schema(name = "NegationQueryNode")
+ public static class NegationNode extends QueryNode {
+ public QueryNode child;
+ }
+
+ @Schema(name = "DateRestrictionQueryNode")
+ public static class DateRestrictionNode extends QueryNode {
+ public DateRangePayload dateRange;
+ public QueryNode child;
+ }
+
+ @Schema(name = "ConceptQueryNode")
+ public static class ConceptNode extends QueryNode {
+ public List ids;
+ public String label;
+ public Boolean excludeFromTimeAggregation;
+ public Boolean excludeFromSecondaryId;
+ public List tables;
+ public List selects;
+ }
+
+ @Schema(name = "SavedQueryNode")
+ public static class SavedQueryNode extends QueryNode {
+ public String query;
+ }
+
+ @Schema(name = "TemporalQueryNode")
+ public static class TemporalNode extends QueryNode {
+ public TemporalModePayload mode;
+ public QueryNode index;
+ public String indexSelector;
+ public QueryNode compare;
+ public String compareSelector;
+ }
+
+ @Schema(name = "ExternalResolvedQueryNode")
+ public static class ExternalResolvedNode extends QueryNode {
+ }
+
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = BeforeTemporalModePayload.class, name = "BEFORE"),
+ @JsonSubTypes.Type(value = AfterTemporalModePayload.class, name = "AFTER"),
+ @JsonSubTypes.Type(value = WhileTemporalModePayload.class, name = "WHILE")
+ })
+ @Schema(
+ description = "Temporal mode, discriminated by `type`.",
+ discriminatorProperty = "type",
+ oneOf = {
+ BeforeTemporalModePayload.class,
+ AfterTemporalModePayload.class,
+ WhileTemporalModePayload.class
+ },
+ discriminatorMapping = {
+ @DiscriminatorMapping(value = "BEFORE", schema = BeforeTemporalModePayload.class),
+ @DiscriminatorMapping(value = "AFTER", schema = AfterTemporalModePayload.class),
+ @DiscriminatorMapping(value = "WHILE", schema = WhileTemporalModePayload.class)
+ }
+ )
+ public abstract static class TemporalModePayload {
+ public TemporalModeType type;
+
+ protected TemporalModePayload() {
+ }
+
+ protected TemporalModePayload(TemporalModeType type) {
+ this.type = type;
+ }
+ }
+
+ public static final class BeforeTemporalModePayload extends TemporalModePayload {
+ public DayRangePayload days;
+
+ public BeforeTemporalModePayload() {
+ super(TemporalModeType.BEFORE);
+ }
+
+ public BeforeTemporalModePayload(DayRangePayload days) {
+ this();
+ this.days = days;
+ }
+ }
+
+ public static final class AfterTemporalModePayload extends TemporalModePayload {
+ public DayRangePayload days;
+
+ public AfterTemporalModePayload() {
+ super(TemporalModeType.AFTER);
+ }
+
+ public AfterTemporalModePayload(DayRangePayload days) {
+ this();
+ this.days = days;
+ }
+ }
+
+ public static final class WhileTemporalModePayload extends TemporalModePayload {
+ public WhileTemporalModePayload() {
+ super(TemporalModeType.WHILE);
+ }
+ }
+
+ public enum TemporalModeType {
+ BEFORE,
+ AFTER,
+ WHILE
+ }
+
+ public static class DayRangePayload {
+ public Integer min;
+ public Integer max;
+ }
+
+ public static class DateRangePayload {
+ public LocalDate min;
+ public LocalDate max;
+ }
+
+ public static class TableConfigPayload {
+ public String id;
+ public DateColumnConfigPayload dateColumn;
+ public List selects;
+ public List filters;
+ }
+
+ public static class DateColumnConfigPayload {
+ public String value;
+ }
+
+ public static class FilterConfigPayload {
+ public String filter;
+ public String type;
+ public Object value;
+ }
+}
From 0f4568426bdd3d136de2404c8e6743770cca76d5 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 7 Apr 2026 09:56:39 +0200
Subject: [PATCH 14/45] adds query service
---
.../quarkus/api/DatasetsResource.java | 26 +-
.../conquery/quarkus/api/QueryResource.java | 22 +-
.../quarkus/api/QueryStateService.java | 312 ++++++++++++++++++
3 files changed, 341 insertions(+), 19 deletions(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index e943cceb79..8c8077d3ab 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -11,6 +11,8 @@
import com.bakdata.conquery.quarkus.api.config.FormQueriesRuntimeConfig;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.security.identity.SecurityIdentity;
+import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
@@ -36,6 +38,12 @@ public class DatasetsResource {
@Inject
ObjectMapper objectMapper;
+ @Inject
+ QueryStateService queryStateService;
+
+ @Inject
+ Instance identity;
+
@GET
public List getDatasets() {
return datasetsConfig.datasets()
@@ -111,7 +119,7 @@ public List getFormQueries(@PathParam("datasetId") String datasetId) {
)
public List getQueries(@PathParam("datasetId") String datasetId) {
requireDataset(datasetId);
- return List.of();
+ return queryStateService.getDatasetQueries(datasetId);
}
@POST
@@ -122,7 +130,21 @@ public List getQueries(@PathParam("datasetId") String data
)
public StartQueryResponse postQueries(@PathParam("datasetId") String datasetId, QuerySubmissionPayload payload) {
requireDataset(datasetId);
- return new StartQueryResponse(java.util.UUID.randomUUID().toString());
+ return queryStateService.createQuery(datasetId, payload, resolveUserName(identity));
+ }
+
+ private static String resolveUserName(Instance identityInstance) {
+ if (identityInstance.isResolvable()) {
+ SecurityIdentity securityIdentity = identityInstance.get();
+ if (securityIdentity != null && !securityIdentity.isAnonymous() && securityIdentity.getPrincipal() != null) {
+ String principalName = securityIdentity.getPrincipal().getName();
+ if (principalName != null && !principalName.isBlank()) {
+ return principalName;
+ }
+ }
+ }
+
+ return "anonymous";
}
private DatasetsRuntimeConfig.DatasetEntry requireDataset(String datasetId) {
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
index 3c19d3ebe5..d663a96d56 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
@@ -1,10 +1,10 @@
package com.bakdata.conquery.quarkus.api;
-import java.time.Instant;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@@ -18,6 +18,8 @@
@Path("/api/queries")
@Produces(MediaType.APPLICATION_JSON)
public class QueryResource {
+ @Inject
+ QueryStateService queryStateService;
@GET
@Path("/{queryId}")
@@ -26,21 +28,7 @@ public class QueryResource {
description = "Returns the current query execution status and metadata."
)
public QueryResponse getQuery(@PathParam("queryId") String queryId) {
- return new NewQueryResponse(
- queryId,
- "Query " + queryId,
- Instant.now().toString(),
- true,
- false,
- false,
- List.of(),
- null,
- null,
- "anonymous",
- List.of(),
- false,
- List.of()
- );
+ return queryStateService.getQuery(queryId);
}
@POST
@@ -50,7 +38,7 @@ public QueryResponse getQuery(@PathParam("queryId") String queryId) {
description = "Cancels a running query."
)
public void cancelQuery(@PathParam("queryId") String queryId) {
- // Stub endpoint for migration progress. Query cancellation wiring follows later.
+ queryStateService.cancelQuery(queryId);
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "status", visible = true)
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
new file mode 100644
index 0000000000..daaebfa382
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
@@ -0,0 +1,312 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.NotFoundException;
+
+@ApplicationScoped
+public class QueryStateService {
+
+ private final Map queriesById = new ConcurrentHashMap<>();
+
+ public DatasetsResource.StartQueryResponse createQuery(String datasetId, QuerySubmissionPayload payload, String ownerName) {
+ String queryId = UUID.randomUUID().toString();
+ Instant createdAt = Instant.now();
+ QueryResource.QueryType queryType = resolveQueryType(payload);
+ QueryResource.QueryDefinition definition = toQueryDefinition(payload, queryType);
+
+ StoredQuery stored = new StoredQuery(
+ queryId,
+ datasetId,
+ labelFor(queryId),
+ createdAt,
+ ownerName,
+ definition,
+ payload != null ? payload.secondaryId : null,
+ containsDates(payload != null ? payload.root : null),
+ QueryResource.QueryStatus.NEW
+ );
+
+ queriesById.put(queryId, stored);
+ return new DatasetsResource.StartQueryResponse(queryId);
+ }
+
+ public List getDatasetQueries(String datasetId) {
+ return queriesById.values()
+ .stream()
+ .filter(query -> query.datasetId().equals(datasetId))
+ .sorted(Comparator.comparing(StoredQuery::createdAt).reversed())
+ .map(this::toSummaryResponse)
+ .toList();
+ }
+
+ public QueryResource.QueryResponse getQuery(String queryId) {
+ StoredQuery query = requireQuery(queryId);
+ return toQueryResponse(query);
+ }
+
+ public void cancelQuery(String queryId) {
+ StoredQuery query = requireQuery(queryId);
+ if (query.status() == QueryResource.QueryStatus.NEW || query.status() == QueryResource.QueryStatus.RUNNING) {
+ query.setStatus(QueryResource.QueryStatus.CANCELED);
+ }
+ }
+
+ private DatasetsResource.QuerySummaryResponse toSummaryResponse(StoredQuery query) {
+ return new DatasetsResource.QuerySummaryResponse(
+ query.id(),
+ query.label(),
+ query.status() == QueryResource.QueryStatus.DONE ? 0L : null,
+ query.createdAt().toString(),
+ List.of(),
+ true,
+ query.ownerName(),
+ false,
+ List.of(),
+ false,
+ false,
+ query.definition().type.name(),
+ query.secondaryId(),
+ query.containsDates()
+ );
+ }
+
+ private QueryResource.QueryResponse toQueryResponse(StoredQuery query) {
+ return switch (query.status()) {
+ case RUNNING -> new QueryResource.RunningQueryResponse(
+ query.id(),
+ query.label(),
+ query.createdAt().toString(),
+ true,
+ false,
+ false,
+ List.of(),
+ query.definition(),
+ query.secondaryId(),
+ query.ownerName(),
+ List.of(),
+ false,
+ List.of(),
+ null
+ );
+ case DONE -> new QueryResource.DoneQueryResponse(
+ query.id(),
+ query.label(),
+ query.createdAt().toString(),
+ true,
+ false,
+ false,
+ List.of(),
+ query.definition(),
+ query.secondaryId(),
+ query.ownerName(),
+ List.of(),
+ false,
+ List.of(),
+ 0L,
+ List.of(),
+ query.definition().type,
+ 0L,
+ query.containsDates()
+ );
+ case FAILED -> new QueryResource.FailedQueryResponse(
+ query.id(),
+ query.label(),
+ query.createdAt().toString(),
+ true,
+ false,
+ false,
+ List.of(),
+ query.definition(),
+ query.secondaryId(),
+ query.ownerName(),
+ List.of(),
+ false,
+ List.of(),
+ new QueryResource.ErrorResponse("Query failed", "FAILED")
+ );
+ case CANCELED -> new QueryResource.CanceledQueryResponse(
+ query.id(),
+ query.label(),
+ query.createdAt().toString(),
+ true,
+ false,
+ false,
+ List.of(),
+ query.definition(),
+ query.secondaryId(),
+ query.ownerName(),
+ List.of(),
+ false,
+ List.of(),
+ new QueryResource.ErrorResponse("Query canceled", "CANCELED")
+ );
+ case NEW -> new QueryResource.NewQueryResponse(
+ query.id(),
+ query.label(),
+ query.createdAt().toString(),
+ true,
+ false,
+ false,
+ List.of(),
+ query.definition(),
+ query.secondaryId(),
+ query.ownerName(),
+ List.of(),
+ false,
+ List.of()
+ );
+ };
+ }
+
+ private QueryResource.QueryDefinition toQueryDefinition(QuerySubmissionPayload payload, QueryResource.QueryType queryType) {
+ QuerySubmissionPayload.QueryNode root = payload != null ? payload.root : null;
+ return switch (queryType) {
+ case SECONDARY_ID_QUERY -> new QueryResource.SecondaryIdQueryDefinition(root, payload != null ? payload.secondaryId : null);
+ case CONCEPT_QUERY -> new QueryResource.ConceptQueryDefinition(root);
+ };
+ }
+
+ private QueryResource.QueryType resolveQueryType(QuerySubmissionPayload payload) {
+ if (payload != null && payload.type != null) {
+ try {
+ return QueryResource.QueryType.valueOf(payload.type.toUpperCase(Locale.ROOT));
+ }
+ catch (IllegalArgumentException ignored) {
+ // Fall through to heuristic handling.
+ }
+ }
+ if (payload != null && payload.secondaryId != null && !payload.secondaryId.isBlank()) {
+ return QueryResource.QueryType.SECONDARY_ID_QUERY;
+ }
+ return QueryResource.QueryType.CONCEPT_QUERY;
+ }
+
+ private boolean containsDates(QuerySubmissionPayload.QueryNode node) {
+ if (node == null) {
+ return false;
+ }
+ if (node instanceof QuerySubmissionPayload.DateRestrictionNode || node instanceof QuerySubmissionPayload.TemporalNode) {
+ return true;
+ }
+ if (node instanceof QuerySubmissionPayload.AndNode andNode) {
+ return anyContainsDates(andNode.children);
+ }
+ if (node instanceof QuerySubmissionPayload.OrNode orNode) {
+ return anyContainsDates(orNode.children);
+ }
+ if (node instanceof QuerySubmissionPayload.NegationNode negationNode) {
+ return containsDates(negationNode.child);
+ }
+ if (node instanceof QuerySubmissionPayload.DateRestrictionNode dateRestrictionNode) {
+ return containsDates(dateRestrictionNode.child);
+ }
+ return false;
+ }
+
+ private boolean anyContainsDates(List children) {
+ if (children == null || children.isEmpty()) {
+ return false;
+ }
+ for (QuerySubmissionPayload.QueryNode child : new ArrayList<>(children)) {
+ if (containsDates(child)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private StoredQuery requireQuery(String queryId) {
+ StoredQuery query = queriesById.get(queryId);
+ if (query == null) {
+ throw new NotFoundException("Unknown query: " + queryId);
+ }
+ return query;
+ }
+
+ private static String labelFor(String queryId) {
+ return "Query " + queryId;
+ }
+
+ private static final class StoredQuery {
+ private final String id;
+ private final String datasetId;
+ private final String label;
+ private final Instant createdAt;
+ private final String ownerName;
+ private final QueryResource.QueryDefinition definition;
+ private final String secondaryId;
+ private final boolean containsDates;
+ private volatile QueryResource.QueryStatus status;
+
+ private StoredQuery(
+ String id,
+ String datasetId,
+ String label,
+ Instant createdAt,
+ String ownerName,
+ QueryResource.QueryDefinition definition,
+ String secondaryId,
+ boolean containsDates,
+ QueryResource.QueryStatus status
+ ) {
+ this.id = id;
+ this.datasetId = datasetId;
+ this.label = label;
+ this.createdAt = createdAt;
+ this.ownerName = ownerName;
+ this.definition = definition;
+ this.secondaryId = secondaryId;
+ this.containsDates = containsDates;
+ this.status = status;
+ }
+
+ public String id() {
+ return id;
+ }
+
+ public String datasetId() {
+ return datasetId;
+ }
+
+ public String label() {
+ return label;
+ }
+
+ public Instant createdAt() {
+ return createdAt;
+ }
+
+ public String ownerName() {
+ return ownerName;
+ }
+
+ public QueryResource.QueryDefinition definition() {
+ return definition;
+ }
+
+ public String secondaryId() {
+ return secondaryId;
+ }
+
+ public boolean containsDates() {
+ return containsDates;
+ }
+
+ public QueryResource.QueryStatus status() {
+ return status;
+ }
+
+ public void setStatus(QueryResource.QueryStatus status) {
+ this.status = status;
+ }
+ }
+}
From 4d0048b9af64c3abe6d916a6c26c017fa7277fc1 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 7 Apr 2026 11:24:01 +0200
Subject: [PATCH 15/45] wire query lifecycle and concept API
---
.../conquery/quarkus/api/ConceptResource.java | 17 +++---
.../quarkus/api/ConceptsResponse.java | 4 +-
.../quarkus/api/DatasetsResource.java | 53 ++++++++++++++-----
.../api/config/ConceptsRuntimeConfig.java | 18 +++++++
.../main/resources/application-dev.properties | 6 ++-
.../src/main/resources/application.properties | 4 ++
6 files changed, 79 insertions(+), 23 deletions(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
index 6859cb53c6..37dbbc2c87 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
@@ -3,11 +3,10 @@
import java.util.List;
import java.util.Map;
-import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
+import com.bakdata.conquery.quarkus.api.config.ConceptsRuntimeConfig;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
-import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@@ -19,7 +18,7 @@
public class ConceptResource {
@Inject
- DatasetsRuntimeConfig datasetsConfig;
+ ConceptsRuntimeConfig conceptsConfig;
@GET
@Path("/{conceptId}")
@@ -28,14 +27,14 @@ public class ConceptResource {
description = "Returns a concept map keyed by concept id, compatible with frontend tree loading."
)
public Map getConcept(@PathParam("conceptId") String conceptId) {
- DatasetsRuntimeConfig.DatasetEntry dataset = datasetsConfig.datasets()
- .stream()
- .filter(entry -> entry.id().equals(conceptId))
- .findFirst()
- .orElseThrow(() -> new NotFoundException("Unknown concept: " + conceptId));
+ ConceptsRuntimeConfig.ConceptEntry concept = conceptsConfig.concepts()
+ .stream()
+ .filter(entry -> entry.id().equals(conceptId))
+ .findFirst()
+ .orElse(null);
ConceptNodeResponse node = new ConceptNodeResponse(
- dataset.label(),
+ concept != null ? concept.label() : conceptId,
null,
true,
List.of(),
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptsResponse.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptsResponse.java
index 3cbc05a87b..2b16d28804 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptsResponse.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptsResponse.java
@@ -28,7 +28,9 @@ public record ConceptSummaryResponse(
@Schema(description = "Whether detailed concept contents are available via /api/concepts/{conceptId}. "
+ "If false, this node is a folder/structure node and should not be draggable.")
Boolean detailsAvailable,
- Boolean codeListResolvable
+ Boolean codeListResolvable,
+ List tables,
+ List selects
) {
}
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index 8c8077d3ab..144ab03d2c 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -9,6 +9,7 @@
import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
import com.bakdata.conquery.quarkus.api.config.EntityPreviewRuntimeConfig;
import com.bakdata.conquery.quarkus.api.config.FormQueriesRuntimeConfig;
+import com.bakdata.conquery.quarkus.api.config.ConceptsRuntimeConfig;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.security.identity.SecurityIdentity;
@@ -38,6 +39,9 @@ public class DatasetsResource {
@Inject
ObjectMapper objectMapper;
+ @Inject
+ ConceptsRuntimeConfig conceptsConfig;
+
@Inject
QueryStateService queryStateService;
@@ -81,22 +85,47 @@ public EntityPreviewResponse getEntityPreview(@PathParam("datasetId") String dat
description = "Returns top-level concept nodes. Nodes with detailsAvailable=false represent folder/structure nodes."
)
public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
- DatasetsRuntimeConfig.DatasetEntry dataset = requireDataset(datasetId);
+ requireDataset(datasetId);
- ConceptsResponse.ConceptSummaryResponse rootConcept = new ConceptsResponse.ConceptSummaryResponse(
- dataset.label(),
- null,
- true,
- List.of(),
- 0L,
- 0L,
- true,
- false
- );
+ java.util.Map concepts = conceptsConfig.concepts()
+ .stream()
+ .filter(entry -> entry.dataset().equals(datasetId))
+ .collect(java.util.stream.Collectors.toMap(
+ ConceptsRuntimeConfig.ConceptEntry::id,
+ entry -> new ConceptsResponse.ConceptSummaryResponse(
+ entry.label(),
+ null,
+ true,
+ List.of(),
+ 0L,
+ 0L,
+ true,
+ false,
+ List.of(),
+ List.of()
+ ),
+ (left, right) -> left,
+ java.util.LinkedHashMap::new
+ ));
+
+ if (concepts.isEmpty()) {
+ concepts.put(datasetId, new ConceptsResponse.ConceptSummaryResponse(
+ datasetId,
+ null,
+ true,
+ List.of(),
+ 0L,
+ 0L,
+ true,
+ false,
+ List.of(),
+ List.of()
+ ));
+ }
return new ConceptsResponse(
List.of(),
- java.util.Map.of(dataset.id(), rootConcept)
+ concepts
);
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java
new file mode 100644
index 0000000000..57077cd9c4
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java
@@ -0,0 +1,18 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import java.util.List;
+
+import io.smallrye.config.ConfigMapping;
+
+@ConfigMapping(prefix = "conquery")
+public interface ConceptsRuntimeConfig {
+ List concepts();
+
+ interface ConceptEntry {
+ String id();
+
+ String label();
+
+ String dataset();
+ }
+}
diff --git a/backend-quarkus/src/main/resources/application-dev.properties b/backend-quarkus/src/main/resources/application-dev.properties
index bf9a1eee0e..22647cfe57 100644
--- a/backend-quarkus/src/main/resources/application-dev.properties
+++ b/backend-quarkus/src/main/resources/application-dev.properties
@@ -10,4 +10,8 @@ conquery.frontend.query-upload.ids[0].print=true
conquery.datasets[0].id=imdb
conquery.datasets[0].label=IMDb
conquery.datasets[1].id=empty-set
-conquery.datasets[1].label=Empty Dataset
\ No newline at end of file
+conquery.datasets[1].label=Empty Dataset
+
+conquery.concepts[0].id=imdb
+conquery.concepts[0].label=IMDb
+conquery.concepts[0].dataset=imdb
diff --git a/backend-quarkus/src/main/resources/application.properties b/backend-quarkus/src/main/resources/application.properties
index 3dddf7d1aa..e99689ea2f 100644
--- a/backend-quarkus/src/main/resources/application.properties
+++ b/backend-quarkus/src/main/resources/application.properties
@@ -9,6 +9,10 @@ conquery.datasets[0].label=IMDb
conquery.datasets[1].id=empty-set
conquery.datasets[1].label=Empty Dataset
+conquery.concepts[0].id=imdb
+conquery.concepts[0].label=IMDb
+conquery.concepts[0].dataset=imdb
+
conquery.entity-preview.all[0].name=imdb:entities
conquery.entity-preview.all[0].label=Entities
conquery.entity-preview.default[0].name=imdb:entities
From 0f606bf6ba9659f0b5b879bb138b4cb557d23f36 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 7 Apr 2026 14:48:21 +0200
Subject: [PATCH 16/45] adds concept id resolve
---
backend-quarkus/pom.xml | 4 +
.../conquery/quarkus/api/ConceptResource.java | 48 ++++++++--
.../conquery/quarkus/api/DatasetService.java | 87 +++++++++++++++++++
.../quarkus/api/DatasetsResource.java | 83 ++++++------------
4 files changed, 155 insertions(+), 67 deletions(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
diff --git a/backend-quarkus/pom.xml b/backend-quarkus/pom.xml
index 77fa66324c..d543150962 100644
--- a/backend-quarkus/pom.xml
+++ b/backend-quarkus/pom.xml
@@ -61,6 +61,10 @@
io.quarkus
quarkus-smallrye-health
+
+ io.quarkus
+ quarkus-hibernate-validator
+
io.quarkus
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
index 37dbbc2c87..8eba6639bb 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
@@ -3,10 +3,15 @@
import java.util.List;
import java.util.Map;
-import com.bakdata.conquery.quarkus.api.config.ConceptsRuntimeConfig;
+import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.inject.Inject;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@@ -18,7 +23,7 @@
public class ConceptResource {
@Inject
- ConceptsRuntimeConfig conceptsConfig;
+ DatasetService datasetService;
@GET
@Path("/{conceptId}")
@@ -26,15 +31,11 @@ public class ConceptResource {
summary = "Get concept details",
description = "Returns a concept map keyed by concept id, compatible with frontend tree loading."
)
- public Map getConcept(@PathParam("conceptId") String conceptId) {
- ConceptsRuntimeConfig.ConceptEntry concept = conceptsConfig.concepts()
- .stream()
- .filter(entry -> entry.id().equals(conceptId))
- .findFirst()
- .orElse(null);
+ public Map getConcept(@PathParam("conceptId") @NotBlank String conceptId) {
+ var concept = datasetService.requireConcept(conceptId);
ConceptNodeResponse node = new ConceptNodeResponse(
- concept != null ? concept.label() : conceptId,
+ concept.label(),
null,
true,
List.of(),
@@ -49,6 +50,20 @@ public Map getConcept(@PathParam("conceptId") Strin
return Map.of(conceptId, node);
}
+ @POST
+ @Path("/{conceptId}/resolve")
+ @Operation(
+ summary = "Resolve concept codes",
+ description = "Resolves uploaded concept codes to concept ids for the same dataset as the requested root concept."
+ )
+ public ConceptResolveResponse resolveConceptCodes(
+ @PathParam("conceptId") @NotBlank String conceptId,
+ @Valid @NotNull ConceptCodeList payload
+ ) {
+ DatasetService.ConceptCodeResolution resolution = datasetService.resolveConceptCodes(conceptId, payload.concepts);
+ return new ConceptResolveResponse(resolution.resolvedConcepts(), resolution.unknownCodes());
+ }
+
public record ConceptNodeResponse(
String label,
String description,
@@ -120,4 +135,19 @@ public record ValueResponse(
String label
) {
}
+
+ public record ConceptResolveResponse(
+ List resolvedConcepts,
+ List unknownCodes
+ ) {
+ }
+
+ public static final class ConceptCodeList {
+ public final @NotNull @NotEmpty List<@NotBlank String> concepts;
+
+ @JsonCreator
+ public ConceptCodeList(@JsonProperty("concepts") List concepts) {
+ this.concepts = concepts;
+ }
+ }
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
new file mode 100644
index 0000000000..27e4138a20
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
@@ -0,0 +1,87 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.bakdata.conquery.quarkus.api.config.ConceptsRuntimeConfig;
+import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.NotFoundException;
+
+@ApplicationScoped
+public class DatasetService {
+
+ @Inject
+ DatasetsRuntimeConfig datasetsConfig;
+
+ @Inject
+ ConceptsRuntimeConfig conceptsConfig;
+
+ public List listDatasets() {
+ return datasetsConfig.datasets();
+ }
+
+ public DatasetsRuntimeConfig.DatasetEntry requireDataset(String datasetId) {
+ return datasetsConfig.datasets()
+ .stream()
+ .filter(dataset -> dataset.id().equals(datasetId))
+ .findFirst()
+ .orElseThrow(() -> new NotFoundException("Unknown dataset: " + datasetId));
+ }
+
+ public ConceptsRuntimeConfig.ConceptEntry requireConcept(String conceptId) {
+ return conceptsConfig.concepts()
+ .stream()
+ .filter(concept -> concept.id().equals(conceptId))
+ .findFirst()
+ .orElseThrow(() -> new NotFoundException("Unknown concept: " + conceptId));
+ }
+
+ public List listConceptsForDataset(String datasetId) {
+ return conceptsConfig.concepts()
+ .stream()
+ .filter(concept -> concept.dataset().equals(datasetId))
+ .toList();
+ }
+
+ public ConceptCodeResolution resolveConceptCodes(String rootConceptId, List codes) {
+ ConceptsRuntimeConfig.ConceptEntry rootConcept = requireConcept(rootConceptId);
+
+ Map lookupByCode = conceptsConfig.concepts().stream()
+ .filter(concept -> concept.dataset().equals(rootConcept.dataset()))
+ .flatMap(concept -> java.util.stream.Stream.of(
+ Map.entry(normalizeCode(concept.id()), concept.id()),
+ Map.entry(normalizeCode(concept.label()), concept.id())
+ ))
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue,
+ (existing, ignored) -> existing
+ ));
+
+ List resolvedInOrder = codes.stream()
+ .map(this::normalizeCode)
+ .map(lookupByCode::get)
+ .filter(java.util.Objects::nonNull)
+ .toList();
+ List unknownCodes = codes.stream()
+ .filter(code -> !lookupByCode.containsKey(normalizeCode(code)))
+ .toList();
+
+ return new ConceptCodeResolution(List.copyOf(new LinkedHashSet<>(resolvedInOrder)), unknownCodes);
+ }
+
+ private String normalizeCode(String value) {
+ return value.trim().toLowerCase(Locale.ROOT);
+ }
+
+ public record ConceptCodeResolution(
+ List resolvedConcepts,
+ List unknownCodes
+ ) {
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index 144ab03d2c..ba0c448c1e 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -3,13 +3,12 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Stream;
-import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
import com.bakdata.conquery.quarkus.api.config.EntityPreviewRuntimeConfig;
import com.bakdata.conquery.quarkus.api.config.FormQueriesRuntimeConfig;
-import com.bakdata.conquery.quarkus.api.config.ConceptsRuntimeConfig;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.security.identity.SecurityIdentity;
@@ -17,7 +16,6 @@
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
-import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@@ -28,7 +26,7 @@
@Produces(MediaType.APPLICATION_JSON)
public class DatasetsResource {
@Inject
- DatasetsRuntimeConfig datasetsConfig;
+ DatasetService datasetService;
@Inject
EntityPreviewRuntimeConfig entityPreviewConfig;
@@ -39,9 +37,6 @@ public class DatasetsResource {
@Inject
ObjectMapper objectMapper;
- @Inject
- ConceptsRuntimeConfig conceptsConfig;
-
@Inject
QueryStateService queryStateService;
@@ -50,8 +45,7 @@ public class DatasetsResource {
@GET
public List getDatasets() {
- return datasetsConfig.datasets()
- .stream()
+ return datasetService.listDatasets().stream()
.map(entry -> new DatasetResponse(entry.id(), entry.label()))
.toList();
}
@@ -59,7 +53,7 @@ public List getDatasets() {
@GET
@Path("/{datasetId}/entity-preview")
public EntityPreviewResponse getEntityPreview(@PathParam("datasetId") String datasetId) {
- requireDataset(datasetId);
+ datasetService.requireDataset(datasetId);
List allSources =
entityPreviewConfig.allSources().stream().map(source -> new EntityPreviewResponse.LabeledSource(source.name(), source.label())).toList();
@@ -85,43 +79,24 @@ public EntityPreviewResponse getEntityPreview(@PathParam("datasetId") String dat
description = "Returns top-level concept nodes. Nodes with detailsAvailable=false represent folder/structure nodes."
)
public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
- requireDataset(datasetId);
-
- java.util.Map concepts = conceptsConfig.concepts()
- .stream()
- .filter(entry -> entry.dataset().equals(datasetId))
- .collect(java.util.stream.Collectors.toMap(
- ConceptsRuntimeConfig.ConceptEntry::id,
- entry -> new ConceptsResponse.ConceptSummaryResponse(
- entry.label(),
- null,
- true,
- List.of(),
- 0L,
- 0L,
- true,
- false,
- List.of(),
- List.of()
- ),
- (left, right) -> left,
- java.util.LinkedHashMap::new
- ));
-
- if (concepts.isEmpty()) {
- concepts.put(datasetId, new ConceptsResponse.ConceptSummaryResponse(
- datasetId,
- null,
- true,
- List.of(),
- 0L,
- 0L,
- true,
- false,
- List.of(),
- List.of()
- ));
- }
+ datasetService.requireDataset(datasetId);
+
+ java.util.Map concepts = new LinkedHashMap<>();
+ datasetService.listConceptsForDataset(datasetId).forEach(entry -> concepts.put(
+ entry.id(),
+ new ConceptsResponse.ConceptSummaryResponse(
+ entry.label(),
+ null,
+ true,
+ List.of(),
+ 0L,
+ 0L,
+ true,
+ false,
+ List.of(),
+ List.of()
+ )
+ ));
return new ConceptsResponse(
List.of(),
@@ -136,7 +111,7 @@ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
description = "Returns raw frontend form configuration objects."
)
public List getFormQueries(@PathParam("datasetId") String datasetId) {
- requireDataset(datasetId);
+ datasetService.requireDataset(datasetId);
return formQueriesConfig.resources().stream().map(this::loadFormResource).toList();
}
@@ -147,7 +122,7 @@ public List getFormQueries(@PathParam("datasetId") String datasetId) {
description = "Returns the query history list for the given dataset."
)
public List getQueries(@PathParam("datasetId") String datasetId) {
- requireDataset(datasetId);
+ datasetService.requireDataset(datasetId);
return queryStateService.getDatasetQueries(datasetId);
}
@@ -158,7 +133,7 @@ public List getQueries(@PathParam("datasetId") String data
description = "Accepts a query payload and returns the created query id."
)
public StartQueryResponse postQueries(@PathParam("datasetId") String datasetId, QuerySubmissionPayload payload) {
- requireDataset(datasetId);
+ datasetService.requireDataset(datasetId);
return queryStateService.createQuery(datasetId, payload, resolveUserName(identity));
}
@@ -176,14 +151,6 @@ private static String resolveUserName(Instance identityInstanc
return "anonymous";
}
- private DatasetsRuntimeConfig.DatasetEntry requireDataset(String datasetId) {
- return datasetsConfig.datasets()
- .stream()
- .filter(dataset -> dataset.id().equals(datasetId))
- .findFirst()
- .orElseThrow(() -> new NotFoundException("Unknown dataset: " + datasetId));
- }
-
private JsonNode loadFormResource(String path) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try (InputStream input = classLoader.getResourceAsStream(path)) {
From b069464938e09e03b36b32450e9d7607d24c243a Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 7 Apr 2026 15:31:52 +0200
Subject: [PATCH 17/45] adds filter service
---
.../conquery/quarkus/api/FilterResource.java | 128 ++++++++++++++++++
.../conquery/quarkus/api/FilterService.java | 99 ++++++++++++++
2 files changed, 227 insertions(+)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FilterResource.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FilterService.java
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FilterResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FilterResource.java
new file mode 100644
index 0000000000..03c2f3cd7a
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FilterResource.java
@@ -0,0 +1,128 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalInt;
+
+import jakarta.inject.Inject;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.openapi.annotations.Operation;
+
+@Path("/api/filters")
+@Produces(MediaType.APPLICATION_JSON)
+public class FilterResource {
+
+ @Inject
+ FilterService filterService;
+
+ @POST
+ @Path("/{filterId}/resolve")
+ @Operation(
+ summary = "Resolve uploaded filter values",
+ description = "Resolves a list of raw values for a given filter id."
+ )
+ public FilterResolveResponse resolve(
+ @PathParam("filterId") @NotBlank String filterId,
+ @Valid @NotNull FilterValues payload
+ ) {
+ FilterService.FilterResolveResult result = filterService.resolveFilterValues(filterId, payload.values);
+ return new FilterResolveResponse(
+ new ResolvedFilterResponse(
+ result.resolvedFilter().tableId(),
+ result.resolvedFilter().filterId(),
+ result.resolvedFilter().value().stream()
+ .map(value -> new ResolvedFilterValueResponse(value.label(), value.value(), value.optionValue()))
+ .toList()
+ ),
+ result.unknownCodes()
+ );
+ }
+
+ @POST
+ @Path("/{filterId}/autocomplete")
+ @Operation(
+ summary = "Autocomplete filter values",
+ description = "Returns autocomplete suggestions for a filter."
+ )
+ public AutocompleteResponse autocomplete(
+ @PathParam("filterId") @NotBlank String filterId,
+ @Valid @NotNull AutocompleteRequest payload
+ ) {
+ FilterService.FilterAutocompleteResult result = filterService.autocomplete(filterId, payload.text, payload.page, payload.pageSize);
+ return new AutocompleteResponse(
+ result.total(),
+ result.values().stream()
+ .map(value -> new AutocompleteValueResponse(
+ value.label(),
+ value.value(),
+ value.optionValue(),
+ value.templateValues(),
+ value.disabled()
+ ))
+ .toList()
+ );
+ }
+
+ public static final class FilterValues {
+ public final @NotNull @NotEmpty List<@NotBlank String> values;
+
+ public FilterValues(List values) {
+ this.values = values;
+ }
+ }
+
+ public static final class AutocompleteRequest {
+ public final @NotNull Optional text;
+ public final @NotNull OptionalInt page;
+ public final @NotNull OptionalInt pageSize;
+
+ public AutocompleteRequest(Optional text, OptionalInt page, OptionalInt pageSize) {
+ this.text = text;
+ this.page = page;
+ this.pageSize = pageSize;
+ }
+ }
+
+ public record FilterResolveResponse(
+ ResolvedFilterResponse resolvedFilter,
+ List unknownCodes
+ ) {
+ }
+
+ public record ResolvedFilterResponse(
+ String tableId,
+ String filterId,
+ List value
+ ) {
+ }
+
+ public record ResolvedFilterValueResponse(
+ String label,
+ String value,
+ String optionValue
+ ) {
+ }
+
+ public record AutocompleteResponse(
+ long total,
+ List values
+ ) {
+ }
+
+ public record AutocompleteValueResponse(
+ String label,
+ String value,
+ String optionValue,
+ java.util.Map templateValues,
+ boolean disabled
+ ) {
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FilterService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FilterService.java
new file mode 100644
index 0000000000..2d77f1cd59
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FilterService.java
@@ -0,0 +1,99 @@
+package com.bakdata.conquery.quarkus.api;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalInt;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.BadRequestException;
+
+@ApplicationScoped
+public class FilterService {
+
+ public FilterResolveResult resolveFilterValues(String filterId, List values) {
+ String tableId = extractTableId(filterId);
+ List resolved = values.stream()
+ .map(String::trim)
+ .filter(value -> !value.isEmpty())
+ .distinct()
+ .map(value -> new ResolvedFilterValue(value, value, value))
+ .toList();
+
+ return new FilterResolveResult(
+ new ResolvedFilter(tableId, filterId, resolved),
+ List.of()
+ );
+ }
+
+ public FilterAutocompleteResult autocomplete(String filterId, Optional text, OptionalInt page, OptionalInt pageSize) {
+ int resolvedPage = page.orElse(0);
+ int resolvedPageSize = pageSize.orElse(50);
+ if (resolvedPage < 0) {
+ throw new BadRequestException("page must be >= 0");
+ }
+ if (resolvedPageSize < 1) {
+ throw new BadRequestException("pageSize must be >= 1");
+ }
+
+ String prefix = text.map(String::trim).orElse("");
+ List candidates = List.of(prefix, prefix + "1", prefix + "2", prefix + "3")
+ .stream()
+ .map(String::trim)
+ .filter(value -> !value.isEmpty())
+ .collect(java.util.stream.Collectors.collectingAndThen(
+ java.util.stream.Collectors.toCollection(LinkedHashSet::new),
+ List::copyOf
+ ));
+
+ int fromIndex = Math.min(resolvedPage * resolvedPageSize, candidates.size());
+ int toIndex = Math.min(fromIndex + resolvedPageSize, candidates.size());
+ List values = candidates.subList(fromIndex, toIndex)
+ .stream()
+ .map(value -> new AutocompleteValue(value, value, value, Map.of(), false))
+ .toList();
+
+ return new FilterAutocompleteResult(values, candidates.size());
+ }
+
+ private String extractTableId(String filterId) {
+ int separatorIndex = filterId.indexOf(':');
+ return separatorIndex > 0 ? filterId.substring(0, separatorIndex) : filterId;
+ }
+
+ public record FilterResolveResult(
+ ResolvedFilter resolvedFilter,
+ List unknownCodes
+ ) {
+ }
+
+ public record ResolvedFilter(
+ String tableId,
+ String filterId,
+ List value
+ ) {
+ }
+
+ public record ResolvedFilterValue(
+ String label,
+ String value,
+ String optionValue
+ ) {
+ }
+
+ public record FilterAutocompleteResult(
+ List values,
+ long total
+ ) {
+ }
+
+ public record AutocompleteValue(
+ String label,
+ String value,
+ String optionValue,
+ Map templateValues,
+ boolean disabled
+ ) {
+ }
+}
From eea24405744a8b2c62fc8a643cdeb78a4ad55517 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 7 Apr 2026 15:56:02 +0200
Subject: [PATCH 18/45] smaller openapi fixes
---
.../conquery/quarkus/api/DatasetsResource.java | 15 ++++-----------
.../conquery/quarkus/api/QueryResource.java | 17 ++++++++++-------
2 files changed, 14 insertions(+), 18 deletions(-)
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index ba0c448c1e..8ce1396eb1 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -9,7 +9,6 @@
import com.bakdata.conquery.quarkus.api.config.EntityPreviewRuntimeConfig;
import com.bakdata.conquery.quarkus.api.config.FormQueriesRuntimeConfig;
-import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.inject.Instance;
@@ -110,7 +109,7 @@ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
summary = "Get form configurations for a dataset",
description = "Returns raw frontend form configuration objects."
)
- public List getFormQueries(@PathParam("datasetId") String datasetId) {
+ public List
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
io.quarkus
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java
index 324e1e66f7..0705ca0e06 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java
@@ -47,8 +47,8 @@ public List list(
return formConfigRepository.listByDataset(datasetId).stream()
.filter(config -> requestedFormTypes == null
|| requestedFormTypes.isEmpty()
- || requestedFormTypes.contains(config.formType()))
- .sorted((left, right) -> right.createdAt().compareTo(left.createdAt()))
+ || requestedFormTypes.contains(config.getFormType()))
+ .sorted((left, right) -> right.getCreatedAt().compareTo(left.getCreatedAt()))
.map(config -> toOverview(config, requester))
.toList();
}
@@ -87,31 +87,31 @@ public void delete(String formConfigId) {
private DatasetFormConfigResource.FormConfigOverviewResponse toOverview(StoredFormConfig config, String requester) {
return new DatasetFormConfigResource.FormConfigOverviewResponse(
- config.id(),
- config.formType(),
- config.label(),
- config.tags(),
- config.ownerName(),
- config.createdAt().toString(),
- config.ownerName().equals(requester),
- !config.groups().isEmpty(),
- config.system()
+ config.getId(),
+ config.getFormType(),
+ config.getLabel(),
+ config.getTags(),
+ config.getOwnerName(),
+ config.getCreatedAt().toString(),
+ config.getOwnerName().equals(requester),
+ !config.getGroups().isEmpty(),
+ config.isSystem()
);
}
private FormConfigResource.FormConfigFullResponse toFull(StoredFormConfig config, String requester) {
return new FormConfigResource.FormConfigFullResponse(
- config.id(),
- config.formType(),
- config.label(),
- config.tags(),
- config.ownerName(),
- config.createdAt().toString(),
- config.ownerName().equals(requester),
- !config.groups().isEmpty(),
- config.system(),
- config.groups(),
- config.values()
+ config.getId(),
+ config.getFormType(),
+ config.getLabel(),
+ config.getTags(),
+ config.getOwnerName(),
+ config.getCreatedAt().toString(),
+ config.getOwnerName().equals(requester),
+ !config.getGroups().isEmpty(),
+ config.isSystem(),
+ config.getGroups(),
+ config.getValues()
);
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
index 802363f8c8..12b501fdbf 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
@@ -46,7 +46,7 @@ public DatasetsResource.StartQueryResponse createQuery(String datasetId, QuerySu
public List getDatasetQueries(String datasetId) {
return queryRepository.listByDataset(datasetId).stream()
- .sorted(Comparator.comparing(StoredQuery::createdAt).reversed())
+ .sorted(Comparator.comparing(StoredQuery::getCreatedAt).reversed())
.map(this::toSummaryResponse)
.toList();
}
@@ -58,7 +58,7 @@ public QueryResource.QueryResponse getQuery(String queryId) {
public void cancelQuery(String queryId) {
StoredQuery query = requireQuery(queryId);
- if (query.status() == QueryResource.QueryStatus.NEW || query.status() == QueryResource.QueryStatus.RUNNING) {
+ if (query.getStatus() == QueryResource.QueryStatus.NEW || query.getStatus() == QueryResource.QueryStatus.RUNNING) {
query.setStatus(QueryResource.QueryStatus.CANCELED);
}
}
@@ -87,105 +87,105 @@ public void deleteQuery(String queryId) {
private DatasetsResource.QuerySummaryResponse toSummaryResponse(StoredQuery query) {
return new DatasetsResource.QuerySummaryResponse(
- query.id(),
- query.label(),
- query.status() == QueryResource.QueryStatus.DONE ? 0L : null,
- query.createdAt().toString(),
- query.tags(),
+ query.getId(),
+ query.getLabel(),
+ query.getStatus() == QueryResource.QueryStatus.DONE ? 0L : null,
+ query.getCreatedAt().toString(),
+ query.getTags(),
true,
- query.ownerName(),
+ query.getOwnerName(),
false,
List.of(),
- query.shared(),
+ query.isShared(),
false,
- query.definition().type.name(),
- query.secondaryId(),
- query.containsDates()
+ query.getDefinition().type.name(),
+ query.getSecondaryId(),
+ query.isContainsDates()
);
}
private QueryResource.QueryResponse toQueryResponse(StoredQuery query) {
- return switch (query.status()) {
+ return switch (query.getStatus()) {
case RUNNING -> new QueryResource.RunningQueryResponse(
- query.id(),
- query.label(),
- query.createdAt().toString(),
+ query.getId(),
+ query.getLabel(),
+ query.getCreatedAt().toString(),
true,
- query.shared(),
+ query.isShared(),
false,
- query.tags(),
- query.definition(),
- query.secondaryId(),
- query.ownerName(),
- query.groups(),
+ query.getTags(),
+ query.getDefinition(),
+ query.getSecondaryId(),
+ query.getOwnerName(),
+ query.getGroups(),
false,
List.of(),
null
);
case DONE -> new QueryResource.DoneQueryResponse(
- query.id(),
- query.label(),
- query.createdAt().toString(),
+ query.getId(),
+ query.getLabel(),
+ query.getCreatedAt().toString(),
true,
- query.shared(),
+ query.isShared(),
false,
- query.tags(),
- query.definition(),
- query.secondaryId(),
- query.ownerName(),
- query.groups(),
+ query.getTags(),
+ query.getDefinition(),
+ query.getSecondaryId(),
+ query.getOwnerName(),
+ query.getGroups(),
false,
List.of(),
0L,
List.of(),
- query.definition().type,
+ query.getDefinition().type,
0L,
- query.containsDates()
+ query.isContainsDates()
);
case FAILED -> new QueryResource.FailedQueryResponse(
- query.id(),
- query.label(),
- query.createdAt().toString(),
+ query.getId(),
+ query.getLabel(),
+ query.getCreatedAt().toString(),
true,
- query.shared(),
+ query.isShared(),
false,
- query.tags(),
- query.definition(),
- query.secondaryId(),
- query.ownerName(),
- query.groups(),
+ query.getTags(),
+ query.getDefinition(),
+ query.getSecondaryId(),
+ query.getOwnerName(),
+ query.getGroups(),
false,
List.of(),
new QueryResource.ErrorResponse("Query failed", "FAILED")
);
case CANCELED -> new QueryResource.CanceledQueryResponse(
- query.id(),
- query.label(),
- query.createdAt().toString(),
+ query.getId(),
+ query.getLabel(),
+ query.getCreatedAt().toString(),
true,
- query.shared(),
+ query.isShared(),
false,
- query.tags(),
- query.definition(),
- query.secondaryId(),
- query.ownerName(),
- query.groups(),
+ query.getTags(),
+ query.getDefinition(),
+ query.getSecondaryId(),
+ query.getOwnerName(),
+ query.getGroups(),
false,
List.of(),
new QueryResource.ErrorResponse("Query canceled", "CANCELED")
);
case NEW -> new QueryResource.NewQueryResponse(
- query.id(),
- query.label(),
- query.createdAt().toString(),
+ query.getId(),
+ query.getLabel(),
+ query.getCreatedAt().toString(),
true,
- query.shared(),
+ query.isShared(),
false,
- query.tags(),
- query.definition(),
- query.secondaryId(),
- query.ownerName(),
- query.groups(),
+ query.getTags(),
+ query.getDefinition(),
+ query.getSecondaryId(),
+ query.getOwnerName(),
+ query.getGroups(),
false,
List.of()
);
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java
index 34ab3c5e2b..da9e9db638 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java
@@ -15,7 +15,7 @@ public class InMemoryFormConfigRepository implements FormConfigRepository {
@Override
public void save(StoredFormConfig config) {
- configsById.put(config.id(), config);
+ configsById.put(config.getId(), config);
}
@Override
@@ -25,7 +25,7 @@ public Optional findById(String formConfigId) {
@Override
public List listByDataset(String datasetId) {
- return configsById.values().stream().filter(config -> config.datasetId().equals(datasetId)).toList();
+ return configsById.values().stream().filter(config -> config.getDatasetId().equals(datasetId)).toList();
}
@Override
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java
index 9531e3d3ed..faa65177c3 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java
@@ -15,7 +15,7 @@ public class InMemoryQueryRepository implements QueryRepository {
@Override
public void save(StoredQuery query) {
- queriesById.put(query.id(), query);
+ queriesById.put(query.getId(), query);
}
@Override
@@ -25,7 +25,7 @@ public Optional findById(String queryId) {
@Override
public List listByDataset(String datasetId) {
- return queriesById.values().stream().filter(query -> query.datasetId().equals(datasetId)).toList();
+ return queriesById.values().stream().filter(query -> query.getDatasetId().equals(datasetId)).toList();
}
@Override
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredFormConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredFormConfig.java
index 45fe06cf3b..4a5b3ee5c2 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredFormConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredFormConfig.java
@@ -1,10 +1,16 @@
package com.bakdata.conquery.quarkus.storage.model;
import java.time.Instant;
-import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
public class StoredFormConfig {
private final String id;
private final String datasetId;
@@ -16,84 +22,4 @@ public class StoredFormConfig {
private final boolean system;
private List groups;
private Map values;
-
- public StoredFormConfig(
- String id,
- String datasetId,
- String formType,
- String label,
- List tags,
- String ownerName,
- Instant createdAt,
- boolean system,
- List groups,
- Map values
- ) {
- this.id = id;
- this.datasetId = datasetId;
- this.formType = formType;
- this.label = label;
- this.tags = new ArrayList<>(tags);
- this.ownerName = ownerName;
- this.createdAt = createdAt;
- this.system = system;
- this.groups = new ArrayList<>(groups);
- this.values = values;
- }
-
- public String id() {
- return id;
- }
-
- public String datasetId() {
- return datasetId;
- }
-
- public String formType() {
- return formType;
- }
-
- public String label() {
- return label;
- }
-
- public void setLabel(String label) {
- this.label = label;
- }
-
- public List tags() {
- return tags;
- }
-
- public void setTags(List tags) {
- this.tags = tags;
- }
-
- public String ownerName() {
- return ownerName;
- }
-
- public Instant createdAt() {
- return createdAt;
- }
-
- public boolean system() {
- return system;
- }
-
- public List groups() {
- return groups;
- }
-
- public void setGroups(List groups) {
- this.groups = groups;
- }
-
- public Map values() {
- return values;
- }
-
- public void setValues(Map values) {
- this.values = values;
- }
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredQuery.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredQuery.java
index 6f5ceb2da7..c8e3a6b43a 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredQuery.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredQuery.java
@@ -4,114 +4,24 @@
import java.util.List;
import com.bakdata.conquery.quarkus.api.QueryResource;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+@Getter
+@Setter
+@AllArgsConstructor
public class StoredQuery {
private final String id;
private final String datasetId;
- private volatile String label;
+ private String label;
private final Instant createdAt;
private final String ownerName;
private final QueryResource.QueryDefinition definition;
private final String secondaryId;
private final boolean containsDates;
- private volatile QueryResource.QueryStatus status;
- private volatile List tags;
- private volatile boolean shared;
- private volatile List groups;
-
- public StoredQuery(
- String id,
- String datasetId,
- String label,
- Instant createdAt,
- String ownerName,
- QueryResource.QueryDefinition definition,
- String secondaryId,
- boolean containsDates,
- QueryResource.QueryStatus status,
- List tags,
- boolean shared,
- List groups
- ) {
- this.id = id;
- this.datasetId = datasetId;
- this.label = label;
- this.createdAt = createdAt;
- this.ownerName = ownerName;
- this.definition = definition;
- this.secondaryId = secondaryId;
- this.containsDates = containsDates;
- this.status = status;
- this.tags = tags;
- this.shared = shared;
- this.groups = groups;
- }
-
- public String id() {
- return id;
- }
-
- public String datasetId() {
- return datasetId;
- }
-
- public String label() {
- return label;
- }
-
- public void setLabel(String label) {
- this.label = label;
- }
-
- public Instant createdAt() {
- return createdAt;
- }
-
- public String ownerName() {
- return ownerName;
- }
-
- public QueryResource.QueryDefinition definition() {
- return definition;
- }
-
- public String secondaryId() {
- return secondaryId;
- }
-
- public boolean containsDates() {
- return containsDates;
- }
-
- public QueryResource.QueryStatus status() {
- return status;
- }
-
- public void setStatus(QueryResource.QueryStatus status) {
- this.status = status;
- }
-
- public List tags() {
- return tags;
- }
-
- public void setTags(List tags) {
- this.tags = tags;
- }
-
- public boolean shared() {
- return shared;
- }
-
- public void setShared(boolean shared) {
- this.shared = shared;
- }
-
- public List groups() {
- return groups;
- }
-
- public void setGroups(List groups) {
- this.groups = groups;
- }
+ private QueryResource.QueryStatus status;
+ private List tags;
+ private boolean shared;
+ private List groups;
}
From 6785bce53eaab67e20b5390c96920e59a0f2cfa1 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Thu, 9 Apr 2026 10:41:20 +0200
Subject: [PATCH 27/45] adds meta storage facade
---
.../conquery/quarkus/api/DatasetService.java | 13 +++---
.../quarkus/api/FormConfigService.java | 14 +++----
.../quarkus/api/QueryStateService.java | 12 +++---
.../meta/InMemoryMetaStorageFacade.java | 35 ++++++++++++++++
.../storage/meta/ManagerMetaStorage.java | 14 +++++++
.../storage/meta/XodusMetaStorageFacade.java | 40 +++++++++++++++++++
6 files changed, 109 insertions(+), 19 deletions(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/ManagerMetaStorage.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
index 5cc1d8d184..72cffda93d 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
@@ -7,6 +7,7 @@
import java.util.stream.Collectors;
import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
+import com.bakdata.conquery.quarkus.storage.meta.ManagerMetaStorage;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
@@ -15,31 +16,31 @@
public class DatasetService {
@Inject
- DatasetCatalogRepository datasetCatalog;
+ ManagerMetaStorage metaStorage;
public List listDatasets() {
- return datasetCatalog.listDatasets();
+ return metaStorage.datasets().listDatasets();
}
public DatasetCatalogRepository.DatasetRecord requireDataset(String datasetId) {
- return datasetCatalog.findDataset(datasetId)
+ return metaStorage.datasets().findDataset(datasetId)
.orElseThrow(() -> new NotFoundException("Unknown dataset: " + datasetId));
}
public DatasetCatalogRepository.ConceptRecord requireConcept(String conceptId) {
- return datasetCatalog.findConcept(conceptId)
+ return metaStorage.datasets().findConcept(conceptId)
.orElseThrow(() -> new NotFoundException("Unknown concept: " + conceptId));
}
public List listConceptsForDataset(String datasetId) {
- return datasetCatalog.listConcepts().stream().filter(concept -> concept.datasetId().equals(datasetId))
+ return metaStorage.datasets().listConcepts().stream().filter(concept -> concept.datasetId().equals(datasetId))
.toList();
}
public ConceptCodeResolution resolveConceptCodes(String rootConceptId, List codes) {
DatasetCatalogRepository.ConceptRecord rootConcept = requireConcept(rootConceptId);
- Map lookupByCode = datasetCatalog.listConcepts().stream()
+ Map lookupByCode = metaStorage.datasets().listConcepts().stream()
.filter(concept -> concept.datasetId().equals(rootConcept.datasetId()))
.flatMap(concept -> java.util.stream.Stream.of(
Map.entry(normalizeCode(concept.id()), concept.id()),
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java
index 0705ca0e06..477dc91a54 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java
@@ -5,7 +5,7 @@
import java.util.Set;
import java.util.UUID;
-import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
+import com.bakdata.conquery.quarkus.storage.meta.ManagerMetaStorage;
import com.bakdata.conquery.quarkus.storage.model.StoredFormConfig;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -15,7 +15,7 @@
public class FormConfigService {
@Inject
- FormConfigRepository formConfigRepository;
+ ManagerMetaStorage metaStorage;
public DatasetFormConfigResource.PostFormConfigResponse create(
String datasetId,
@@ -35,7 +35,7 @@ public DatasetFormConfigResource.PostFormConfigResponse create(
List.of(),
payload.values
);
- formConfigRepository.save(stored);
+ metaStorage.formConfigs().save(stored);
return new DatasetFormConfigResource.PostFormConfigResponse(id);
}
@@ -44,7 +44,7 @@ public List list(
Set requestedFormTypes,
String requester
) {
- return formConfigRepository.listByDataset(datasetId).stream()
+ return metaStorage.formConfigs().listByDataset(datasetId).stream()
.filter(config -> requestedFormTypes == null
|| requestedFormTypes.isEmpty()
|| requestedFormTypes.contains(config.getFormType()))
@@ -75,12 +75,12 @@ public FormConfigResource.FormConfigFullResponse patch(
if (payload.values != null) {
config.setValues(payload.values);
}
- formConfigRepository.save(config);
+ metaStorage.formConfigs().save(config);
return toFull(config, requester);
}
public void delete(String formConfigId) {
- if (!formConfigRepository.deleteById(formConfigId)) {
+ if (!metaStorage.formConfigs().deleteById(formConfigId)) {
throw new NotFoundException("Unknown form-config: " + formConfigId);
}
}
@@ -116,6 +116,6 @@ private FormConfigResource.FormConfigFullResponse toFull(StoredFormConfig config
}
private StoredFormConfig require(String formConfigId) {
- return formConfigRepository.findById(formConfigId).orElseThrow(() -> new NotFoundException("Unknown form-config: " + formConfigId));
+ return metaStorage.formConfigs().findById(formConfigId).orElseThrow(() -> new NotFoundException("Unknown form-config: " + formConfigId));
}
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
index 12b501fdbf..50ba02305c 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
@@ -7,7 +7,7 @@
import java.util.Locale;
import java.util.UUID;
-import com.bakdata.conquery.quarkus.storage.QueryRepository;
+import com.bakdata.conquery.quarkus.storage.meta.ManagerMetaStorage;
import com.bakdata.conquery.quarkus.storage.model.StoredQuery;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -17,7 +17,7 @@
public class QueryStateService {
@Inject
- QueryRepository queryRepository;
+ ManagerMetaStorage metaStorage;
public DatasetsResource.StartQueryResponse createQuery(String datasetId, QuerySubmissionPayload payload, String ownerName) {
String queryId = UUID.randomUUID().toString();
@@ -40,12 +40,12 @@ public DatasetsResource.StartQueryResponse createQuery(String datasetId, QuerySu
List.of()
);
- queryRepository.save(stored);
+ metaStorage.queries().save(stored);
return new DatasetsResource.StartQueryResponse(queryId);
}
public List getDatasetQueries(String datasetId) {
- return queryRepository.listByDataset(datasetId).stream()
+ return metaStorage.queries().listByDataset(datasetId).stream()
.sorted(Comparator.comparing(StoredQuery::getCreatedAt).reversed())
.map(this::toSummaryResponse)
.toList();
@@ -80,7 +80,7 @@ public void patchQuery(String queryId, QueryPatch patch) {
}
public void deleteQuery(String queryId) {
- if (!queryRepository.deleteById(queryId)) {
+ if (!metaStorage.queries().deleteById(queryId)) {
throw new NotFoundException("Unknown query: " + queryId);
}
}
@@ -274,7 +274,7 @@ private boolean anyContainsDates(List children
}
private StoredQuery requireQuery(String queryId) {
- return queryRepository.findById(queryId).orElseThrow(() -> new NotFoundException("Unknown query: " + queryId));
+ return metaStorage.queries().findById(queryId).orElseThrow(() -> new NotFoundException("Unknown query: " + queryId));
}
private static String labelFor(String queryId) {
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java
new file mode 100644
index 0000000000..00d2cd16dd
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java
@@ -0,0 +1,35 @@
+package com.bakdata.conquery.quarkus.storage.meta;
+
+import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
+import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
+import com.bakdata.conquery.quarkus.storage.QueryRepository;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class InMemoryMetaStorageFacade implements ManagerMetaStorage {
+
+ @Inject
+ DatasetCatalogRepository datasetCatalogRepository;
+
+ @Inject
+ QueryRepository queryRepository;
+
+ @Inject
+ FormConfigRepository formConfigRepository;
+
+ @Override
+ public DatasetCatalogRepository datasets() {
+ return datasetCatalogRepository;
+ }
+
+ @Override
+ public QueryRepository queries() {
+ return queryRepository;
+ }
+
+ @Override
+ public FormConfigRepository formConfigs() {
+ return formConfigRepository;
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/ManagerMetaStorage.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/ManagerMetaStorage.java
new file mode 100644
index 0000000000..3beedcf9bb
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/ManagerMetaStorage.java
@@ -0,0 +1,14 @@
+package com.bakdata.conquery.quarkus.storage.meta;
+
+import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
+import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
+import com.bakdata.conquery.quarkus.storage.QueryRepository;
+
+public interface ManagerMetaStorage {
+
+ DatasetCatalogRepository datasets();
+
+ QueryRepository queries();
+
+ FormConfigRepository formConfigs();
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java
new file mode 100644
index 0000000000..a5e72f3a21
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java
@@ -0,0 +1,40 @@
+package com.bakdata.conquery.quarkus.storage.meta;
+
+import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
+import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
+import com.bakdata.conquery.quarkus.storage.QueryRepository;
+
+/**
+ * Integration seam for a future Xodus-backed manager meta storage implementation.
+ */
+public class XodusMetaStorageFacade implements ManagerMetaStorage {
+
+ private final DatasetCatalogRepository datasetCatalogRepository;
+ private final QueryRepository queryRepository;
+ private final FormConfigRepository formConfigRepository;
+
+ public XodusMetaStorageFacade(
+ DatasetCatalogRepository datasetCatalogRepository,
+ QueryRepository queryRepository,
+ FormConfigRepository formConfigRepository
+ ) {
+ this.datasetCatalogRepository = datasetCatalogRepository;
+ this.queryRepository = queryRepository;
+ this.formConfigRepository = formConfigRepository;
+ }
+
+ @Override
+ public DatasetCatalogRepository datasets() {
+ return datasetCatalogRepository;
+ }
+
+ @Override
+ public QueryRepository queries() {
+ return queryRepository;
+ }
+
+ @Override
+ public FormConfigRepository formConfigs() {
+ return formConfigRepository;
+ }
+}
From 87d3fd2b965f211a04292a39a4cf6d5ff7c02d4c Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Fri, 17 Apr 2026 12:53:35 +0200
Subject: [PATCH 28/45] adds facades for xodus
---
.gitignore | 1 +
.../quarkus/storage/meta/InMemoryMetaStorageFacade.java | 2 ++
.../quarkus/storage/meta/XodusMetaStorageFacade.java | 6 ++++++
.../src/main/resources/application-dev.properties | 2 ++
backend-quarkus/src/main/resources/application.properties | 3 +++
5 files changed, 14 insertions(+)
diff --git a/.gitignore b/.gitignore
index 59811b6afb..70015e34ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,3 +78,4 @@ tutorial/mimic_iii_demo/data/**
cypress/screenshots/
cypress/videos/
+.codex
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java
index 00d2cd16dd..272021cc2b 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java
@@ -3,10 +3,12 @@
import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
import com.bakdata.conquery.quarkus.storage.QueryRepository;
+import io.quarkus.arc.properties.IfBuildProperty;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "IN_MEMORY", enableIfMissing = true)
public class InMemoryMetaStorageFacade implements ManagerMetaStorage {
@Inject
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java
index a5e72f3a21..d9a8236139 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java
@@ -3,16 +3,22 @@
import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
import com.bakdata.conquery.quarkus.storage.QueryRepository;
+import io.quarkus.arc.properties.IfBuildProperty;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
/**
* Integration seam for a future Xodus-backed manager meta storage implementation.
*/
+@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "XODUS")
public class XodusMetaStorageFacade implements ManagerMetaStorage {
private final DatasetCatalogRepository datasetCatalogRepository;
private final QueryRepository queryRepository;
private final FormConfigRepository formConfigRepository;
+ @Inject
public XodusMetaStorageFacade(
DatasetCatalogRepository datasetCatalogRepository,
QueryRepository queryRepository,
diff --git a/backend-quarkus/src/main/resources/application-dev.properties b/backend-quarkus/src/main/resources/application-dev.properties
index 22647cfe57..8a588d84bb 100644
--- a/backend-quarkus/src/main/resources/application-dev.properties
+++ b/backend-quarkus/src/main/resources/application-dev.properties
@@ -1,6 +1,8 @@
quarkus.http.cors.enabled=true
quarkus.http.cors.origins=/.*/
+conquery.storage.backend=IN_MEMORY
+
conquery.frontend.query-upload.ids[0].name=ID
conquery.frontend.query-upload.ids[0].field=pid
conquery.frontend.query-upload.ids[0].label=result
diff --git a/backend-quarkus/src/main/resources/application.properties b/backend-quarkus/src/main/resources/application.properties
index e99689ea2f..ed5ed7ef77 100644
--- a/backend-quarkus/src/main/resources/application.properties
+++ b/backend-quarkus/src/main/resources/application.properties
@@ -4,6 +4,9 @@ quarkus.smallrye-openapi.path=/q/openapi
quarkus.swagger-ui.always-include=true
quarkus.swagger-ui.path=/q/swagger-ui
+# Build-time selection of manager meta storage backend: IN_MEMORY or XODUS.
+conquery.storage.backend=IN_MEMORY
+
conquery.datasets[0].id=imdb
conquery.datasets[0].label=IMDb
conquery.datasets[1].id=empty-set
From 6b8982a52622fe9cbfc5579bbda3af729d3d89a2 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Thu, 23 Apr 2026 15:20:40 +0200
Subject: [PATCH 29/45] adds xodus implementation
---
backend-quarkus/pom.xml | 5 +
.../conquery/quarkus/api/ConceptResource.java | 56 +++-
.../conquery/quarkus/api/DatasetService.java | 70 +++--
.../api/config/StorageRuntimeConfig.java | 10 +
.../api/config/TablesRuntimeConfig.java | 35 +++
.../ConfigDatasetCatalogRepository.java | 147 +++++++++-
.../storage/DatasetCatalogRepository.java | 51 +++-
.../storage/InMemoryFormConfigRepository.java | 2 +
.../storage/InMemoryQueryRepository.java | 2 +
.../quarkus/storage/NamespaceStorage.java | 25 ++
.../storage/NamespaceStorageRegistry.java | 11 +
.../RepositoryNamespaceStorageRegistry.java | 110 ++++++++
.../meta/InMemoryMetaStorageFacade.java | 9 -
.../storage/meta/ManagerMetaStorage.java | 3 -
.../storage/meta/XodusMetaStorageFacade.java | 9 -
.../xodus/XodusDatasetCatalogRepository.java | 250 ++++++++++++++++++
.../XodusDatasetEnvironmentProvider.java | 109 ++++++++
.../xodus/XodusEnvironmentProvider.java | 38 +++
.../xodus/XodusFormConfigRepository.java | 97 +++++++
.../storage/xodus/XodusQueryRepository.java | 97 +++++++
.../conquery/quarkus/util/ScopedId.java | 20 ++
.../main/resources/application-dev.properties | 15 ++
.../src/main/resources/application.properties | 15 ++
.../quarkus/api/ConceptResourceTest.java | 6 +-
24 files changed, 1138 insertions(+), 54 deletions(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/NamespaceStorage.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/NamespaceStorageRegistry.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/RepositoryNamespaceStorageRegistry.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetCatalogRepository.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetEnvironmentProvider.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusEnvironmentProvider.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusFormConfigRepository.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusQueryRepository.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/util/ScopedId.java
diff --git a/backend-quarkus/pom.xml b/backend-quarkus/pom.xml
index 00b22531bd..cb38a68723 100644
--- a/backend-quarkus/pom.xml
+++ b/backend-quarkus/pom.xml
@@ -71,6 +71,11 @@
${lombok.version}
provided
+
+ org.jetbrains.xodus
+ xodus-environment
+ 1.3.232
+
io.quarkus
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
index 8eba6639bb..b3baf095b0 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
@@ -2,7 +2,10 @@
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
+import com.bakdata.conquery.quarkus.util.ScopedId;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.inject.Inject;
@@ -33,6 +36,7 @@ public class ConceptResource {
)
public Map getConcept(@PathParam("conceptId") @NotBlank String conceptId) {
var concept = datasetService.requireConcept(conceptId);
+ List tables = datasetService.listTablesForDataset(datasetId(concept.id())).stream().map(this::toTableResponse).toList();
ConceptNodeResponse node = new ConceptNodeResponse(
concept.label(),
@@ -43,13 +47,53 @@ public Map getConcept(@PathParam("conceptId") @NotB
0L,
true,
false,
- List.of(),
+ tables,
List.of()
);
return Map.of(conceptId, node);
}
+ private TableResponse toTableResponse(DatasetCatalogRepository.TableRecord table) {
+ List columns = table.columns().stream().map(this::toColumnResponse).toList();
+ List filters = columns.stream()
+ .map(column -> new FilterResponse(column.id(), column.label(), null, null, column.type().name()))
+ .toList();
+ List supportedSecondaryIds = table.columns().stream()
+ .map(DatasetCatalogRepository.ColumnRecord::secondaryId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+
+ return new TableResponse(
+ table.id(),
+ datasetId(table.id()),
+ table.label(),
+ false,
+ true,
+ filters,
+ List.of(),
+ columns,
+ table.primaryColumn(),
+ supportedSecondaryIds,
+ null
+ );
+ }
+
+ private String datasetId(String scopedId) {
+ return ScopedId.extractDatasetId(scopedId)
+ .orElseThrow(() -> new IllegalStateException("Expected dataset-scoped id but got: " + scopedId));
+ }
+
+ private ColumnResponse toColumnResponse(DatasetCatalogRepository.ColumnRecord column) {
+ return new ColumnResponse(
+ column.id(),
+ column.label(),
+ column.type(),
+ column.secondaryId()
+ );
+ }
+
@POST
@Path("/{conceptId}/resolve")
@Operation(
@@ -87,11 +131,21 @@ public record TableResponse(
Boolean defaultSelected,
List filters,
List selects,
+ List columns,
+ String primaryColumn,
List supportedSecondaryIds,
DateColumnResponse dateColumn
) {
}
+ public record ColumnResponse(
+ String id,
+ String label,
+ DatasetCatalogRepository.ColumnType type,
+ String secondaryId
+ ) {
+ }
+
public record SelectResponse(
String id,
String label,
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
index 72cffda93d..6db22dd824 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
@@ -6,8 +6,11 @@
import java.util.Map;
import java.util.stream.Collectors;
+import com.bakdata.conquery.quarkus.api.config.StorageRuntimeConfig;
import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
-import com.bakdata.conquery.quarkus.storage.meta.ManagerMetaStorage;
+import com.bakdata.conquery.quarkus.storage.NamespaceStorage;
+import com.bakdata.conquery.quarkus.storage.NamespaceStorageRegistry;
+import com.bakdata.conquery.quarkus.util.ScopedId;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
@@ -16,41 +19,53 @@
public class DatasetService {
@Inject
- ManagerMetaStorage metaStorage;
+ NamespaceStorageRegistry namespaceStorageRegistry;
+
+ @Inject
+ @SuppressWarnings("unused")
+ StorageRuntimeConfig storageRuntimeConfig;
public List listDatasets() {
- return metaStorage.datasets().listDatasets();
+ return namespaceStorageRegistry.listDatasets();
}
public DatasetCatalogRepository.DatasetRecord requireDataset(String datasetId) {
- return metaStorage.datasets().findDataset(datasetId)
- .orElseThrow(() -> new NotFoundException("Unknown dataset: " + datasetId));
+ return requireNamespace(datasetId).dataset();
}
public DatasetCatalogRepository.ConceptRecord requireConcept(String conceptId) {
- return metaStorage.datasets().findConcept(conceptId)
- .orElseThrow(() -> new NotFoundException("Unknown concept: " + conceptId));
+ NamespaceStorage namespace = requireNamespaceForScopedObject(conceptId, "concept");
+ return namespace.findConcept(conceptId).orElseThrow(() -> new NotFoundException("Unknown concept: " + conceptId));
}
public List listConceptsForDataset(String datasetId) {
- return metaStorage.datasets().listConcepts().stream().filter(concept -> concept.datasetId().equals(datasetId))
- .toList();
+ return requireNamespace(datasetId).listConcepts();
+ }
+
+ public List listTablesForDataset(String datasetId) {
+ return requireNamespace(datasetId).listTables();
+ }
+
+ public DatasetCatalogRepository.TableRecord requireTable(String tableId) {
+ NamespaceStorage namespace = requireNamespaceForScopedObject(tableId, "table");
+ return namespace.findTable(tableId).orElseThrow(() -> new NotFoundException("Unknown table: " + tableId));
}
public ConceptCodeResolution resolveConceptCodes(String rootConceptId, List codes) {
- DatasetCatalogRepository.ConceptRecord rootConcept = requireConcept(rootConceptId);
-
- Map lookupByCode = metaStorage.datasets().listConcepts().stream()
- .filter(concept -> concept.datasetId().equals(rootConcept.datasetId()))
- .flatMap(concept -> java.util.stream.Stream.of(
- Map.entry(normalizeCode(concept.id()), concept.id()),
- Map.entry(normalizeCode(concept.label()), concept.id())
- ))
- .collect(Collectors.toMap(
- Map.Entry::getKey,
- Map.Entry::getValue,
- (existing, ignored) -> existing
- ));
+ requireConcept(rootConceptId);
+ NamespaceStorage namespace = requireNamespaceForScopedObject(rootConceptId, "concept");
+
+ // TODO This does not look right yet: Concept codes need to be resolved against the leaf-children of the provided root concept
+ Map lookupByCode = namespace.listConcepts().stream()
+ .flatMap(concept -> java.util.stream.Stream.of(
+ Map.entry(normalizeCode(concept.id()), concept.id()),
+ Map.entry(normalizeCode(concept.label()), concept.id())
+ ))
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue,
+ (existing, ignored) -> existing
+ ));
List resolvedInOrder = codes.stream()
.map(this::normalizeCode)
@@ -68,6 +83,17 @@ private String normalizeCode(String value) {
return value.trim().toLowerCase(Locale.ROOT);
}
+ private NamespaceStorage requireNamespace(String datasetId) {
+ return namespaceStorageRegistry.findNamespace(datasetId)
+ .orElseThrow(() -> new NotFoundException("Unknown dataset: " + datasetId));
+ }
+
+ private NamespaceStorage requireNamespaceForScopedObject(String scopedId, String objectType) {
+ return ScopedId.extractDatasetId(scopedId)
+ .flatMap(namespaceStorageRegistry::findNamespace)
+ .orElseThrow(() -> new NotFoundException("Unknown " + objectType + ": " + scopedId));
+ }
+
public record ConceptCodeResolution(
List resolvedConcepts,
List unknownCodes
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java
new file mode 100644
index 0000000000..89edc12d58
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java
@@ -0,0 +1,10 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigMapping(prefix = "conquery.storage")
+public interface StorageRuntimeConfig {
+ @WithDefault("IN_MEMORY")
+ String backend();
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java
new file mode 100644
index 0000000000..9237ec6763
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java
@@ -0,0 +1,35 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import java.util.List;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigMapping(prefix = "conquery")
+public interface TablesRuntimeConfig {
+ List tables();
+
+ interface TableEntry {
+ String id();
+
+ String label();
+
+ String dataset();
+
+ @WithDefault("")
+ String primaryColumn();
+
+ List columns();
+ }
+
+ interface ColumnEntry {
+ String id();
+
+ String label();
+
+ String type();
+
+ @WithDefault("__unset__")
+ String secondaryId();
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java
index d6dcb66432..ec595b2da6 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java
@@ -1,14 +1,22 @@
package com.bakdata.conquery.quarkus.storage;
import java.util.List;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
import java.util.Optional;
import com.bakdata.conquery.quarkus.api.config.ConceptsRuntimeConfig;
import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
+import com.bakdata.conquery.quarkus.api.config.TablesRuntimeConfig;
+import com.bakdata.conquery.quarkus.util.ScopedId;
+import io.quarkus.arc.properties.IfBuildProperty;
+import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "IN_MEMORY", enableIfMissing = true)
public class ConfigDatasetCatalogRepository implements DatasetCatalogRepository {
@Inject
@@ -17,27 +25,150 @@ public class ConfigDatasetCatalogRepository implements DatasetCatalogRepository
@Inject
ConceptsRuntimeConfig conceptsConfig;
+ @Inject
+ TablesRuntimeConfig tablesConfig;
+
+ private Map datasetsById = Map.of();
+ private Map> conceptsByDatasetId = Map.of();
+ private Map> tablesByDatasetId = Map.of();
+
+ @PostConstruct
+ void init() {
+ Map datasetIndex = new LinkedHashMap<>();
+ Map> conceptDatasetIndex = new LinkedHashMap<>();
+ Map> tableDatasetIndex = new LinkedHashMap<>();
+
+ datasetsConfig.datasets().forEach(dataset -> datasetIndex.put(dataset.id(), new DatasetRecord(dataset.id(), dataset.label())));
+ conceptsConfig.concepts().forEach(concept -> {
+ validateScopedIdBelongsToDataset(concept.id(), concept.dataset(), "concept");
+ ConceptRecord record = new ConceptRecord(concept.id(), concept.label());
+ conceptDatasetIndex.computeIfAbsent(concept.dataset(), ignored -> new LinkedHashMap<>()).put(record.id(), record);
+ });
+ tablesConfig.tables().forEach(table -> {
+ validateScopedIdBelongsToDataset(table.id(), table.dataset(), "table");
+ TableRecord record = new TableRecord(
+ table.id(),
+ table.label(),
+ table.columns().stream().map(column -> new ColumnRecord(
+ column.id(),
+ column.label(),
+ parseColumnType(column.type()),
+ blankToNull(column.secondaryId())
+ )).toList(),
+ blankToNull(table.primaryColumn())
+ );
+ tableDatasetIndex.computeIfAbsent(table.dataset(), ignored -> new LinkedHashMap<>()).put(record.id(), record);
+ });
+
+ datasetsById = java.util.Collections.unmodifiableMap(new LinkedHashMap<>(datasetIndex));
+ conceptsByDatasetId = freezeNestedMap(conceptDatasetIndex);
+ tablesByDatasetId = freezeNestedMap(tableDatasetIndex);
+ }
+
@Override
public List listDatasets() {
- return datasetsConfig.datasets().stream()
- .map(dataset -> new DatasetRecord(dataset.id(), dataset.label()))
- .toList();
+ return datasetsById.values().stream().toList();
}
@Override
public Optional findDataset(String datasetId) {
- return listDatasets().stream().filter(dataset -> dataset.id().equals(datasetId)).findFirst();
+ return Optional.ofNullable(datasetsById.get(datasetId));
+ }
+
+ @Override
+ public void saveDataset(DatasetRecord dataset) {
+ throw readOnly();
+ }
+
+ @Override
+ public boolean deleteDataset(String datasetId) {
+ throw readOnly();
}
@Override
public List listConcepts() {
- return conceptsConfig.concepts().stream()
- .map(concept -> new ConceptRecord(concept.id(), concept.label(), concept.dataset()))
- .toList();
+ return conceptsByDatasetId.values().stream().flatMap(map -> map.values().stream()).toList();
+ }
+
+ @Override
+ public List listConceptsForDataset(String datasetId) {
+ return conceptsByDatasetId.getOrDefault(datasetId, Map.of()).values().stream().toList();
}
@Override
public Optional findConcept(String conceptId) {
- return listConcepts().stream().filter(concept -> concept.id().equals(conceptId)).findFirst();
+ return ScopedId.extractDatasetId(conceptId)
+ .flatMap(datasetId -> Optional.ofNullable(conceptsByDatasetId.getOrDefault(datasetId, Map.of()).get(conceptId)));
+ }
+
+ @Override
+ public void saveConcept(ConceptRecord concept) {
+ throw readOnly();
+ }
+
+ @Override
+ public boolean deleteConcept(String conceptId) {
+ throw readOnly();
+ }
+
+ @Override
+ public List listTables() {
+ return tablesByDatasetId.values().stream().flatMap(map -> map.values().stream()).toList();
+ }
+
+ @Override
+ public List listTablesForDataset(String datasetId) {
+ return tablesByDatasetId.getOrDefault(datasetId, Map.of()).values().stream().toList();
+ }
+
+ @Override
+ public Optional findTable(String tableId) {
+ return ScopedId.extractDatasetId(tableId)
+ .flatMap(datasetId -> Optional.ofNullable(tablesByDatasetId.getOrDefault(datasetId, Map.of()).get(tableId)));
+ }
+
+ @Override
+ public void saveTable(TableRecord table) {
+ throw readOnly();
+ }
+
+ @Override
+ public boolean deleteTable(String tableId) {
+ throw readOnly();
+ }
+
+ private DatasetCatalogRepository.ColumnType parseColumnType(String type) {
+ try {
+ return DatasetCatalogRepository.ColumnType.valueOf(type.trim().toUpperCase(Locale.ROOT));
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException("Unsupported table column type: " + type, e);
+ }
+ }
+
+ private String blankToNull(String value) {
+ return value == null || value.isBlank() || "__unset__".equals(value) ? null : value;
+ }
+
+ private void validateScopedIdBelongsToDataset(String scopedId, String datasetId, String objectType) {
+ String derivedDataset = ScopedId.extractDatasetId(scopedId)
+ .orElseThrow(() -> new IllegalArgumentException("Configured " + objectType + " id is blank"));
+ if (!derivedDataset.equals(datasetId)) {
+ throw new IllegalArgumentException(
+ "Configured " + objectType + " id '" + scopedId + "' does not match dataset '" + datasetId + "'."
+ );
+ }
+ }
+
+ private Map> freezeNestedMap(Map> source) {
+ Map> result = new LinkedHashMap<>();
+ source.forEach((datasetId, entries) -> result.put(datasetId, java.util.Collections.unmodifiableMap(new LinkedHashMap<>(entries))));
+ return java.util.Collections.unmodifiableMap(result);
+ }
+
+ private UnsupportedOperationException readOnly() {
+ return new UnsupportedOperationException(
+ "ConfigDatasetCatalogRepository is read-only. Adapt application configuration and restart."
+ );
}
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java
index c55610453d..b39e2f65ef 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java
@@ -9,10 +9,30 @@ public interface DatasetCatalogRepository {
Optional findDataset(String datasetId);
+ void saveDataset(DatasetRecord dataset);
+
+ boolean deleteDataset(String datasetId);
+
List listConcepts();
+ List listConceptsForDataset(String datasetId);
+
Optional findConcept(String conceptId);
+ void saveConcept(ConceptRecord concept);
+
+ boolean deleteConcept(String conceptId);
+
+ List listTables();
+
+ List listTablesForDataset(String datasetId);
+
+ Optional findTable(String tableId);
+
+ void saveTable(TableRecord table);
+
+ boolean deleteTable(String tableId);
+
record DatasetRecord(
String id,
String label
@@ -20,9 +40,38 @@ record DatasetRecord(
}
record ConceptRecord(
+ String id,
+ String label
+ ) {
+ }
+
+ record TableRecord(
String id,
String label,
- String datasetId
+ List columns,
+ String primaryColumn
) {
+ public TableRecord {
+ columns = columns == null ? List.of() : List.copyOf(columns);
+ }
+ }
+
+ record ColumnRecord(
+ String id,
+ String label,
+ ColumnType type,
+ String secondaryId
+ ) {
+ }
+
+ enum ColumnType {
+ STRING,
+ INTEGER,
+ BOOLEAN,
+ REAL,
+ DECIMAL,
+ MONEY,
+ DATE,
+ DATE_RANGE
}
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java
index da9e9db638..98dd29de97 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java
@@ -6,9 +6,11 @@
import java.util.concurrent.ConcurrentHashMap;
import com.bakdata.conquery.quarkus.storage.model.StoredFormConfig;
+import io.quarkus.arc.properties.IfBuildProperty;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "IN_MEMORY", enableIfMissing = true)
public class InMemoryFormConfigRepository implements FormConfigRepository {
private final Map configsById = new ConcurrentHashMap<>();
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java
index faa65177c3..f033587241 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java
@@ -6,9 +6,11 @@
import java.util.concurrent.ConcurrentHashMap;
import com.bakdata.conquery.quarkus.storage.model.StoredQuery;
+import io.quarkus.arc.properties.IfBuildProperty;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "IN_MEMORY", enableIfMissing = true)
public class InMemoryQueryRepository implements QueryRepository {
private final Map queriesById = new ConcurrentHashMap<>();
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/NamespaceStorage.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/NamespaceStorage.java
new file mode 100644
index 0000000000..37814e5d8f
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/NamespaceStorage.java
@@ -0,0 +1,25 @@
+package com.bakdata.conquery.quarkus.storage;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface NamespaceStorage {
+
+ DatasetCatalogRepository.DatasetRecord dataset();
+
+ List listConcepts();
+
+ Optional findConcept(String conceptId);
+
+ void saveConcept(DatasetCatalogRepository.ConceptRecord concept);
+
+ boolean deleteConcept(String conceptId);
+
+ List listTables();
+
+ Optional findTable(String tableId);
+
+ void saveTable(DatasetCatalogRepository.TableRecord table);
+
+ boolean deleteTable(String tableId);
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/NamespaceStorageRegistry.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/NamespaceStorageRegistry.java
new file mode 100644
index 0000000000..4c2dc4f33e
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/NamespaceStorageRegistry.java
@@ -0,0 +1,11 @@
+package com.bakdata.conquery.quarkus.storage;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface NamespaceStorageRegistry {
+
+ List listDatasets();
+
+ Optional findNamespace(String datasetId);
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/RepositoryNamespaceStorageRegistry.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/RepositoryNamespaceStorageRegistry.java
new file mode 100644
index 0000000000..45dfbd0881
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/RepositoryNamespaceStorageRegistry.java
@@ -0,0 +1,110 @@
+package com.bakdata.conquery.quarkus.storage;
+
+import java.util.List;
+import java.util.Optional;
+
+import com.bakdata.conquery.quarkus.util.ScopedId;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class RepositoryNamespaceStorageRegistry implements NamespaceStorageRegistry {
+
+ @Inject
+ DatasetCatalogRepository catalogRepository;
+
+ @Override
+ public List listDatasets() {
+ return catalogRepository.listDatasets();
+ }
+
+ @Override
+ public Optional findNamespace(String datasetId) {
+ return catalogRepository.findDataset(datasetId).map(dataset -> new RepositoryNamespaceStorage(dataset, catalogRepository));
+ }
+
+ private static final class RepositoryNamespaceStorage implements NamespaceStorage {
+ private final DatasetCatalogRepository.DatasetRecord dataset;
+ private final DatasetCatalogRepository catalogRepository;
+
+ private RepositoryNamespaceStorage(
+ DatasetCatalogRepository.DatasetRecord dataset,
+ DatasetCatalogRepository catalogRepository
+ ) {
+ this.dataset = dataset;
+ this.catalogRepository = catalogRepository;
+ }
+
+ @Override
+ public DatasetCatalogRepository.DatasetRecord dataset() {
+ return dataset;
+ }
+
+ @Override
+ public List listConcepts() {
+ return catalogRepository.listConceptsForDataset(dataset.id());
+ }
+
+ @Override
+ public Optional findConcept(String conceptId) {
+ if (!belongsToNamespace(conceptId)) {
+ return Optional.empty();
+ }
+ return catalogRepository.findConcept(conceptId);
+ }
+
+ @Override
+ public void saveConcept(DatasetCatalogRepository.ConceptRecord concept) {
+ if (!belongsToNamespace(concept.id())) {
+ throw new IllegalArgumentException("Concept id does not belong to namespace '" + dataset.id() + "': " + concept.id());
+ }
+ catalogRepository.saveConcept(new DatasetCatalogRepository.ConceptRecord(concept.id(), concept.label()));
+ }
+
+ @Override
+ public boolean deleteConcept(String conceptId) {
+ if (!belongsToNamespace(conceptId)) {
+ return false;
+ }
+ return catalogRepository.deleteConcept(conceptId);
+ }
+
+ @Override
+ public List listTables() {
+ return catalogRepository.listTablesForDataset(dataset.id());
+ }
+
+ @Override
+ public Optional findTable(String tableId) {
+ if (!belongsToNamespace(tableId)) {
+ return Optional.empty();
+ }
+ return catalogRepository.findTable(tableId);
+ }
+
+ @Override
+ public void saveTable(DatasetCatalogRepository.TableRecord table) {
+ if (!belongsToNamespace(table.id())) {
+ throw new IllegalArgumentException("Table id does not belong to namespace '" + dataset.id() + "': " + table.id());
+ }
+ catalogRepository.saveTable(new DatasetCatalogRepository.TableRecord(
+ table.id(),
+ table.label(),
+ table.columns(),
+ table.primaryColumn()
+ ));
+ }
+
+ @Override
+ public boolean deleteTable(String tableId) {
+ if (!belongsToNamespace(tableId)) {
+ return false;
+ }
+ return catalogRepository.deleteTable(tableId);
+ }
+
+ private boolean belongsToNamespace(String scopedId) {
+ return ScopedId.extractDatasetId(scopedId).map(dataset.id()::equals).orElse(false);
+ }
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java
index 272021cc2b..0b96cdd781 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/InMemoryMetaStorageFacade.java
@@ -1,6 +1,5 @@
package com.bakdata.conquery.quarkus.storage.meta;
-import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
import com.bakdata.conquery.quarkus.storage.QueryRepository;
import io.quarkus.arc.properties.IfBuildProperty;
@@ -11,20 +10,12 @@
@IfBuildProperty(name = "conquery.storage.backend", stringValue = "IN_MEMORY", enableIfMissing = true)
public class InMemoryMetaStorageFacade implements ManagerMetaStorage {
- @Inject
- DatasetCatalogRepository datasetCatalogRepository;
-
@Inject
QueryRepository queryRepository;
@Inject
FormConfigRepository formConfigRepository;
- @Override
- public DatasetCatalogRepository datasets() {
- return datasetCatalogRepository;
- }
-
@Override
public QueryRepository queries() {
return queryRepository;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/ManagerMetaStorage.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/ManagerMetaStorage.java
index 3beedcf9bb..a3fc0ca3c5 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/ManagerMetaStorage.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/ManagerMetaStorage.java
@@ -1,13 +1,10 @@
package com.bakdata.conquery.quarkus.storage.meta;
-import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
import com.bakdata.conquery.quarkus.storage.QueryRepository;
public interface ManagerMetaStorage {
- DatasetCatalogRepository datasets();
-
QueryRepository queries();
FormConfigRepository formConfigs();
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java
index d9a8236139..f9d022e3c5 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/meta/XodusMetaStorageFacade.java
@@ -1,6 +1,5 @@
package com.bakdata.conquery.quarkus.storage.meta;
-import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
import com.bakdata.conquery.quarkus.storage.QueryRepository;
import io.quarkus.arc.properties.IfBuildProperty;
@@ -14,26 +13,18 @@
@IfBuildProperty(name = "conquery.storage.backend", stringValue = "XODUS")
public class XodusMetaStorageFacade implements ManagerMetaStorage {
- private final DatasetCatalogRepository datasetCatalogRepository;
private final QueryRepository queryRepository;
private final FormConfigRepository formConfigRepository;
@Inject
public XodusMetaStorageFacade(
- DatasetCatalogRepository datasetCatalogRepository,
QueryRepository queryRepository,
FormConfigRepository formConfigRepository
) {
- this.datasetCatalogRepository = datasetCatalogRepository;
this.queryRepository = queryRepository;
this.formConfigRepository = formConfigRepository;
}
- @Override
- public DatasetCatalogRepository datasets() {
- return datasetCatalogRepository;
- }
-
@Override
public QueryRepository queries() {
return queryRepository;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetCatalogRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetCatalogRepository.java
new file mode 100644
index 0000000000..8808ba2498
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetCatalogRepository.java
@@ -0,0 +1,250 @@
+package com.bakdata.conquery.quarkus.storage.xodus;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
+import com.bakdata.conquery.quarkus.util.ScopedId;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.arc.properties.IfBuildProperty;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jetbrains.exodus.bindings.StringBinding;
+import jetbrains.exodus.env.Cursor;
+import jetbrains.exodus.env.Environment;
+import jetbrains.exodus.env.StoreConfig;
+import jetbrains.exodus.env.Transaction;
+
+@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "XODUS")
+public class XodusDatasetCatalogRepository implements DatasetCatalogRepository {
+
+ private static final String DATASET_STORE = "dataset";
+ private static final String CONCEPT_STORE = "concepts";
+ private static final String TABLE_STORE = "tables";
+ private static final String DATASET_KEY = "dataset";
+
+ @Inject
+ XodusDatasetEnvironmentProvider datasetEnvironmentProvider;
+
+ @Inject
+ ObjectMapper objectMapper;
+
+ @Override
+ public List listDatasets() {
+ List datasets = new ArrayList<>();
+ for (String datasetId : datasetEnvironmentProvider.listDatasetIds()) {
+ findDataset(datasetId).ifPresent(datasets::add);
+ }
+ return datasets;
+ }
+
+ @Override
+ public Optional findDataset(String datasetId) {
+ Optional environmentOpt = datasetEnvironmentProvider.findEnvironment(datasetId);
+ if (environmentOpt.isEmpty()) {
+ return Optional.empty();
+ }
+ Environment environment = environmentOpt.get();
+ return environment.computeInReadonlyTransaction(tx -> {
+ var datasetStore = environment.openStore(DATASET_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ var entry = datasetStore.get(tx, StringBinding.stringToEntry(DATASET_KEY));
+ if (entry == null) {
+ return Optional.empty();
+ }
+ return Optional.of(deserialize(StringBinding.entryToString(entry), DatasetRecord.class));
+ });
+ }
+
+ @Override
+ public void saveDataset(DatasetRecord dataset) {
+ Environment environment = datasetEnvironmentProvider.getOrCreateEnvironment(dataset.id());
+ environment.executeInTransaction(tx -> {
+ var datasetStore = environment.openStore(DATASET_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ datasetStore.put(tx, StringBinding.stringToEntry(DATASET_KEY), StringBinding.stringToEntry(serialize(dataset)));
+ });
+ }
+
+ @Override
+ public boolean deleteDataset(String datasetId) {
+ boolean exists = findDataset(datasetId).isPresent();
+ datasetEnvironmentProvider.removeEnvironment(datasetId);
+ return exists;
+ }
+
+ @Override
+ public List listConcepts() {
+ List concepts = new ArrayList<>();
+ for (String datasetId : datasetEnvironmentProvider.listDatasetIds()) {
+ concepts.addAll(listConceptsForDataset(datasetId));
+ }
+ return concepts;
+ }
+
+ @Override
+ public List listConceptsForDataset(String datasetId) {
+ Optional environment = datasetEnvironmentProvider.findEnvironment(datasetId);
+ if (environment.isEmpty()) {
+ return List.of();
+ }
+ return environment.get().computeInReadonlyTransaction(tx -> {
+ var conceptsStore = environment.get().openStore(CONCEPT_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ return readAllConcepts(conceptsStore, tx);
+ });
+ }
+
+ @Override
+ public Optional findConcept(String conceptId) {
+ Optional derivedDatasetId = ScopedId.extractDatasetId(conceptId);
+ if (derivedDatasetId.isEmpty()) {
+ return Optional.empty();
+ }
+ Optional environment = datasetEnvironmentProvider.findEnvironment(derivedDatasetId.get());
+ if (environment.isEmpty()) {
+ return Optional.empty();
+ }
+ return environment.get().computeInReadonlyTransaction(tx -> {
+ var conceptsStore = environment.get().openStore(CONCEPT_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ var entry = conceptsStore.get(tx, StringBinding.stringToEntry(conceptId));
+ if (entry == null) {
+ return Optional.empty();
+ }
+ return Optional.of(deserialize(StringBinding.entryToString(entry), ConceptRecord.class));
+ });
+ }
+
+ @Override
+ public void saveConcept(ConceptRecord concept) {
+ String datasetId = ScopedId.extractDatasetId(concept.id())
+ .orElseThrow(() -> new IllegalArgumentException("Concept id is not scoped by dataset: " + concept.id()));
+ Environment environment = datasetEnvironmentProvider.getOrCreateEnvironment(datasetId);
+ environment.executeInTransaction(tx -> {
+ var conceptsStore = environment.openStore(CONCEPT_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ conceptsStore.put(tx, StringBinding.stringToEntry(concept.id()), StringBinding.stringToEntry(serialize(concept)));
+ });
+ }
+
+ @Override
+ public boolean deleteConcept(String conceptId) {
+ Optional derivedDatasetId = ScopedId.extractDatasetId(conceptId);
+ if (derivedDatasetId.isEmpty()) {
+ return false;
+ }
+ Optional environment = datasetEnvironmentProvider.findEnvironment(derivedDatasetId.get());
+ if (environment.isEmpty()) {
+ return false;
+ }
+ return environment.get().computeInTransaction(tx -> {
+ var conceptsStore = environment.get().openStore(CONCEPT_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ return conceptsStore.delete(tx, StringBinding.stringToEntry(conceptId));
+ });
+ }
+
+ @Override
+ public List listTables() {
+ List tables = new ArrayList<>();
+ for (String datasetId : datasetEnvironmentProvider.listDatasetIds()) {
+ tables.addAll(listTablesForDataset(datasetId));
+ }
+ return tables;
+ }
+
+ @Override
+ public List listTablesForDataset(String datasetId) {
+ Optional environment = datasetEnvironmentProvider.findEnvironment(datasetId);
+ if (environment.isEmpty()) {
+ return List.of();
+ }
+ return environment.get().computeInReadonlyTransaction(tx -> {
+ var tablesStore = environment.get().openStore(TABLE_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ return readAllTables(tablesStore, tx);
+ });
+ }
+
+ @Override
+ public Optional findTable(String tableId) {
+ Optional derivedDatasetId = ScopedId.extractDatasetId(tableId);
+ if (derivedDatasetId.isEmpty()) {
+ return Optional.empty();
+ }
+ Optional environment = datasetEnvironmentProvider.findEnvironment(derivedDatasetId.get());
+ if (environment.isEmpty()) {
+ return Optional.empty();
+ }
+ return environment.get().computeInReadonlyTransaction(tx -> {
+ var tablesStore = environment.get().openStore(TABLE_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ var entry = tablesStore.get(tx, StringBinding.stringToEntry(tableId));
+ if (entry == null) {
+ return Optional.empty();
+ }
+ return Optional.of(deserialize(StringBinding.entryToString(entry), TableRecord.class));
+ });
+ }
+
+ @Override
+ public void saveTable(TableRecord table) {
+ String datasetId = ScopedId.extractDatasetId(table.id())
+ .orElseThrow(() -> new IllegalArgumentException("Table id is not scoped by dataset: " + table.id()));
+ Environment environment = datasetEnvironmentProvider.getOrCreateEnvironment(datasetId);
+ environment.executeInTransaction(tx -> {
+ var tablesStore = environment.openStore(TABLE_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ tablesStore.put(tx, StringBinding.stringToEntry(table.id()), StringBinding.stringToEntry(serialize(table)));
+ });
+ }
+
+ @Override
+ public boolean deleteTable(String tableId) {
+ Optional derivedDatasetId = ScopedId.extractDatasetId(tableId);
+ if (derivedDatasetId.isEmpty()) {
+ return false;
+ }
+ Optional environment = datasetEnvironmentProvider.findEnvironment(derivedDatasetId.get());
+ if (environment.isEmpty()) {
+ return false;
+ }
+ return environment.get().computeInTransaction(tx -> {
+ var tablesStore = environment.get().openStore(TABLE_STORE, StoreConfig.WITHOUT_DUPLICATES, tx);
+ return tablesStore.delete(tx, StringBinding.stringToEntry(tableId));
+ });
+ }
+
+ private List readAllConcepts(jetbrains.exodus.env.Store conceptsStore, Transaction tx) {
+ List result = new ArrayList<>();
+ try (Cursor cursor = conceptsStore.openCursor(tx)) {
+ while (cursor.getNext()) {
+ result.add(deserialize(StringBinding.entryToString(cursor.getValue()), ConceptRecord.class));
+ }
+ }
+ return result;
+ }
+
+ private List readAllTables(jetbrains.exodus.env.Store tablesStore, Transaction tx) {
+ List result = new ArrayList<>();
+ try (Cursor cursor = tablesStore.openCursor(tx)) {
+ while (cursor.getNext()) {
+ result.add(deserialize(StringBinding.entryToString(cursor.getValue()), TableRecord.class));
+ }
+ }
+ return result;
+ }
+
+ private String serialize(Object value) {
+ try {
+ return objectMapper.writeValueAsString(value);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to serialize catalog value for Xodus", e);
+ }
+ }
+
+ private T deserialize(String payload, Class type) {
+ try {
+ return objectMapper.readValue(payload, type);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to deserialize catalog value from Xodus", e);
+ }
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetEnvironmentProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetEnvironmentProvider.java
new file mode 100644
index 0000000000..51d8027891
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetEnvironmentProvider.java
@@ -0,0 +1,109 @@
+package com.bakdata.conquery.quarkus.storage.xodus;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import io.quarkus.arc.properties.IfBuildProperty;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import jetbrains.exodus.env.Environment;
+import jetbrains.exodus.env.Environments;
+
+@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "XODUS")
+public class XodusDatasetEnvironmentProvider {
+
+ private static final String DATASET_PREFIX = "dataset_";
+
+ @ConfigProperty(name = "conquery.storage.xodus.path", defaultValue = "storage/quarkus-meta")
+ String xodusPath;
+
+ private final Map environmentsByDatasetId = new ConcurrentHashMap<>();
+
+ public Environment getOrCreateEnvironment(String datasetId) {
+ return environmentsByDatasetId.computeIfAbsent(datasetId, ignored -> Environments.newInstance(datasetPath(datasetId).toFile()));
+ }
+
+ public Optional findEnvironment(String datasetId) {
+ Environment existing = environmentsByDatasetId.get(datasetId);
+ if (existing != null) {
+ return Optional.of(existing);
+ }
+ Path path = datasetPath(datasetId);
+ if (!Files.isDirectory(path)) {
+ return Optional.empty();
+ }
+ return Optional.of(getOrCreateEnvironment(datasetId));
+ }
+
+ public List listDatasetIds() {
+ Path base = datasetBasePath();
+ if (!Files.isDirectory(base)) {
+ return List.of();
+ }
+ try {
+ List ids = new ArrayList<>();
+ try (var stream = Files.list(base)) {
+ stream.filter(Files::isDirectory)
+ .map(path -> path.getFileName().toString())
+ .filter(name -> name.startsWith(DATASET_PREFIX))
+ .map(name -> name.substring(DATASET_PREFIX.length()))
+ .forEach(ids::add);
+ }
+ return ids;
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to list dataset environments in " + base, e);
+ }
+ }
+
+ public void removeEnvironment(String datasetId) {
+ Environment environment = environmentsByDatasetId.remove(datasetId);
+ if (environment != null) {
+ environment.close();
+ }
+ deleteRecursively(datasetPath(datasetId));
+ }
+
+ @PreDestroy
+ void closeAll() {
+ environmentsByDatasetId.values().forEach(Environment::close);
+ environmentsByDatasetId.clear();
+ }
+
+ private Path datasetBasePath() {
+ return Path.of(xodusPath, "datasets");
+ }
+
+ private Path datasetPath(String datasetId) {
+ return datasetBasePath().resolve(DATASET_PREFIX + datasetId);
+ }
+
+ private void deleteRecursively(Path path) {
+ if (!Files.exists(path)) {
+ return;
+ }
+ try (var walk = Files.walk(path)) {
+ walk.sorted((left, right) -> right.getNameCount() - left.getNameCount())
+ .forEach(current -> {
+ try {
+ Files.deleteIfExists(current);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to delete " + current, e);
+ }
+ });
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to walk for deletion " + path, e);
+ }
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusEnvironmentProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusEnvironmentProvider.java
new file mode 100644
index 0000000000..686295e9f6
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusEnvironmentProvider.java
@@ -0,0 +1,38 @@
+package com.bakdata.conquery.quarkus.storage.xodus;
+
+import java.nio.file.Path;
+
+import io.quarkus.arc.properties.IfBuildProperty;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import jetbrains.exodus.env.Environment;
+import jetbrains.exodus.env.Environments;
+
+@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "XODUS")
+public class XodusEnvironmentProvider {
+
+ @ConfigProperty(name = "conquery.storage.xodus.path", defaultValue = "storage/quarkus-meta")
+ String xodusPath;
+
+ private Environment environment;
+
+ @PostConstruct
+ void init() {
+ environment = Environments.newInstance(Path.of(xodusPath).toFile());
+ }
+
+ @PreDestroy
+ void close() {
+ if (environment != null) {
+ environment.close();
+ }
+ }
+
+ public Environment getEnvironment() {
+ return environment;
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusFormConfigRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusFormConfigRepository.java
new file mode 100644
index 0000000000..5547dbb598
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusFormConfigRepository.java
@@ -0,0 +1,97 @@
+package com.bakdata.conquery.quarkus.storage.xodus;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import com.bakdata.conquery.quarkus.storage.FormConfigRepository;
+import com.bakdata.conquery.quarkus.storage.model.StoredFormConfig;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.arc.properties.IfBuildProperty;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jetbrains.exodus.bindings.StringBinding;
+import jetbrains.exodus.env.Cursor;
+import jetbrains.exodus.env.Environment;
+import jetbrains.exodus.env.Store;
+import jetbrains.exodus.env.StoreConfig;
+
+@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "XODUS")
+public class XodusFormConfigRepository implements FormConfigRepository {
+
+ private static final String STORE_NAME = "quarkus_form_configs";
+
+ @Inject
+ XodusEnvironmentProvider environmentProvider;
+
+ @Inject
+ ObjectMapper objectMapper;
+
+ private Environment environment;
+ private Store store;
+
+ @PostConstruct
+ void init() {
+ environment = environmentProvider.getEnvironment();
+ store = environment.computeInTransaction(tx -> environment.openStore(STORE_NAME, StoreConfig.WITHOUT_DUPLICATES, tx));
+ }
+
+ @Override
+ public void save(StoredFormConfig config) {
+ String payload = serialize(config);
+ environment.executeInTransaction(tx -> store.put(tx, StringBinding.stringToEntry(config.getId()), StringBinding.stringToEntry(payload)));
+ }
+
+ @Override
+ public Optional findById(String formConfigId) {
+ return environment.computeInReadonlyTransaction(tx -> {
+ var value = store.get(tx, StringBinding.stringToEntry(formConfigId));
+ if (value == null) {
+ return Optional.empty();
+ }
+ return Optional.of(deserialize(StringBinding.entryToString(value), StoredFormConfig.class));
+ });
+ }
+
+ @Override
+ public List listByDataset(String datasetId) {
+ return environment.computeInReadonlyTransaction(tx -> {
+ List result = new ArrayList<>();
+ try (Cursor cursor = store.openCursor(tx)) {
+ while (cursor.getNext()) {
+ StoredFormConfig config = deserialize(StringBinding.entryToString(cursor.getValue()), StoredFormConfig.class);
+ if (datasetId.equals(config.getDatasetId())) {
+ result.add(config);
+ }
+ }
+ }
+ return result;
+ });
+ }
+
+ @Override
+ public boolean deleteById(String formConfigId) {
+ return environment.computeInTransaction(tx -> store.delete(tx, StringBinding.stringToEntry(formConfigId)));
+ }
+
+ private String serialize(Object value) {
+ try {
+ return objectMapper.writeValueAsString(value);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to serialize value for Xodus store " + STORE_NAME, e);
+ }
+ }
+
+ private T deserialize(String payload, Class type) {
+ try {
+ return objectMapper.readValue(payload, type);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to deserialize value from Xodus store " + STORE_NAME, e);
+ }
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusQueryRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusQueryRepository.java
new file mode 100644
index 0000000000..167a91f42c
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusQueryRepository.java
@@ -0,0 +1,97 @@
+package com.bakdata.conquery.quarkus.storage.xodus;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import com.bakdata.conquery.quarkus.storage.QueryRepository;
+import com.bakdata.conquery.quarkus.storage.model.StoredQuery;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkus.arc.properties.IfBuildProperty;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jetbrains.exodus.bindings.StringBinding;
+import jetbrains.exodus.env.Cursor;
+import jetbrains.exodus.env.Environment;
+import jetbrains.exodus.env.Store;
+import jetbrains.exodus.env.StoreConfig;
+
+@ApplicationScoped
+@IfBuildProperty(name = "conquery.storage.backend", stringValue = "XODUS")
+public class XodusQueryRepository implements QueryRepository {
+
+ private static final String STORE_NAME = "quarkus_queries";
+
+ @Inject
+ XodusEnvironmentProvider environmentProvider;
+
+ @Inject
+ ObjectMapper objectMapper;
+
+ private Environment environment;
+ private Store store;
+
+ @PostConstruct
+ void init() {
+ environment = environmentProvider.getEnvironment();
+ store = environment.computeInTransaction(tx -> environment.openStore(STORE_NAME, StoreConfig.WITHOUT_DUPLICATES, tx));
+ }
+
+ @Override
+ public void save(StoredQuery query) {
+ String payload = serialize(query);
+ environment.executeInTransaction(tx -> store.put(tx, StringBinding.stringToEntry(query.getId()), StringBinding.stringToEntry(payload)));
+ }
+
+ @Override
+ public Optional findById(String queryId) {
+ return environment.computeInReadonlyTransaction(tx -> {
+ var value = store.get(tx, StringBinding.stringToEntry(queryId));
+ if (value == null) {
+ return Optional.empty();
+ }
+ return Optional.of(deserialize(StringBinding.entryToString(value), StoredQuery.class));
+ });
+ }
+
+ @Override
+ public List listByDataset(String datasetId) {
+ return environment.computeInReadonlyTransaction(tx -> {
+ List result = new ArrayList<>();
+ try (Cursor cursor = store.openCursor(tx)) {
+ while (cursor.getNext()) {
+ StoredQuery query = deserialize(StringBinding.entryToString(cursor.getValue()), StoredQuery.class);
+ if (datasetId.equals(query.getDatasetId())) {
+ result.add(query);
+ }
+ }
+ }
+ return result;
+ });
+ }
+
+ @Override
+ public boolean deleteById(String queryId) {
+ return environment.computeInTransaction(tx -> store.delete(tx, StringBinding.stringToEntry(queryId)));
+ }
+
+ private String serialize(Object value) {
+ try {
+ return objectMapper.writeValueAsString(value);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to serialize value for Xodus store " + STORE_NAME, e);
+ }
+ }
+
+ private T deserialize(String payload, Class type) {
+ try {
+ return objectMapper.readValue(payload, type);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to deserialize value from Xodus store " + STORE_NAME, e);
+ }
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/util/ScopedId.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/util/ScopedId.java
new file mode 100644
index 0000000000..928fd68692
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/util/ScopedId.java
@@ -0,0 +1,20 @@
+package com.bakdata.conquery.quarkus.util;
+
+import java.util.Optional;
+
+public final class ScopedId {
+
+ private ScopedId() {
+ }
+
+ public static Optional extractDatasetId(String scopedId) {
+ if (scopedId == null || scopedId.isBlank()) {
+ return Optional.empty();
+ }
+ int separator = scopedId.indexOf('.');
+ if (separator <= 0) {
+ return Optional.of(scopedId);
+ }
+ return Optional.of(scopedId.substring(0, separator));
+ }
+}
diff --git a/backend-quarkus/src/main/resources/application-dev.properties b/backend-quarkus/src/main/resources/application-dev.properties
index 8a588d84bb..be0c213435 100644
--- a/backend-quarkus/src/main/resources/application-dev.properties
+++ b/backend-quarkus/src/main/resources/application-dev.properties
@@ -17,3 +17,18 @@ conquery.datasets[1].label=Empty Dataset
conquery.concepts[0].id=imdb
conquery.concepts[0].label=IMDb
conquery.concepts[0].dataset=imdb
+
+conquery.tables[0].id=imdb.title
+conquery.tables[0].label=Titles
+conquery.tables[0].dataset=imdb
+conquery.tables[0].primary-column=imdb.title.id
+conquery.tables[0].columns[0].id=imdb.title.id
+conquery.tables[0].columns[0].label=Title ID
+conquery.tables[0].columns[0].type=INTEGER
+conquery.tables[0].columns[0].secondary-id=pid
+conquery.tables[0].columns[1].id=imdb.title.name
+conquery.tables[0].columns[1].label=Title
+conquery.tables[0].columns[1].type=STRING
+conquery.tables[0].columns[2].id=imdb.title.release_date
+conquery.tables[0].columns[2].label=Release Date
+conquery.tables[0].columns[2].type=DATE
diff --git a/backend-quarkus/src/main/resources/application.properties b/backend-quarkus/src/main/resources/application.properties
index ed5ed7ef77..755f1b773e 100644
--- a/backend-quarkus/src/main/resources/application.properties
+++ b/backend-quarkus/src/main/resources/application.properties
@@ -16,6 +16,21 @@ conquery.concepts[0].id=imdb
conquery.concepts[0].label=IMDb
conquery.concepts[0].dataset=imdb
+conquery.tables[0].id=imdb.title
+conquery.tables[0].label=Titles
+conquery.tables[0].dataset=imdb
+conquery.tables[0].primary-column=imdb.title.id
+conquery.tables[0].columns[0].id=imdb.title.id
+conquery.tables[0].columns[0].label=Title ID
+conquery.tables[0].columns[0].type=INTEGER
+conquery.tables[0].columns[0].secondary-id=pid
+conquery.tables[0].columns[1].id=imdb.title.name
+conquery.tables[0].columns[1].label=Title
+conquery.tables[0].columns[1].type=STRING
+conquery.tables[0].columns[2].id=imdb.title.release_date
+conquery.tables[0].columns[2].label=Release Date
+conquery.tables[0].columns[2].type=DATE
+
conquery.entity-preview.all[0].name=imdb:entities
conquery.entity-preview.all[0].label=Entities
conquery.entity-preview.default[0].name=imdb:entities
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/ConceptResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/ConceptResourceTest.java
index 6e00999b55..a66b7021d4 100644
--- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/ConceptResourceTest.java
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/ConceptResourceTest.java
@@ -17,7 +17,11 @@ void conceptEndpointReturnsConceptMap() {
.statusCode(200)
.body("imdb.label", equalTo("IMDb"))
.body("imdb.detailsAvailable", equalTo(true))
- .body("imdb.tables.size()", equalTo(0))
+ .body("imdb.tables.size()", equalTo(1))
+ .body("imdb.tables[0].id", equalTo("imdb.title"))
+ .body("imdb.tables[0].columns.size()", equalTo(3))
+ .body("imdb.tables[0].columns[0].type", equalTo("INTEGER"))
+ .body("imdb.tables[0].primaryColumn", equalTo("imdb.title.id"))
.body("imdb.selects.size()", equalTo(0));
}
From adce5d7eb83b612b4dfc4c2a7f9be8ba32411823 Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Tue, 16 Jun 2026 14:31:41 +0200
Subject: [PATCH 30/45] static meta data from folder
---
backend-quarkus/README.md | 14 +
.../api/config/ConceptsRuntimeConfig.java | 15 +-
.../config/DatasetMetadataRuntimeConfig.java | 27 ++
.../api/config/DatasetsRuntimeConfig.java | 12 +-
.../config/EntityPreviewRuntimeConfig.java | 18 +
.../api/config/FormQueriesRuntimeConfig.java | 3 +
.../api/config/FrontendRuntimeConfig.java | 49 +++
.../api/config/StorageRuntimeConfig.java | 16 +
.../api/config/TablesRuntimeConfig.java | 33 +-
.../ConfigDatasetCatalogRepository.java | 30 +-
.../storage/DatasetMetadataFolderLoader.java | 325 ++++++++++++++++++
.../XodusDatasetEnvironmentProvider.java | 9 +-
.../xodus/XodusEnvironmentProvider.java | 9 +-
.../main/resources/application-dev.properties | 5 +
.../src/main/resources/application.properties | 6 +
.../conquery/quarkus/api/MeResourceTest.java | 2 +-
.../DatasetMetadataFolderLoaderTest.java | 94 +++++
17 files changed, 644 insertions(+), 23 deletions(-)
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetMetadataRuntimeConfig.java
create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetMetadataFolderLoader.java
create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/storage/DatasetMetadataFolderLoaderTest.java
diff --git a/backend-quarkus/README.md b/backend-quarkus/README.md
index 90103b693e..9171a40b91 100644
--- a/backend-quarkus/README.md
+++ b/backend-quarkus/README.md
@@ -19,6 +19,20 @@ mvn -pl backend-quarkus quarkus:dev
Default port is `8090`.
+### Ingest generated dataset metadata folders at startup
+
+You can mount generated metadata folders (for example `.../gen/demo`) into the container and let the backend ingest
+them during boot:
+
+```properties
+conquery.metadata.enabled=true
+conquery.metadata.root-path=/data/gen
+conquery.metadata.folders[0]=demo
+```
+
+This reads `conceptTrees/*.concept.json` and `tables/*.table.json` from each configured folder and loads them as
+dataset metadata.
+
## Migration strategy
1. Keep `backend` (Dropwizard) running while we port feature slices.
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java
index 57077cd9c4..1d7bea8e95 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java
@@ -1,18 +1,31 @@
package com.bakdata.conquery.quarkus.api.config;
import java.util.List;
+import java.util.Optional;
import io.smallrye.config.ConfigMapping;
@ConfigMapping(prefix = "conquery")
public interface ConceptsRuntimeConfig {
- List concepts();
+ /**
+ * Static root concept definitions used when no generated metadata folder is mounted.
+ */
+ Optional> concepts();
interface ConceptEntry {
+ /**
+ * Concept id as exposed to the frontend API.
+ */
String id();
+ /**
+ * Human-readable concept label shown in the concept tree.
+ */
String label();
+ /**
+ * Dataset id this concept belongs to.
+ */
String dataset();
}
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetMetadataRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetMetadataRuntimeConfig.java
new file mode 100644
index 0000000000..32ed6f3d7b
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetMetadataRuntimeConfig.java
@@ -0,0 +1,27 @@
+package com.bakdata.conquery.quarkus.api.config;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigMapping(prefix = "conquery.metadata")
+public interface DatasetMetadataRuntimeConfig {
+
+ /**
+ * Enables startup ingestion of generated dataset metadata folders.
+ */
+ @WithDefault("false")
+ boolean enabled();
+
+ /**
+ * Base directory that contains generated dataset metadata folders.
+ */
+ Optional rootPath();
+
+ /**
+ * Folder names or absolute paths to ingest as static dataset metadata.
+ */
+ Optional> folders();
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java
index 232980e749..d8707c37d7 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java
@@ -1,16 +1,26 @@
package com.bakdata.conquery.quarkus.api.config;
import java.util.List;
+import java.util.Optional;
import io.smallrye.config.ConfigMapping;
@ConfigMapping(prefix = "conquery")
public interface DatasetsRuntimeConfig {
- List datasets();
+ /**
+ * Static dataset definitions used when no generated metadata folder is mounted.
+ */
+ Optional> datasets();
interface DatasetEntry {
+ /**
+ * Stable dataset id used in API paths and dataset-scoped storage.
+ */
String id();
+ /**
+ * Human-readable dataset label shown in frontend dataset selection.
+ */
String label();
}
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java
index d7755e354c..4a71706ea8 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java
@@ -9,19 +9,37 @@
@ConfigMapping(prefix = "conquery.entity-preview")
public interface EntityPreviewRuntimeConfig {
+ /**
+ * All entity preview sources available for frontend previews.
+ */
@WithName("all")
List allSources();
+ /**
+ * Entity preview sources selected by default.
+ */
@WithName("default")
List defaultSources();
+ /**
+ * Optional concept id used as the entry point for entity preview search.
+ */
Optional searchConcept();
+ /**
+ * Optional comma-separated list of filter ids available for entity preview search.
+ */
Optional searchFilters();
interface Source {
+ /**
+ * Stable source id used by the frontend.
+ */
String name();
+ /**
+ * Human-readable source label.
+ */
String label();
}
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java
index 9cfdecdaf7..75ef8b359a 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java
@@ -7,6 +7,9 @@
@ConfigMapping(prefix = "conquery.form-queries")
public interface FormQueriesRuntimeConfig {
+ /**
+ * Classpath resources containing frontend form query configuration payloads.
+ */
@WithDefault("forms/export_form.frontend_conf.json,forms/table_export_form.frontend_conf.json")
List resources();
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java
index a9f1a8564b..c117917119 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java
@@ -10,49 +10,98 @@
@ConfigMapping(prefix = "conquery.frontend")
public interface FrontendRuntimeConfig {
+ /**
+ * Currency formatting options returned by the frontend configuration endpoint.
+ */
Currency currency();
+
+ /**
+ * Query upload id-column configuration returned by the frontend configuration endpoint.
+ */
QueryUpload queryUpload();
+ /**
+ * Number of years before the current year used to derive the observation period start.
+ */
@WithDefault("6")
int observationPeriodYears();
+ /**
+ * Optional manual/help URL shown by the frontend.
+ */
Optional manualUrl();
+ /**
+ * Optional support contact email shown by the frontend.
+ */
Optional contactEmail();
interface Currency {
+ /**
+ * Currency unit or symbol.
+ */
@WithDefault("€")
String unit();
+ /**
+ * Thousands separator for currency values.
+ */
@WithDefault(".")
String thousandSeparator();
+ /**
+ * Decimal separator for currency values.
+ */
@WithDefault(",")
String decimalSeparator();
+ /**
+ * Number of fraction digits for currency values.
+ */
@WithDefault("2")
int decimalScale();
}
interface QueryUpload {
+ /**
+ * Logical table name used by the frontend query upload workflow.
+ */
@WithDefault("entities")
String table();
+ /**
+ * Entity id columns accepted by the frontend query upload workflow.
+ */
Optional> ids();
}
interface IdColumn {
+ /**
+ * Display name of the upload id column.
+ */
@WithDefault("ID")
String name();
+ /**
+ * Field name used in uploaded rows.
+ */
@WithDefault("pid")
String field();
+ /**
+ * Localized labels for this id column.
+ */
Map label();
+ /**
+ * Whether this id column is the primary entity id.
+ */
@WithDefault("true")
boolean primaryId();
+ /**
+ * Whether this id column should be printed in uploaded query result displays.
+ */
@WithDefault("true")
boolean print();
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java
index 89edc12d58..26c6eb63e5 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java
@@ -5,6 +5,22 @@
@ConfigMapping(prefix = "conquery.storage")
public interface StorageRuntimeConfig {
+ /**
+ * Storage backend used for manager metadata and dataset catalog repositories.
+ */
@WithDefault("IN_MEMORY")
String backend();
+
+ /**
+ * Xodus-specific storage settings used when the XODUS backend is enabled.
+ */
+ Xodus xodus();
+
+ interface Xodus {
+ /**
+ * Base directory for Xodus-backed manager metadata and dataset-scoped environments.
+ */
+ @WithDefault("storage/quarkus-meta")
+ String path();
+ }
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java
index 9237ec6763..4d362942f0 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java
@@ -1,34 +1,65 @@
package com.bakdata.conquery.quarkus.api.config;
import java.util.List;
+import java.util.Optional;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
@ConfigMapping(prefix = "conquery")
public interface TablesRuntimeConfig {
- List tables();
+ /**
+ * Static table definitions used when no generated metadata folder is mounted.
+ */
+ Optional> tables();
interface TableEntry {
+ /**
+ * Table id as exposed to frontend concept details.
+ */
String id();
+ /**
+ * Human-readable table label shown in frontend concept details.
+ */
String label();
+ /**
+ * Dataset id this table belongs to.
+ */
String dataset();
+ /**
+ * Optional primary column id for entity identity in this table.
+ */
@WithDefault("")
String primaryColumn();
+ /**
+ * Column metadata available for filters, selects, and secondary id resolution.
+ */
List columns();
}
interface ColumnEntry {
+ /**
+ * Column id as exposed in frontend table/filter/select models.
+ */
String id();
+ /**
+ * Human-readable column label.
+ */
String label();
+ /**
+ * Conquery column type, for example STRING, INTEGER, DATE, MONEY, or DATE_RANGE.
+ */
String type();
+ /**
+ * Optional secondary id category represented by this column.
+ */
@WithDefault("__unset__")
String secondaryId();
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java
index ec595b2da6..fc55e24cd0 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java
@@ -28,6 +28,9 @@ public class ConfigDatasetCatalogRepository implements DatasetCatalogRepository
@Inject
TablesRuntimeConfig tablesConfig;
+ @Inject
+ DatasetMetadataFolderLoader metadataFolderLoader;
+
private Map datasetsById = Map.of();
private Map> conceptsByDatasetId = Map.of();
private Map> tablesByDatasetId = Map.of();
@@ -38,27 +41,32 @@ void init() {
Map> conceptDatasetIndex = new LinkedHashMap<>();
Map> tableDatasetIndex = new LinkedHashMap<>();
- datasetsConfig.datasets().forEach(dataset -> datasetIndex.put(dataset.id(), new DatasetRecord(dataset.id(), dataset.label())));
- conceptsConfig.concepts().forEach(concept -> {
+ datasetsConfig.datasets().orElse(List.of()).forEach(dataset -> datasetIndex.put(dataset.id(), new DatasetRecord(dataset.id(), dataset.label())));
+ conceptsConfig.concepts().orElse(List.of()).forEach(concept -> {
validateScopedIdBelongsToDataset(concept.id(), concept.dataset(), "concept");
ConceptRecord record = new ConceptRecord(concept.id(), concept.label());
conceptDatasetIndex.computeIfAbsent(concept.dataset(), ignored -> new LinkedHashMap<>()).put(record.id(), record);
});
- tablesConfig.tables().forEach(table -> {
+ tablesConfig.tables().orElse(List.of()).forEach(table -> {
validateScopedIdBelongsToDataset(table.id(), table.dataset(), "table");
TableRecord record = new TableRecord(
- table.id(),
- table.label(),
- table.columns().stream().map(column -> new ColumnRecord(
- column.id(),
- column.label(),
- parseColumnType(column.type()),
- blankToNull(column.secondaryId())
- )).toList(),
+ table.id(),
+ table.label(),
+ table.columns().stream().map(column -> new ColumnRecord(
+ column.id(),
+ column.label(),
+ parseColumnType(column.type()),
+ blankToNull(column.secondaryId())
+ )).toList(),
blankToNull(table.primaryColumn())
);
tableDatasetIndex.computeIfAbsent(table.dataset(), ignored -> new LinkedHashMap<>()).put(record.id(), record);
});
+ metadataFolderLoader.loadConfiguredDatasets().forEach(dataset -> {
+ datasetIndex.put(dataset.dataset().id(), dataset.dataset());
+ conceptDatasetIndex.put(dataset.dataset().id(), new LinkedHashMap<>(dataset.conceptsById()));
+ tableDatasetIndex.put(dataset.dataset().id(), new LinkedHashMap<>(dataset.tablesById()));
+ });
datasetsById = java.util.Collections.unmodifiableMap(new LinkedHashMap<>(datasetIndex));
conceptsByDatasetId = freezeNestedMap(conceptDatasetIndex);
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetMetadataFolderLoader.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetMetadataFolderLoader.java
new file mode 100644
index 0000000000..48a85fed38
--- /dev/null
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetMetadataFolderLoader.java
@@ -0,0 +1,325 @@
+package com.bakdata.conquery.quarkus.storage;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import com.bakdata.conquery.quarkus.api.config.DatasetMetadataRuntimeConfig;
+import com.bakdata.conquery.quarkus.util.ScopedId;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class DatasetMetadataFolderLoader {
+
+ private static final TypeReference> STRUCTURE_PAYLOAD_LIST = new TypeReference<>() {
+ };
+
+ @Inject
+ DatasetMetadataRuntimeConfig metadataConfig;
+
+ @Inject
+ ObjectMapper objectMapper;
+
+ public List loadConfiguredDatasets() {
+ if (!metadataConfig.enabled()) {
+ return List.of();
+ }
+
+ String rootPath = metadataConfig.rootPath()
+ .orElseThrow(() -> new IllegalStateException("conquery.metadata.root-path is required when metadata ingestion is enabled."));
+ List folders = metadataConfig.folders()
+ .orElseThrow(() -> new IllegalStateException("conquery.metadata.folders must contain at least one folder when metadata ingestion is enabled."));
+ if (folders.isEmpty()) {
+ throw new IllegalStateException("conquery.metadata.folders must not be empty when metadata ingestion is enabled.");
+ }
+
+ Map loadedByDatasetId = new LinkedHashMap<>();
+ Path root = Path.of(rootPath);
+ for (String configuredFolder : folders) {
+ Path folderPath = toFolderPath(root, configuredFolder);
+ LoadedDatasetMetadata loaded = loadDatasetFolder(folderPath);
+ LoadedDatasetMetadata previous = loadedByDatasetId.put(loaded.dataset().id(), loaded);
+ if (previous != null) {
+ throw new IllegalStateException("Duplicate dataset id '" + loaded.dataset().id() + "' across configured metadata folders.");
+ }
+ }
+
+ return loadedByDatasetId.values().stream().toList();
+ }
+
+ private LoadedDatasetMetadata loadDatasetFolder(Path folderPath) {
+ if (!Files.isDirectory(folderPath)) {
+ throw new IllegalStateException("Configured metadata folder does not exist or is not a directory: " + folderPath);
+ }
+
+ String folderName = folderPath.getFileName() == null ? folderPath.toString() : folderPath.getFileName().toString();
+ String datasetId = resolveDatasetId(folderPath).orElse(folderName);
+ DatasetCatalogRepository.DatasetRecord dataset = new DatasetCatalogRepository.DatasetRecord(datasetId, folderName);
+
+ Map conceptsById = loadConcepts(folderPath, datasetId);
+ Map tablesById = loadTables(folderPath, datasetId);
+
+ return new LoadedDatasetMetadata(
+ dataset,
+ Map.copyOf(conceptsById),
+ Map.copyOf(tablesById)
+ );
+ }
+
+ private Map loadConcepts(Path folderPath, String datasetId) {
+ Path conceptsDir = folderPath.resolve("conceptTrees");
+ if (!Files.isDirectory(conceptsDir)) {
+ throw new IllegalStateException("Metadata folder is missing conceptTrees/: " + folderPath);
+ }
+
+ Map conceptsById = new LinkedHashMap<>();
+ try (Stream files = Files.list(conceptsDir)) {
+ files.filter(Files::isRegularFile)
+ .filter(path -> path.getFileName().toString().endsWith(".concept.json"))
+ .sorted()
+ .forEach(path -> {
+ ConceptPayload payload = read(path, ConceptPayload.class);
+ String conceptName = firstNonBlank(payload.name(), stripSuffix(path.getFileName().toString(), ".concept.json"))
+ .orElseThrow(() -> new IllegalStateException("Concept file has no name: " + path));
+ String conceptId = ensureDatasetScopedId(datasetId, conceptName);
+ String conceptLabel = firstNonBlank(payload.label(), conceptName).orElse(conceptName);
+ conceptsById.put(conceptId, new DatasetCatalogRepository.ConceptRecord(conceptId, conceptLabel));
+ });
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to list concept metadata in " + conceptsDir, e);
+ }
+
+ return conceptsById;
+ }
+
+ private Map loadTables(Path folderPath, String datasetId) {
+ Path tablesDir = folderPath.resolve("tables");
+ if (!Files.isDirectory(tablesDir)) {
+ throw new IllegalStateException("Metadata folder is missing tables/: " + folderPath);
+ }
+
+ Map tablesById = new LinkedHashMap<>();
+ try (Stream files = Files.list(tablesDir)) {
+ files.filter(Files::isRegularFile)
+ .filter(path -> path.getFileName().toString().endsWith(".table.json"))
+ .sorted()
+ .forEach(path -> {
+ TablePayload payload = read(path, TablePayload.class);
+ String tableName = firstNonBlank(payload.name(), stripSuffix(path.getFileName().toString(), ".table.json"))
+ .orElseThrow(() -> new IllegalStateException("Table file has no name: " + path));
+ String tableId = ensureDatasetScopedId(datasetId, tableName);
+ String tableLabel = firstNonBlank(payload.label(), tableName).orElse(tableName);
+ List columns = Optional.ofNullable(payload.columns()).orElse(List.of()).stream()
+ .map(column -> toColumn(datasetId, tableName, tableId, column))
+ .toList();
+ String primaryColumn = toColumnId(datasetId, tableName, tableId, payload.primaryColumn()).orElse(null);
+ tablesById.put(tableId, new DatasetCatalogRepository.TableRecord(tableId, tableLabel, columns, primaryColumn));
+ });
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to list table metadata in " + tablesDir, e);
+ }
+
+ return tablesById;
+ }
+
+ private DatasetCatalogRepository.ColumnRecord toColumn(
+ String datasetId,
+ String tableName,
+ String tableId,
+ ColumnPayload payload
+ ) {
+ String rawColumnName = firstNonBlank(payload.name(), payload.id())
+ .orElseThrow(() -> new IllegalStateException("Column entry is missing a name/id in table " + tableId));
+ String columnId = toColumnId(datasetId, tableName, tableId, rawColumnName)
+ .orElseThrow(() -> new IllegalStateException("Column id must not be blank in table " + tableId));
+ String columnLabel = firstNonBlank(payload.label(), rawColumnName).orElse(rawColumnName);
+ DatasetCatalogRepository.ColumnType type = parseColumnType(payload.type());
+ String secondaryId = normalizeBlank(payload.secondaryId()).orElse(null);
+ return new DatasetCatalogRepository.ColumnRecord(columnId, columnLabel, type, secondaryId);
+ }
+
+ private Optional toColumnId(String datasetId, String tableName, String tableId, String rawId) {
+ Optional value = normalizeBlank(rawId);
+ if (value.isEmpty()) {
+ return Optional.empty();
+ }
+ String id = value.get();
+ if (id.startsWith(tableId + ".")) {
+ return Optional.of(id);
+ }
+ if (id.startsWith(tableName + ".")) {
+ return Optional.of(datasetId + "." + id);
+ }
+ if (ScopedId.extractDatasetId(id).filter(datasetId::equals).isPresent() && id.contains(".")) {
+ return Optional.of(id);
+ }
+ if (id.contains(".")) {
+ return Optional.of(datasetId + "." + id);
+ }
+ return Optional.of(tableId + "." + id);
+ }
+
+ private String ensureDatasetScopedId(String datasetId, String rawId) {
+ String id = normalizeBlank(rawId).orElseThrow(() -> new IllegalStateException("Id must not be blank."));
+ if (ScopedId.extractDatasetId(id).filter(datasetId::equals).isPresent() && id.contains(".")) {
+ return id;
+ }
+ return datasetId + "." + id;
+ }
+
+ private Optional resolveDatasetId(Path folderPath) {
+ List structureFiles;
+ try (Stream files = Files.list(folderPath)) {
+ structureFiles = files.filter(Files::isRegularFile)
+ .filter(path -> path.getFileName().toString().startsWith("structure"))
+ .filter(path -> path.getFileName().toString().endsWith(".json"))
+ .sorted()
+ .toList();
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to list structure files in metadata folder " + folderPath, e);
+ }
+
+ Set datasetIds = structureFiles.stream()
+ .flatMap(path -> readStructureDatasetIds(path).stream())
+ .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));
+ if (datasetIds.isEmpty()) {
+ return Optional.empty();
+ }
+ if (datasetIds.size() > 1) {
+ throw new IllegalStateException("Metadata folder contains multiple dataset ids in structure files: " + folderPath + " -> " + datasetIds);
+ }
+ return datasetIds.stream().findFirst();
+ }
+
+ private List readStructureDatasetIds(Path structureFile) {
+ List payload = read(structureFile, STRUCTURE_PAYLOAD_LIST);
+ return payload.stream()
+ .map(StructureEntryPayload::datasetValue)
+ .flatMap(Optional::stream)
+ .toList();
+ }
+
+ private T read(Path path, Class type) {
+ try {
+ return objectMapper.readValue(path.toFile(), type);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to parse metadata file: " + path, e);
+ }
+ }
+
+ private T read(Path path, TypeReference type) {
+ try {
+ return objectMapper.readValue(path.toFile(), type);
+ }
+ catch (IOException e) {
+ throw new IllegalStateException("Failed to parse metadata file: " + path, e);
+ }
+ }
+
+ private Path toFolderPath(Path root, String configuredFolder) {
+ String value = normalizeBlank(configuredFolder)
+ .orElseThrow(() -> new IllegalStateException("conquery.metadata.folders must not contain blank entries."));
+ Path configuredPath = Path.of(value);
+ return configuredPath.isAbsolute() ? configuredPath : root.resolve(configuredPath);
+ }
+
+ private DatasetCatalogRepository.ColumnType parseColumnType(String rawType) {
+ String type = normalizeBlank(rawType)
+ .orElseThrow(() -> new IllegalStateException("Table column type must not be blank."));
+ try {
+ return DatasetCatalogRepository.ColumnType.valueOf(type.trim().toUpperCase(java.util.Locale.ROOT));
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Unsupported table column type: " + rawType, e);
+ }
+ }
+
+ private Optional firstNonBlank(String... candidates) {
+ for (String candidate : candidates) {
+ Optional value = normalizeBlank(candidate);
+ if (value.isPresent()) {
+ return value;
+ }
+ }
+ return Optional.empty();
+ }
+
+ private Optional normalizeBlank(String value) {
+ if (value == null) {
+ return Optional.empty();
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? Optional.empty() : Optional.of(trimmed);
+ }
+
+ private String stripSuffix(String fileName, String suffix) {
+ if (fileName.endsWith(suffix)) {
+ return fileName.substring(0, fileName.length() - suffix.length());
+ }
+ return fileName;
+ }
+
+ public record LoadedDatasetMetadata(
+ DatasetCatalogRepository.DatasetRecord dataset,
+ Map conceptsById,
+ Map tablesById
+ ) {
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record StructureEntryPayload(
+ String dataset
+ ) {
+ Optional datasetValue() {
+ return normalize(dataset);
+ }
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record ConceptPayload(
+ String name,
+ String label
+ ) {
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record TablePayload(
+ String name,
+ String label,
+ String primaryColumn,
+ List columns
+ ) {
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record ColumnPayload(
+ String id,
+ String name,
+ String label,
+ String type,
+ String secondaryId
+ ) {
+ }
+
+ private static Optional normalize(String value) {
+ if (value == null) {
+ return Optional.empty();
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? Optional.empty() : Optional.of(trimmed);
+ }
+}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetEnvironmentProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetEnvironmentProvider.java
index 51d8027891..727c4f03a8 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetEnvironmentProvider.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusDatasetEnvironmentProvider.java
@@ -9,10 +9,11 @@
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
+import com.bakdata.conquery.quarkus.api.config.StorageRuntimeConfig;
import io.quarkus.arc.properties.IfBuildProperty;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
-import org.eclipse.microprofile.config.inject.ConfigProperty;
+import jakarta.inject.Inject;
import jetbrains.exodus.env.Environment;
import jetbrains.exodus.env.Environments;
@@ -23,8 +24,8 @@ public class XodusDatasetEnvironmentProvider {
private static final String DATASET_PREFIX = "dataset_";
- @ConfigProperty(name = "conquery.storage.xodus.path", defaultValue = "storage/quarkus-meta")
- String xodusPath;
+ @Inject
+ StorageRuntimeConfig storageRuntimeConfig;
private final Map environmentsByDatasetId = new ConcurrentHashMap<>();
@@ -80,7 +81,7 @@ void closeAll() {
}
private Path datasetBasePath() {
- return Path.of(xodusPath, "datasets");
+ return Path.of(storageRuntimeConfig.xodus().path(), "datasets");
}
private Path datasetPath(String datasetId) {
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusEnvironmentProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusEnvironmentProvider.java
index 686295e9f6..03be2daa36 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusEnvironmentProvider.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/xodus/XodusEnvironmentProvider.java
@@ -2,11 +2,12 @@
import java.nio.file.Path;
+import com.bakdata.conquery.quarkus.api.config.StorageRuntimeConfig;
import io.quarkus.arc.properties.IfBuildProperty;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
-import org.eclipse.microprofile.config.inject.ConfigProperty;
+import jakarta.inject.Inject;
import jetbrains.exodus.env.Environment;
import jetbrains.exodus.env.Environments;
@@ -15,14 +16,14 @@
@IfBuildProperty(name = "conquery.storage.backend", stringValue = "XODUS")
public class XodusEnvironmentProvider {
- @ConfigProperty(name = "conquery.storage.xodus.path", defaultValue = "storage/quarkus-meta")
- String xodusPath;
+ @Inject
+ StorageRuntimeConfig storageRuntimeConfig;
private Environment environment;
@PostConstruct
void init() {
- environment = Environments.newInstance(Path.of(xodusPath).toFile());
+ environment = Environments.newInstance(Path.of(storageRuntimeConfig.xodus().path()).toFile());
}
@PreDestroy
diff --git a/backend-quarkus/src/main/resources/application-dev.properties b/backend-quarkus/src/main/resources/application-dev.properties
index be0c213435..48c51f9591 100644
--- a/backend-quarkus/src/main/resources/application-dev.properties
+++ b/backend-quarkus/src/main/resources/application-dev.properties
@@ -3,6 +3,11 @@ quarkus.http.cors.origins=/.*/
conquery.storage.backend=IN_MEMORY
+# Optional startup ingestion of generated dataset metadata folders.
+# conquery.metadata.enabled=true
+# conquery.metadata.root-path=/data/gen
+# conquery.metadata.folders[0]=demo
+
conquery.frontend.query-upload.ids[0].name=ID
conquery.frontend.query-upload.ids[0].field=pid
conquery.frontend.query-upload.ids[0].label=result
diff --git a/backend-quarkus/src/main/resources/application.properties b/backend-quarkus/src/main/resources/application.properties
index 755f1b773e..b9fed21654 100644
--- a/backend-quarkus/src/main/resources/application.properties
+++ b/backend-quarkus/src/main/resources/application.properties
@@ -7,6 +7,12 @@ quarkus.swagger-ui.path=/q/swagger-ui
# Build-time selection of manager meta storage backend: IN_MEMORY or XODUS.
conquery.storage.backend=IN_MEMORY
+# Optional startup ingestion of generated dataset metadata folders.
+# Useful for container volume mounts (e.g. mount /data/gen and load folder "demo").
+# conquery.metadata.enabled=true
+# conquery.metadata.root-path=/data/gen
+# conquery.metadata.folders[0]=demo
+
conquery.datasets[0].id=imdb
conquery.datasets[0].label=IMDb
conquery.datasets[1].id=empty-set
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/MeResourceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/MeResourceTest.java
index d38cf5b766..5e7fde410b 100644
--- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/MeResourceTest.java
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/api/MeResourceTest.java
@@ -25,7 +25,7 @@ void meEndpointRespondsWithExpectedShape() {
@Test
@TestSecurity(user = "testUser")
- void authneticatedMeEndpointRespondsWithExpectedShape() {
+ void authenticatedMeEndpointRespondsWithExpectedShape() {
given()
.when().get("/api/me")
.then()
diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/storage/DatasetMetadataFolderLoaderTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/storage/DatasetMetadataFolderLoaderTest.java
new file mode 100644
index 0000000000..bdeb79ad3f
--- /dev/null
+++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/storage/DatasetMetadataFolderLoaderTest.java
@@ -0,0 +1,94 @@
+package com.bakdata.conquery.quarkus.storage;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+
+import com.bakdata.conquery.quarkus.api.config.DatasetMetadataRuntimeConfig;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class DatasetMetadataFolderLoaderTest {
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void loadsConfiguredMetadataFolderAndScopesIds() throws Exception {
+ Path root = tempDir.resolve("gen");
+ Path demo = root.resolve("demo");
+ Files.createDirectories(demo.resolve("conceptTrees"));
+ Files.createDirectories(demo.resolve("tables"));
+
+ Files.writeString(
+ demo.resolve("structure_demo.json"),
+ """
+ [
+ {"dataset":"fdb_demo","label":"Group 1"},
+ {"dataset":"fdb_demo","label":"Group 2"}
+ ]
+ """
+ );
+ Files.writeString(
+ demo.resolve("conceptTrees/icd.concept.json"),
+ """
+ {"name":"icd","label":"ICD"}
+ """
+ );
+ Files.writeString(
+ demo.resolve("tables/kh_diagnose.table.json"),
+ """
+ {
+ "name":"kh_diagnose",
+ "columns":[
+ {"name":"icd_code","type":"STRING","secondaryId":"icd_code"},
+ {"name":"entlassungsdatum","type":"DATE"}
+ ]
+ }
+ """
+ );
+
+ DatasetMetadataFolderLoader loader = new DatasetMetadataFolderLoader();
+ loader.objectMapper = new ObjectMapper();
+ loader.metadataConfig = new DatasetMetadataRuntimeConfig() {
+ @Override
+ public boolean enabled() {
+ return true;
+ }
+
+ @Override
+ public Optional rootPath() {
+ return Optional.of(root.toString());
+ }
+
+ @Override
+ public Optional> folders() {
+ return Optional.of(List.of("demo"));
+ }
+ };
+
+ List loaded = loader.loadConfiguredDatasets();
+ assertEquals(1, loaded.size());
+
+ DatasetMetadataFolderLoader.LoadedDatasetMetadata dataset = loaded.getFirst();
+ assertEquals("fdb_demo", dataset.dataset().id());
+ assertEquals("demo", dataset.dataset().label());
+
+ assertTrue(dataset.conceptsById().containsKey("fdb_demo.icd"));
+ DatasetCatalogRepository.ConceptRecord concept = dataset.conceptsById().get("fdb_demo.icd");
+ assertEquals("ICD", concept.label());
+
+ assertTrue(dataset.tablesById().containsKey("fdb_demo.kh_diagnose"));
+ DatasetCatalogRepository.TableRecord table = dataset.tablesById().get("fdb_demo.kh_diagnose");
+ assertEquals("fdb_demo.kh_diagnose.icd_code", table.columns().get(0).id());
+ assertEquals("icd_code", table.columns().get(0).secondaryId());
+ assertEquals("fdb_demo.kh_diagnose.entlassungsdatum", table.columns().get(1).id());
+ assertNotNull(table.columns().get(1).type());
+ }
+}
From 39e749bb9a0ce25546b94003a4688f363e84fc4d Mon Sep 17 00:00:00 2001
From: MT <12283268+thoniTUB@users.noreply.github.com>
Date: Mon, 6 Jul 2026 12:39:32 +0200
Subject: [PATCH 31/45] work on three stage objects models json/domain/ui for
concepts
---
backend-quarkus/pom.xml | 4 +
.../conquery/quarkus/api/ConceptResource.java | 67 ++++--
.../api/DatasetFormConfigResource.java | 6 +-
.../quarkus/api/DatasetsResource.java | 61 +++++-
.../quarkus/api/FormConfigResource.java | 3 +-
.../conquery/quarkus/api/QueryResource.java | 3 +-
.../api/config/TablesRuntimeConfig.java | 66 ------
.../config/ConceptsRuntimeConfig.java | 8 +-
.../config/DatasetMetadataRuntimeConfig.java | 2 +-
.../config/DatasetsRuntimeConfig.java | 2 +-
.../config/EntityPreviewRuntimeConfig.java | 2 +-
.../config/FormQueriesRuntimeConfig.java | 2 +-
.../config/FrontendConfigResource.java | 3 +-
.../config/FrontendConfigurationResponse.java | 2 +-
.../config/FrontendRuntimeConfig.java | 2 +-
.../config/StorageRuntimeConfig.java | 2 +-
.../{api => services}/DatasetService.java | 59 ++---
.../{api => services}/EntityQueryService.java | 5 +-
.../{api => services}/FormConfigService.java | 4 +-
.../{api => services}/QueryStateService.java | 26 +--
.../{api => services}/QueryUploadService.java | 2 +-
.../ConfigDatasetCatalogRepository.java | 54 +----
.../storage/DatasetCatalogRepository.java | 88 +++++++-
.../storage/DatasetMetadataFolderLoader.java | 206 ++++++++++++------
.../quarkus/storage/NamespaceStorage.java | 6 +-
.../RepositoryNamespaceStorageRegistry.java | 8 +-
.../xodus/XodusDatasetCatalogRepository.java | 32 +--
.../XodusDatasetEnvironmentProvider.java | 2 +-
.../xodus/XodusEnvironmentProvider.java | 9 +-
.../resources/application-test.properties | 9 +
.../src/main/resources/application.properties | 43 +---
.../quarkus/api/DatasetsResourceTest.java | 4 +-
.../quarkus/api/QueryResourceTest.java | 25 ++-
.../quarkus/services/DatasetServiceTest.java | 106 +++++++++
.../DatasetMetadataFolderLoaderTest.java | 141 +++++++++++-
.../empty-set/conceptTrees/.gitkeep | 1 +
.../test-meta-data/empty-set/dataset.json | 4 +
.../empty-set/structure_empty-set.json | 1 +
.../test-meta-data/empty-set/tables/.gitkeep | 1 +
.../imdb/conceptTrees/imdb.concept.json | 6 +
.../test-meta-data/imdb/dataset.json | 4 +
.../test-meta-data/imdb/structure_imdb.json | 1 +
.../imdb/tables/titles.table.json | 23 ++
43 files changed, 728 insertions(+), 377 deletions(-)
delete mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => }/config/ConceptsRuntimeConfig.java (79%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => }/config/DatasetMetadataRuntimeConfig.java (92%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => }/config/DatasetsRuntimeConfig.java (91%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => }/config/EntityPreviewRuntimeConfig.java (94%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => }/config/FormQueriesRuntimeConfig.java (89%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => }/config/FrontendConfigResource.java (97%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => }/config/FrontendConfigurationResponse.java (94%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => }/config/FrontendRuntimeConfig.java (97%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => }/config/StorageRuntimeConfig.java (92%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => services}/DatasetService.java (64%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => services}/EntityQueryService.java (94%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => services}/FormConfigService.java (95%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => services}/QueryStateService.java (93%)
rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/{api => services}/QueryUploadService.java (96%)
create mode 100644 backend-quarkus/src/main/resources/application-test.properties
create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java
create mode 100644 backend-quarkus/src/test/resources/test-meta-data/empty-set/conceptTrees/.gitkeep
create mode 100644 backend-quarkus/src/test/resources/test-meta-data/empty-set/dataset.json
create mode 100644 backend-quarkus/src/test/resources/test-meta-data/empty-set/structure_empty-set.json
create mode 100644 backend-quarkus/src/test/resources/test-meta-data/empty-set/tables/.gitkeep
create mode 100644 backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json
create mode 100644 backend-quarkus/src/test/resources/test-meta-data/imdb/dataset.json
create mode 100644 backend-quarkus/src/test/resources/test-meta-data/imdb/structure_imdb.json
create mode 100644 backend-quarkus/src/test/resources/test-meta-data/imdb/tables/titles.table.json
diff --git a/backend-quarkus/pom.xml b/backend-quarkus/pom.xml
index cb38a68723..ef3a477cc7 100644
--- a/backend-quarkus/pom.xml
+++ b/backend-quarkus/pom.xml
@@ -92,6 +92,10 @@
rest-assured
test
+
+ jakarta.validation
+ jakarta.validation-api
+
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
index b3baf095b0..d6b31c84e1 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/ConceptResource.java
@@ -1,9 +1,8 @@
package com.bakdata.conquery.quarkus.api;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
+import com.bakdata.conquery.quarkus.services.DatasetService;
import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
import com.bakdata.conquery.quarkus.util.ScopedId;
import com.fasterxml.jackson.annotation.JsonCreator;
@@ -35,23 +34,48 @@ public class ConceptResource {
description = "Returns a concept map keyed by concept id, compatible with frontend tree loading."
)
public Map getConcept(@PathParam("conceptId") @NotBlank String conceptId) {
- var concept = datasetService.requireConcept(conceptId);
- List tables = datasetService.listTablesForDataset(datasetId(concept.id())).stream().map(this::toTableResponse).toList();
+ DatasetCatalogRepository.Concept concept = datasetService.requireConcept(conceptId);
+ String datasetId = ScopedId.extractDatasetId(conceptId)
+ .orElseThrow(() -> new IllegalStateException("Expected dataset-scoped concept id but got: " + conceptId));
+
+ Map nodes = new HashMap<>();
+
+ Map children = concept.children();
+
+ // Add main node
ConceptNodeResponse node = new ConceptNodeResponse(
concept.label(),
- null,
+ concept.description(),
true,
- List.of(),
+ concept.childrenIds(),
0L,
0L,
true,
- false,
- tables,
+ !children.isEmpty(),
+ datasetService.listTablesForDataset(datasetId).stream().map(this::toTableResponse).toList(),
List.of()
);
-
- return Map.of(conceptId, node);
+ nodes.put(conceptId, node);
+
+ // Add children
+ children.forEach((id, child) -> {
+ ConceptNodeResponse childNode = new ConceptNodeResponse(
+ child.label(),
+ child.description(),
+ true,
+ child.children(),
+ 0L,
+ 0L,
+ true,
+ !children.isEmpty(),
+ List.of(),
+ List.of()
+ );
+ nodes.put(id, childNode);
+ });
+
+ return nodes;
}
private TableResponse toTableResponse(DatasetCatalogRepository.TableRecord table) {
@@ -60,14 +84,14 @@ private TableResponse toTableResponse(DatasetCatalogRepository.TableRecord table
.map(column -> new FilterResponse(column.id(), column.label(), null, null, column.type().name()))
.toList();
List supportedSecondaryIds = table.columns().stream()
- .map(DatasetCatalogRepository.ColumnRecord::secondaryId)
- .filter(Objects::nonNull)
- .distinct()
- .toList();
+ .map(DatasetCatalogRepository.ColumnRecord::secondaryId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
return new TableResponse(
table.id(),
- datasetId(table.id()),
+ tableDatasetId(table.id()),
table.label(),
false,
true,
@@ -80,11 +104,6 @@ private TableResponse toTableResponse(DatasetCatalogRepository.TableRecord table
);
}
- private String datasetId(String scopedId) {
- return ScopedId.extractDatasetId(scopedId)
- .orElseThrow(() -> new IllegalStateException("Expected dataset-scoped id but got: " + scopedId));
- }
-
private ColumnResponse toColumnResponse(DatasetCatalogRepository.ColumnRecord column) {
return new ColumnResponse(
column.id(),
@@ -94,6 +113,12 @@ private ColumnResponse toColumnResponse(DatasetCatalogRepository.ColumnRecord co
);
}
+ private String tableDatasetId(String scopedId) {
+ return ScopedId.extractDatasetId(scopedId)
+ .orElseThrow(() -> new IllegalStateException("Expected dataset-scoped table id but got: " + scopedId));
+ }
+
+
@POST
@Path("/{conceptId}/resolve")
@Operation(
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetFormConfigResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetFormConfigResource.java
index 820139b59d..d590155e15 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetFormConfigResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetFormConfigResource.java
@@ -4,6 +4,8 @@
import java.util.Map;
import java.util.Set;
+import com.bakdata.conquery.quarkus.services.DatasetService;
+import com.bakdata.conquery.quarkus.services.FormConfigService;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@@ -25,10 +27,10 @@
public class DatasetFormConfigResource {
@Inject
- DatasetService datasetService;
+ DatasetService datasetService;
@Inject
- FormConfigService formConfigService;
+ FormConfigService formConfigService;
@Inject
Instance identity;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
index 7c89d6a762..8f582c872b 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetsResource.java
@@ -6,10 +6,17 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.stream.Stream;
-import com.bakdata.conquery.quarkus.api.config.EntityPreviewRuntimeConfig;
-import com.bakdata.conquery.quarkus.api.config.FormQueriesRuntimeConfig;
+import com.bakdata.conquery.quarkus.config.EntityPreviewRuntimeConfig;
+import com.bakdata.conquery.quarkus.config.FormQueriesRuntimeConfig;
+import com.bakdata.conquery.quarkus.services.DatasetService;
+import com.bakdata.conquery.quarkus.services.EntityQueryService;
+import com.bakdata.conquery.quarkus.services.QueryStateService;
+import com.bakdata.conquery.quarkus.services.QueryUploadService;
+import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
+import com.bakdata.conquery.quarkus.util.ScopedId;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -95,18 +102,18 @@ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
datasetService.requireDataset(datasetId);
java.util.Map concepts = new LinkedHashMap<>();
- datasetService.listConceptsForDataset(datasetId).forEach(entry -> concepts.put(
+ datasetService.listRootConceptsForDataset(datasetId).forEach(entry -> concepts.put(
entry.id(),
new ConceptsResponse.ConceptSummaryResponse(
entry.label(),
null,
true,
- List.of(),
+ entry.childrenIds(),
0L,
0L,
true,
- false,
- List.of(),
+ !entry.children().isEmpty(),
+ datasetService.listTablesForDataset(datasetId(entry.id())).stream().map(this::toTableResponse).toList(),
List.of()
)
));
@@ -117,6 +124,48 @@ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) {
);
}
+
+ private ConceptResource.TableResponse toTableResponse(DatasetCatalogRepository.TableRecord table) {
+ List columns = table.columns().stream().map(this::toColumnResponse).toList();
+ List filters = columns.stream()
+ .map(column -> new ConceptResource.FilterResponse(column.id(), column.label(), null, null, column.type().name()))
+ .toList();
+ List supportedSecondaryIds = table.columns().stream()
+ .map(DatasetCatalogRepository.ColumnRecord::secondaryId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+
+ return new ConceptResource.TableResponse(
+ table.id(),
+ datasetId(table.id()),
+ table.label(),
+ false,
+ true,
+ filters,
+ List.of(),
+ columns,
+ table.primaryColumn(),
+ supportedSecondaryIds,
+ null
+ );
+ }
+
+ private String datasetId(String scopedId) {
+ return ScopedId.extractDatasetId(scopedId)
+ .orElseThrow(() -> new IllegalStateException("Expected dataset-scoped id but got: " + scopedId));
+ }
+
+ private ConceptResource.ColumnResponse toColumnResponse(DatasetCatalogRepository.ColumnRecord column) {
+ return new ConceptResource.ColumnResponse(
+ column.id(),
+ column.label(),
+ column.type(),
+ column.secondaryId()
+ );
+ }
+
+
@GET
@Path("/{datasetId}/form-queries")
@Operation(
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigResource.java
index 67736302e2..cf405b20b4 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigResource.java
@@ -3,6 +3,7 @@
import java.util.List;
import java.util.Map;
+import com.bakdata.conquery.quarkus.services.FormConfigService;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@@ -23,7 +24,7 @@
public class FormConfigResource {
@Inject
- FormConfigService formConfigService;
+ FormConfigService formConfigService;
@Inject
Instance identity;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
index 7004d59c71..0f4e173512 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryResource.java
@@ -2,6 +2,7 @@
import java.util.List;
+import com.bakdata.conquery.quarkus.services.QueryStateService;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.inject.Inject;
@@ -21,7 +22,7 @@
@Produces(MediaType.APPLICATION_JSON)
public class QueryResource {
@Inject
- QueryStateService queryStateService;
+ QueryStateService queryStateService;
@GET
@Path("/{queryId}")
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java
deleted file mode 100644
index 4d362942f0..0000000000
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/TablesRuntimeConfig.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.bakdata.conquery.quarkus.api.config;
-
-import java.util.List;
-import java.util.Optional;
-
-import io.smallrye.config.ConfigMapping;
-import io.smallrye.config.WithDefault;
-
-@ConfigMapping(prefix = "conquery")
-public interface TablesRuntimeConfig {
- /**
- * Static table definitions used when no generated metadata folder is mounted.
- */
- Optional> tables();
-
- interface TableEntry {
- /**
- * Table id as exposed to frontend concept details.
- */
- String id();
-
- /**
- * Human-readable table label shown in frontend concept details.
- */
- String label();
-
- /**
- * Dataset id this table belongs to.
- */
- String dataset();
-
- /**
- * Optional primary column id for entity identity in this table.
- */
- @WithDefault("")
- String primaryColumn();
-
- /**
- * Column metadata available for filters, selects, and secondary id resolution.
- */
- List columns();
- }
-
- interface ColumnEntry {
- /**
- * Column id as exposed in frontend table/filter/select models.
- */
- String id();
-
- /**
- * Human-readable column label.
- */
- String label();
-
- /**
- * Conquery column type, for example STRING, INTEGER, DATE, MONEY, or DATE_RANGE.
- */
- String type();
-
- /**
- * Optional secondary id category represented by this column.
- */
- @WithDefault("__unset__")
- String secondaryId();
- }
-}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/ConceptsRuntimeConfig.java
similarity index 79%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/ConceptsRuntimeConfig.java
index 1d7bea8e95..ee49adba69 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/ConceptsRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/ConceptsRuntimeConfig.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api.config;
+package com.bakdata.conquery.quarkus.config;
import java.util.List;
import java.util.Optional;
@@ -23,6 +23,12 @@ interface ConceptEntry {
*/
String label();
+ /**
+ * Human-readable concept description shown in the concept tree.
+ */
+ String description();
+
+
/**
* Dataset id this concept belongs to.
*/
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetMetadataRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/DatasetMetadataRuntimeConfig.java
similarity index 92%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetMetadataRuntimeConfig.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/DatasetMetadataRuntimeConfig.java
index 32ed6f3d7b..41772c9484 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetMetadataRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/DatasetMetadataRuntimeConfig.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api.config;
+package com.bakdata.conquery.quarkus.config;
import java.util.List;
import java.util.Optional;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/DatasetsRuntimeConfig.java
similarity index 91%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/DatasetsRuntimeConfig.java
index d8707c37d7..0b7127c960 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/DatasetsRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/DatasetsRuntimeConfig.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api.config;
+package com.bakdata.conquery.quarkus.config;
import java.util.List;
import java.util.Optional;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/EntityPreviewRuntimeConfig.java
similarity index 94%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/EntityPreviewRuntimeConfig.java
index 4a71706ea8..ac80c963ed 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/EntityPreviewRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/EntityPreviewRuntimeConfig.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api.config;
+package com.bakdata.conquery.quarkus.config;
import java.util.List;
import java.util.Optional;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FormQueriesRuntimeConfig.java
similarity index 89%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FormQueriesRuntimeConfig.java
index 75ef8b359a..b0baf10a72 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FormQueriesRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FormQueriesRuntimeConfig.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api.config;
+package com.bakdata.conquery.quarkus.config;
import java.util.List;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResource.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FrontendConfigResource.java
similarity index 97%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResource.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FrontendConfigResource.java
index 369a18b968..aa51af3fdb 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigResource.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FrontendConfigResource.java
@@ -1,6 +1,5 @@
-package com.bakdata.conquery.quarkus.api.config;
+package com.bakdata.conquery.quarkus.config;
-import java.time.LocalDate;
import java.time.Year;
import java.util.List;
import java.util.Locale;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigurationResponse.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FrontendConfigurationResponse.java
similarity index 94%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigurationResponse.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FrontendConfigurationResponse.java
index c5d48992c3..c8e21bbb38 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendConfigurationResponse.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FrontendConfigurationResponse.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api.config;
+package com.bakdata.conquery.quarkus.config;
import java.time.LocalDate;
import java.util.List;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FrontendRuntimeConfig.java
similarity index 97%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FrontendRuntimeConfig.java
index c117917119..0293960ba0 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/FrontendRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/FrontendRuntimeConfig.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api.config;
+package com.bakdata.conquery.quarkus.config;
import java.util.List;
import java.util.Map;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/StorageRuntimeConfig.java
similarity index 92%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/StorageRuntimeConfig.java
index 26c6eb63e5..e1f921f7d4 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/config/StorageRuntimeConfig.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/config/StorageRuntimeConfig.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api.config;
+package com.bakdata.conquery.quarkus.config;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java
similarity index 64%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java
index 6db22dd824..92459fa2a3 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java
@@ -1,12 +1,11 @@
-package com.bakdata.conquery.quarkus.api;
+package com.bakdata.conquery.quarkus.services;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
-import com.bakdata.conquery.quarkus.api.config.StorageRuntimeConfig;
+import com.bakdata.conquery.quarkus.config.StorageRuntimeConfig;
import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository;
import com.bakdata.conquery.quarkus.storage.NamespaceStorage;
import com.bakdata.conquery.quarkus.storage.NamespaceStorageRegistry;
@@ -14,6 +13,7 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
+import org.jboss.resteasy.reactive.common.NotImplementedYet;
@ApplicationScoped
public class DatasetService {
@@ -33,12 +33,12 @@ public DatasetCatalogRepository.DatasetRecord requireDataset(String datasetId) {
return requireNamespace(datasetId).dataset();
}
- public DatasetCatalogRepository.ConceptRecord requireConcept(String conceptId) {
+ public DatasetCatalogRepository.Concept requireConcept(String conceptId) {
NamespaceStorage namespace = requireNamespaceForScopedObject(conceptId, "concept");
return namespace.findConcept(conceptId).orElseThrow(() -> new NotFoundException("Unknown concept: " + conceptId));
}
- public List listConceptsForDataset(String datasetId) {
+ public List listRootConceptsForDataset(String datasetId) {
return requireNamespace(datasetId).listConcepts();
}
@@ -52,33 +52,34 @@ public DatasetCatalogRepository.TableRecord requireTable(String tableId) {
}
public ConceptCodeResolution resolveConceptCodes(String rootConceptId, List codes) {
- requireConcept(rootConceptId);
+ DatasetCatalogRepository.Concept rootConcept = requireConcept(rootConceptId);
NamespaceStorage namespace = requireNamespaceForScopedObject(rootConceptId, "concept");
- // TODO This does not look right yet: Concept codes need to be resolved against the leaf-children of the provided root concept
- Map lookupByCode = namespace.listConcepts().stream()
- .flatMap(concept -> java.util.stream.Stream.of(
- Map.entry(normalizeCode(concept.id()), concept.id()),
- Map.entry(normalizeCode(concept.label()), concept.id())
- ))
- .collect(Collectors.toMap(
- Map.Entry::getKey,
- Map.Entry::getValue,
- (existing, ignored) -> existing
- ));
-
- List resolvedInOrder = codes.stream()
- .map(this::normalizeCode)
- .map(lookupByCode::get)
- .filter(java.util.Objects::nonNull)
- .toList();
- List unknownCodes = codes.stream()
- .filter(code -> !lookupByCode.containsKey(normalizeCode(code)))
- .toList();
-
- return new ConceptCodeResolution(List.copyOf(new LinkedHashSet<>(resolvedInOrder)), unknownCodes);
+ // TODO implement with updated concept model
+ throw new NotImplementedYet();
+
+// List resolvedInOrder = codes.stream()
+// .map(this::normalizeCode)
+// .map(lookupByCode::get)
+// .filter(java.util.Objects::nonNull)
+// .toList();
+// List unknownCodes = codes.stream()
+// .filter(code -> !lookupByCode.containsKey(normalizeCode(code)))
+// .toList();
+//
+// return new ConceptCodeResolution(List.copyOf(new LinkedHashSet<>(resolvedInOrder)), unknownCodes);
}
+ private Stream> resolvableCodeEntries(DatasetCatalogRepository.ConceptElement concept) {
+ if (concept.condition() == null) {
+ return Stream.empty();
+ }
+ return concept.condition().connectorValues().stream()
+ .map(this::normalizeCode)
+ .map(code -> Map.entry(code, concept.id()));
+ }
+
+
private String normalizeCode(String value) {
return value.trim().toLowerCase(Locale.ROOT);
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/EntityQueryService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/EntityQueryService.java
similarity index 94%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/EntityQueryService.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/EntityQueryService.java
index 7fc69ee2ac..501bb14c5b 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/EntityQueryService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/EntityQueryService.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api;
+package com.bakdata.conquery.quarkus.services;
import java.util.ArrayList;
import java.util.List;
@@ -6,7 +6,8 @@
import java.util.Optional;
import java.util.stream.Collectors;
-import com.bakdata.conquery.quarkus.api.config.FrontendRuntimeConfig;
+import com.bakdata.conquery.quarkus.api.QuerySubmissionPayload;
+import com.bakdata.conquery.quarkus.config.FrontendRuntimeConfig;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/FormConfigService.java
similarity index 95%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/FormConfigService.java
index 477dc91a54..fb06edd2d7 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/FormConfigService.java
@@ -1,10 +1,12 @@
-package com.bakdata.conquery.quarkus.api;
+package com.bakdata.conquery.quarkus.services;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.UUID;
+import com.bakdata.conquery.quarkus.api.DatasetFormConfigResource;
+import com.bakdata.conquery.quarkus.api.FormConfigResource;
import com.bakdata.conquery.quarkus.storage.meta.ManagerMetaStorage;
import com.bakdata.conquery.quarkus.storage.model.StoredFormConfig;
import jakarta.enterprise.context.ApplicationScoped;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/QueryStateService.java
similarity index 93%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/QueryStateService.java
index 50ba02305c..daa11b9b9d 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryStateService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/QueryStateService.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api;
+package com.bakdata.conquery.quarkus.services;
import java.time.Instant;
import java.util.ArrayList;
@@ -7,6 +7,9 @@
import java.util.Locale;
import java.util.UUID;
+import com.bakdata.conquery.quarkus.api.DatasetsResource;
+import com.bakdata.conquery.quarkus.api.QueryResource;
+import com.bakdata.conquery.quarkus.api.QuerySubmissionPayload;
import com.bakdata.conquery.quarkus.storage.meta.ManagerMetaStorage;
import com.bakdata.conquery.quarkus.storage.model.StoredQuery;
import jakarta.enterprise.context.ApplicationScoped;
@@ -246,20 +249,13 @@ private boolean containsDates(QuerySubmissionPayload.QueryNode node) {
if (node instanceof QuerySubmissionPayload.DateRestrictionNode || node instanceof QuerySubmissionPayload.TemporalNode) {
return true;
}
- if (node instanceof QuerySubmissionPayload.AndNode andNode) {
- return anyContainsDates(andNode.children);
- }
- if (node instanceof QuerySubmissionPayload.OrNode orNode) {
- return anyContainsDates(orNode.children);
- }
- if (node instanceof QuerySubmissionPayload.NegationNode negationNode) {
- return containsDates(negationNode.child);
- }
- if (node instanceof QuerySubmissionPayload.DateRestrictionNode dateRestrictionNode) {
- return containsDates(dateRestrictionNode.child);
- }
- return false;
- }
+ return switch (node) {
+ case QuerySubmissionPayload.AndNode andNode -> anyContainsDates(andNode.children);
+ case QuerySubmissionPayload.OrNode orNode -> anyContainsDates(orNode.children);
+ case QuerySubmissionPayload.NegationNode negationNode -> containsDates(negationNode.child);
+ default -> false;
+ };
+ }
private boolean anyContainsDates(List children) {
if (children == null || children.isEmpty()) {
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryUploadService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/QueryUploadService.java
similarity index 96%
rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryUploadService.java
rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/QueryUploadService.java
index 229c9f7b55..5a3bc6116f 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryUploadService.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/QueryUploadService.java
@@ -1,4 +1,4 @@
-package com.bakdata.conquery.quarkus.api;
+package com.bakdata.conquery.quarkus.services;
import java.util.ArrayList;
import java.util.List;
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java
index fc55e24cd0..78c0b36e8e 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java
@@ -6,9 +6,6 @@
import java.util.Map;
import java.util.Optional;
-import com.bakdata.conquery.quarkus.api.config.ConceptsRuntimeConfig;
-import com.bakdata.conquery.quarkus.api.config.DatasetsRuntimeConfig;
-import com.bakdata.conquery.quarkus.api.config.TablesRuntimeConfig;
import com.bakdata.conquery.quarkus.util.ScopedId;
import io.quarkus.arc.properties.IfBuildProperty;
import jakarta.annotation.PostConstruct;
@@ -19,49 +16,19 @@
@IfBuildProperty(name = "conquery.storage.backend", stringValue = "IN_MEMORY", enableIfMissing = true)
public class ConfigDatasetCatalogRepository implements DatasetCatalogRepository {
- @Inject
- DatasetsRuntimeConfig datasetsConfig;
-
- @Inject
- ConceptsRuntimeConfig conceptsConfig;
-
- @Inject
- TablesRuntimeConfig tablesConfig;
-
@Inject
DatasetMetadataFolderLoader metadataFolderLoader;
private Map datasetsById = Map.of();
- private Map> conceptsByDatasetId = Map.of();
+ private Map> conceptsByDatasetId = Map.of();
private Map> tablesByDatasetId = Map.of();
@PostConstruct
void init() {
Map datasetIndex = new LinkedHashMap<>();
- Map> conceptDatasetIndex = new LinkedHashMap<>();
+ Map> conceptDatasetIndex = new LinkedHashMap<>();
Map> tableDatasetIndex = new LinkedHashMap<>();
- datasetsConfig.datasets().orElse(List.of()).forEach(dataset -> datasetIndex.put(dataset.id(), new DatasetRecord(dataset.id(), dataset.label())));
- conceptsConfig.concepts().orElse(List.of()).forEach(concept -> {
- validateScopedIdBelongsToDataset(concept.id(), concept.dataset(), "concept");
- ConceptRecord record = new ConceptRecord(concept.id(), concept.label());
- conceptDatasetIndex.computeIfAbsent(concept.dataset(), ignored -> new LinkedHashMap<>()).put(record.id(), record);
- });
- tablesConfig.tables().orElse(List.of()).forEach(table -> {
- validateScopedIdBelongsToDataset(table.id(), table.dataset(), "table");
- TableRecord record = new TableRecord(
- table.id(),
- table.label(),
- table.columns().stream().map(column -> new ColumnRecord(
- column.id(),
- column.label(),
- parseColumnType(column.type()),
- blankToNull(column.secondaryId())
- )).toList(),
- blankToNull(table.primaryColumn())
- );
- tableDatasetIndex.computeIfAbsent(table.dataset(), ignored -> new LinkedHashMap<>()).put(record.id(), record);
- });
metadataFolderLoader.loadConfiguredDatasets().forEach(dataset -> {
datasetIndex.put(dataset.dataset().id(), dataset.dataset());
conceptDatasetIndex.put(dataset.dataset().id(), new LinkedHashMap<>(dataset.conceptsById()));
@@ -94,23 +61,18 @@ public boolean deleteDataset(String datasetId) {
}
@Override
- public List listConcepts() {
- return conceptsByDatasetId.values().stream().flatMap(map -> map.values().stream()).toList();
- }
-
- @Override
- public List listConceptsForDataset(String datasetId) {
+ public List listConceptsForDataset(String datasetId) {
return conceptsByDatasetId.getOrDefault(datasetId, Map.of()).values().stream().toList();
}
@Override
- public Optional findConcept(String conceptId) {
+ public Optional findConcept(String conceptId) {
return ScopedId.extractDatasetId(conceptId)
.flatMap(datasetId -> Optional.ofNullable(conceptsByDatasetId.getOrDefault(datasetId, Map.of()).get(conceptId)));
}
@Override
- public void saveConcept(ConceptRecord concept) {
+ public void saveConcept(Concept concept) {
throw readOnly();
}
@@ -119,12 +81,6 @@ public boolean deleteConcept(String conceptId) {
throw readOnly();
}
- @Override
- public List listTables() {
- return tablesByDatasetId.values().stream().flatMap(map -> map.values().stream()).toList();
- }
-
- @Override
public List listTablesForDataset(String datasetId) {
return tablesByDatasetId.getOrDefault(datasetId, Map.of()).values().stream().toList();
}
diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java
index b39e2f65ef..f0a6ca0afe 100644
--- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java
+++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java
@@ -1,7 +1,6 @@
package com.bakdata.conquery.quarkus.storage;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
public interface DatasetCatalogRepository {
@@ -13,18 +12,14 @@ public interface DatasetCatalogRepository {
boolean deleteDataset(String datasetId);
- List listConcepts();
+ List listConceptsForDataset(String datasetId);
- List listConceptsForDataset(String datasetId);
+ Optional findConcept(String conceptId);
- Optional findConcept(String conceptId);
-
- void saveConcept(ConceptRecord concept);
+ void saveConcept(Concept concept);
boolean deleteConcept(String conceptId);
- List listTables();
-
List listTablesForDataset(String datasetId);
Optional findTable(String tableId);
@@ -39,10 +34,81 @@ record DatasetRecord(
) {
}
- record ConceptRecord(
+ record ConceptElement(
String id,
- String label
+ String label,
+ String description,
+ String parentId,
+ List children,
+ ConceptCondition condition
+ ) {
+ public ConceptElement(String id, String label, String description) {
+ this(id, label, description,null, List.of(), null);
+ }
+
+ public ConceptElement {
+ children = children == null ? List.of() : List.copyOf(children);
+ }
+
+ public boolean hasResolvableCodes() {
+ return condition != null && !condition.connectorValues().isEmpty();
+ }
+ }
+
+ record Concept(
+ String id,
+ String label,
+ String description,
+ // All Children flat lookup
+ Map children,
+ // Direct children
+ List childrenIds,
+ List connectors
+
+ ){}
+
+ record Connector(
+ String column,
+ String label,
+ String name,
+ List