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 getFormQueries(@PathParam("datasetId") String datasetId) { datasetService.requireDataset(datasetId); return formQueriesConfig.resources().stream().map(this::loadFormResource).toList(); } @@ -151,13 +150,13 @@ private static String resolveUserName(Instance identityInstanc return "anonymous"; } - private JsonNode loadFormResource(String path) { + private Object 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); + return objectMapper.readValue(input, Object.class); } catch (IOException e) { throw new UncheckedIOException("Failed to parse form resource: " + path, e); @@ -173,7 +172,7 @@ public record QuerySummaryResponse( boolean own, String ownerName, boolean system, - List resultUrls, + List resultUrls, boolean shared, boolean canExpand, String queryType, @@ -182,12 +181,6 @@ public record QuerySummaryResponse( ) { } - public record ResultUrlResponse( - String label, - String url - ) { - } - public record StartQueryResponse( String id ) { 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 d663a96d56..0c522c181c 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 @@ -67,7 +67,10 @@ public void cancelQuery(@PathParam("queryId") String queryId) { @DiscriminatorMapping(value = "CANCELED", schema = CanceledQueryResponse.class) } ) - public abstract static class QueryResponse { + public interface QueryResponse { + } + + public abstract static class BaseQueryResponse implements QueryResponse { public final String id; public final String label; public final String createdAt; @@ -83,7 +86,7 @@ public abstract static class QueryResponse { public final List availableSecondaryIds; public final QueryStatus status; - protected QueryResponse( + protected BaseQueryResponse( String id, String label, String createdAt, @@ -116,7 +119,7 @@ protected QueryResponse( } } - public static final class NewQueryResponse extends QueryResponse { + public static final class NewQueryResponse extends BaseQueryResponse { public NewQueryResponse( String id, String label, @@ -136,7 +139,7 @@ public NewQueryResponse( } } - public static final class RunningQueryResponse extends QueryResponse { + public static final class RunningQueryResponse extends BaseQueryResponse { public final Double progress; public RunningQueryResponse( @@ -160,7 +163,7 @@ public RunningQueryResponse( } } - public static final class DoneQueryResponse extends QueryResponse { + public static final class DoneQueryResponse extends BaseQueryResponse { public final Long numberOfResults; public final List resultUrls; public final QueryType queryType; @@ -196,7 +199,7 @@ public DoneQueryResponse( } } - public static final class FailedQueryResponse extends QueryResponse { + public static final class FailedQueryResponse extends BaseQueryResponse { public final ErrorResponse error; public FailedQueryResponse( @@ -220,7 +223,7 @@ public FailedQueryResponse( } } - public static final class CanceledQueryResponse extends QueryResponse { + public static final class CanceledQueryResponse extends BaseQueryResponse { public final ErrorResponse error; public CanceledQueryResponse( From 1f0a0487c9fa78fb7d1f968bcb26acf35832f41c Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Wed, 8 Apr 2026 07:54:40 +0200 Subject: [PATCH 19/45] adds patching of query metadata and deleting --- .../conquery/quarkus/api/QueryResource.java | 32 +++++ .../quarkus/api/QueryStateService.java | 111 ++++++++++++++---- 2 files changed, 123 insertions(+), 20 deletions(-) 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 0c522c181c..7004d59c71 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 @@ -5,7 +5,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; @@ -41,6 +43,29 @@ public void cancelQuery(@PathParam("queryId") String queryId) { queryStateService.cancelQuery(queryId); } + @PATCH + @Path("/{queryId}") + @Operation( + summary = "Patch query metadata", + description = "Updates mutable query metadata like label, tags, sharing and groups." + ) + public void patchQuery(@PathParam("queryId") String queryId, QueryPatchPayload payload) { + queryStateService.patchQuery( + queryId, + new QueryStateService.QueryPatch(payload.label, payload.tags, payload.shared, payload.groups) + ); + } + + @DELETE + @Path("/{queryId}") + @Operation( + summary = "Delete a query", + description = "Deletes a query from history." + ) + public void deleteQuery(@PathParam("queryId") String queryId) { + queryStateService.deleteQuery(queryId); + } + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "status", visible = true) @JsonSubTypes({ @JsonSubTypes.Type(value = NewQueryResponse.class, name = "NEW"), @@ -310,4 +335,11 @@ public record ResultUrlResponse( String url ) { } + + public static final class QueryPatchPayload { + public String label; + public List tags; + public Boolean shared; + public List groups; + } } 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 daaebfa382..498474a92f 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 @@ -32,7 +32,10 @@ public DatasetsResource.StartQueryResponse createQuery(String datasetId, QuerySu definition, payload != null ? payload.secondaryId : null, containsDates(payload != null ? payload.root : null), - QueryResource.QueryStatus.NEW + QueryResource.QueryStatus.NEW, + List.of(), + false, + List.of() ); queriesById.put(queryId, stored); @@ -60,18 +63,41 @@ public void cancelQuery(String queryId) { } } + public void patchQuery(String queryId, QueryPatch patch) { + StoredQuery query = requireQuery(queryId); + if (patch.label() != null && !patch.label().isBlank()) { + query.setLabel(patch.label()); + } + if (patch.tags() != null) { + query.setTags(List.copyOf(patch.tags())); + } + if (patch.shared() != null) { + query.setShared(patch.shared()); + } + if (patch.groups() != null) { + query.setGroups(List.copyOf(patch.groups())); + } + } + + public void deleteQuery(String queryId) { + StoredQuery removed = queriesById.remove(queryId); + if (removed == null) { + throw new NotFoundException("Unknown query: " + 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(), - List.of(), + query.tags(), true, query.ownerName(), false, List.of(), - false, + query.shared(), false, query.definition().type.name(), query.secondaryId(), @@ -86,13 +112,13 @@ private QueryResource.QueryResponse toQueryResponse(StoredQuery query) { query.label(), query.createdAt().toString(), true, + query.shared(), false, - false, - List.of(), + query.tags(), query.definition(), query.secondaryId(), query.ownerName(), - List.of(), + query.groups(), false, List.of(), null @@ -102,13 +128,13 @@ private QueryResource.QueryResponse toQueryResponse(StoredQuery query) { query.label(), query.createdAt().toString(), true, + query.shared(), false, - false, - List.of(), + query.tags(), query.definition(), query.secondaryId(), query.ownerName(), - List.of(), + query.groups(), false, List.of(), 0L, @@ -122,13 +148,13 @@ private QueryResource.QueryResponse toQueryResponse(StoredQuery query) { query.label(), query.createdAt().toString(), true, + query.shared(), false, - false, - List.of(), + query.tags(), query.definition(), query.secondaryId(), query.ownerName(), - List.of(), + query.groups(), false, List.of(), new QueryResource.ErrorResponse("Query failed", "FAILED") @@ -138,13 +164,13 @@ private QueryResource.QueryResponse toQueryResponse(StoredQuery query) { query.label(), query.createdAt().toString(), true, + query.shared(), false, - false, - List.of(), + query.tags(), query.definition(), query.secondaryId(), query.ownerName(), - List.of(), + query.groups(), false, List.of(), new QueryResource.ErrorResponse("Query canceled", "CANCELED") @@ -154,13 +180,13 @@ private QueryResource.QueryResponse toQueryResponse(StoredQuery query) { query.label(), query.createdAt().toString(), true, + query.shared(), false, - false, - List.of(), + query.tags(), query.definition(), query.secondaryId(), query.ownerName(), - List.of(), + query.groups(), false, List.of() ); @@ -239,13 +265,16 @@ private static String labelFor(String queryId) { private static final class StoredQuery { private final String id; private final String datasetId; - private final String label; + private volatile 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; private StoredQuery( String id, @@ -256,7 +285,10 @@ private StoredQuery( QueryResource.QueryDefinition definition, String secondaryId, boolean containsDates, - QueryResource.QueryStatus status + QueryResource.QueryStatus status, + List tags, + boolean shared, + List groups ) { this.id = id; this.datasetId = datasetId; @@ -267,6 +299,9 @@ private StoredQuery( this.secondaryId = secondaryId; this.containsDates = containsDates; this.status = status; + this.tags = tags; + this.shared = shared; + this.groups = groups; } public String id() { @@ -281,6 +316,10 @@ public String label() { return label; } + public void setLabel(String label) { + this.label = label; + } + public Instant createdAt() { return createdAt; } @@ -308,5 +347,37 @@ public QueryResource.QueryStatus 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; + } + } + + public record QueryPatch( + String label, + List tags, + Boolean shared, + List groups + ) { } } From b746ec970a3582b0081e3965116abb3643f73da1 Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:22:28 +0200 Subject: [PATCH 20/45] refactor type discrimination on query submission payload --- .../quarkus/api/QueryStateService.java | 34 ++++++++-- .../quarkus/api/QuerySubmissionPayload.java | 63 ++++++++++++++----- 2 files changed, 77 insertions(+), 20 deletions(-) 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 498474a92f..e5c429d7ea 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 @@ -30,8 +30,8 @@ public DatasetsResource.StartQueryResponse createQuery(String datasetId, QuerySu createdAt, ownerName, definition, - payload != null ? payload.secondaryId : null, - containsDates(payload != null ? payload.root : null), + extractSecondaryId(payload), + containsDates(extractRoot(payload)), QueryResource.QueryStatus.NEW, List.of(), false, @@ -194,14 +194,20 @@ private QueryResource.QueryResponse toQueryResponse(StoredQuery query) { } private QueryResource.QueryDefinition toQueryDefinition(QuerySubmissionPayload payload, QueryResource.QueryType queryType) { - QuerySubmissionPayload.QueryNode root = payload != null ? payload.root : null; + QuerySubmissionPayload.QueryNode root = extractRoot(payload); return switch (queryType) { - case SECONDARY_ID_QUERY -> new QueryResource.SecondaryIdQueryDefinition(root, payload != null ? payload.secondaryId : null); + case SECONDARY_ID_QUERY -> new QueryResource.SecondaryIdQueryDefinition(root, extractSecondaryId(payload)); case CONCEPT_QUERY -> new QueryResource.ConceptQueryDefinition(root); }; } private QueryResource.QueryType resolveQueryType(QuerySubmissionPayload payload) { + if (payload instanceof QuerySubmissionPayload.SecondaryIdQuerySubmissionPayload) { + return QueryResource.QueryType.SECONDARY_ID_QUERY; + } + if (payload instanceof QuerySubmissionPayload.ConceptQuerySubmissionPayload) { + return QueryResource.QueryType.CONCEPT_QUERY; + } if (payload != null && payload.type != null) { try { return QueryResource.QueryType.valueOf(payload.type.toUpperCase(Locale.ROOT)); @@ -210,12 +216,30 @@ private QueryResource.QueryType resolveQueryType(QuerySubmissionPayload payload) // Fall through to heuristic handling. } } - if (payload != null && payload.secondaryId != null && !payload.secondaryId.isBlank()) { + String secondaryId = extractSecondaryId(payload); + if (secondaryId != null && !secondaryId.isBlank()) { return QueryResource.QueryType.SECONDARY_ID_QUERY; } return QueryResource.QueryType.CONCEPT_QUERY; } + private String extractSecondaryId(QuerySubmissionPayload payload) { + if (payload instanceof QuerySubmissionPayload.SecondaryIdQuerySubmissionPayload secondaryIdPayload) { + return secondaryIdPayload.secondaryId; + } + return null; + } + + private QuerySubmissionPayload.QueryNode extractRoot(QuerySubmissionPayload payload) { + if (payload instanceof QuerySubmissionPayload.ConceptQuerySubmissionPayload conceptPayload) { + return conceptPayload.root; + } + if (payload instanceof QuerySubmissionPayload.SecondaryIdQuerySubmissionPayload secondaryIdPayload) { + return secondaryIdPayload.root; + } + return null; + } + private boolean containsDates(QuerySubmissionPayload.QueryNode node) { if (node == null) { return false; 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 index 603ade0346..b216b5ed3b 100644 --- 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 @@ -1,34 +1,67 @@ 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 { +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = QuerySubmissionPayload.GenericQuerySubmissionPayload.class +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = QuerySubmissionPayload.ConceptQuerySubmissionPayload.class, name = "CONCEPT_QUERY"), + @JsonSubTypes.Type(value = QuerySubmissionPayload.SecondaryIdQuerySubmissionPayload.class, name = "SECONDARY_ID_QUERY"), + @JsonSubTypes.Type(value = QuerySubmissionPayload.GenericQuerySubmissionPayload.class, name = "FORM_QUERY") +}) +@Schema( + description = "Query submission payload, discriminated by `type`.", + discriminatorProperty = "type", + oneOf = { + QuerySubmissionPayload.ConceptQuerySubmissionPayload.class, + QuerySubmissionPayload.SecondaryIdQuerySubmissionPayload.class, + QuerySubmissionPayload.GenericQuerySubmissionPayload.class + }, + discriminatorMapping = { + @DiscriminatorMapping(value = "CONCEPT_QUERY", schema = QuerySubmissionPayload.ConceptQuerySubmissionPayload.class), + @DiscriminatorMapping(value = "SECONDARY_ID_QUERY", schema = QuerySubmissionPayload.SecondaryIdQuerySubmissionPayload.class), + @DiscriminatorMapping(value = "FORM_QUERY", schema = QuerySubmissionPayload.GenericQuerySubmissionPayload.class) + } +) +public abstract 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<>(); + public static final class ConceptQuerySubmissionPayload extends QuerySubmissionPayload { + @Schema(description = "Query root node. Polymorphic and discriminated by field `type`.") + public QueryNode root; + + public ConceptQuerySubmissionPayload() { + this.type = "CONCEPT_QUERY"; + } + } + + public static final class SecondaryIdQuerySubmissionPayload extends QuerySubmissionPayload { + public String secondaryId; + @Schema(description = "Query root node. Polymorphic and discriminated by field `type`.") + public QueryNode root; - @JsonAnySetter - public void setAdditionalProperty(String name, Object value) { - additionalProperties.put(name, value); + public SecondaryIdQuerySubmissionPayload() { + this.type = "SECONDARY_ID_QUERY"; + } } - @JsonAnyGetter - public Map getAdditionalProperties() { - return additionalProperties; + public static final class GenericQuerySubmissionPayload extends QuerySubmissionPayload { + public Map values; + + public GenericQuerySubmissionPayload() { + } } @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") From ae00d44f791b56d193ba0790c960c9e24834a134 Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:41:26 +0200 Subject: [PATCH 21/45] adds endpoint for entity upload --- .../quarkus/api/DatasetsResource.java | 43 +++++++++++++++++ .../quarkus/api/QueryUploadService.java | 48 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryUploadService.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 8ce1396eb1..6ce2a9cc6c 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,10 @@ import io.quarkus.security.identity.SecurityIdentity; import jakarta.enterprise.inject.Instance; 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; @@ -39,6 +43,9 @@ public class DatasetsResource { @Inject QueryStateService queryStateService; + @Inject + QueryUploadService queryUploadService; + @Inject Instance identity; @@ -136,6 +143,23 @@ public StartQueryResponse postQueries(@PathParam("datasetId") String datasetId, return queryStateService.createQuery(datasetId, payload, resolveUserName(identity)); } + @POST + @Path("/{datasetId}/queries/upload") + @Operation( + summary = "Upload query entities", + description = "Uploads entity id rows for query upload workflow." + ) + public UploadQueryResponse uploadQueries( + @PathParam("datasetId") String datasetId, + @Valid @NotNull QueryUploadPayload payload + ) { + datasetService.requireDataset(datasetId); + QueryUploadService.UploadResult result = queryUploadService.processUpload( + new QueryUploadService.QueryUploadPayload(payload.format, payload.values, payload.label) + ); + return new UploadQueryResponse(result.resolved(), result.unresolvedId(), result.unreadableDate()); + } + private static String resolveUserName(Instance identityInstance) { if (identityInstance.isResolvable()) { SecurityIdentity securityIdentity = identityInstance.get(); @@ -186,6 +210,25 @@ public record StartQueryResponse( ) { } + public record UploadQueryResponse( + int resolved, + List> unresolvedId, + List> unreadableDate + ) { + } + + public static final class QueryUploadPayload { + public final @NotNull @NotEmpty List<@NotBlank String> format; + public final @NotNull List<@NotNull List<@NotBlank String>> values; + public final @NotBlank String label; + + public QueryUploadPayload(List format, List> values, String label) { + this.format = format; + this.values = values; + this.label = label; + } + } + public record DatasetResponse( String id, String label 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/api/QueryUploadService.java new file mode 100644 index 0000000000..229c9f7b55 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/QueryUploadService.java @@ -0,0 +1,48 @@ +package com.bakdata.conquery.quarkus.api; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class QueryUploadService { + + public UploadResult processUpload(QueryUploadPayload payload) { + int expectedColumns = payload.format().size(); + List> unresolvedId = new ArrayList<>(); + List> unreadableDate = new ArrayList<>(); + int resolved = 0; + + for (List row : payload.values()) { + if (row == null || row.isEmpty()) { + unresolvedId.add(List.of()); + continue; + } + if (row.size() < expectedColumns) { + unresolvedId.add(List.copyOf(row)); + continue; + } + // TODO(quarkus-migration): Implement actual ID resolution against dataset contents. + // We currently only validate row shape. We still need to verify that externally + // provided IDs are known in the selected dataset and mark unknown ones unresolved. + resolved++; + } + + return new UploadResult(resolved, unresolvedId, unreadableDate); + } + + public record QueryUploadPayload( + List format, + List> values, + String label + ) { + } + + public record UploadResult( + int resolved, + List> unresolvedId, + List> unreadableDate + ) { + } +} From 1de6b0f6e6b2961e0ff36e73fcee4e9cc94be40e Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:35:08 +0200 Subject: [PATCH 22/45] adds entity enpoints and placeholder logic --- .../quarkus/api/DatasetsResource.java | 69 +++++++++ .../quarkus/api/EntityQueryService.java | 131 ++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/EntityQueryService.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 6ce2a9cc6c..285eadb035 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 @@ -5,6 +5,7 @@ import java.io.UncheckedIOException; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import com.bakdata.conquery.quarkus.api.config.EntityPreviewRuntimeConfig; @@ -46,6 +47,9 @@ public class DatasetsResource { @Inject QueryUploadService queryUploadService; + @Inject + EntityQueryService entityQueryService; + @Inject Instance identity; @@ -160,6 +164,45 @@ public UploadQueryResponse uploadQueries( return new UploadQueryResponse(result.resolved(), result.unresolvedId(), result.unreadableDate()); } + @POST + @Path("/{datasetId}/queries/entity") + @Operation( + summary = "Get entity history", + description = "Returns history data for a single entity." + ) + public EntityQueryService.EntityHistoryResponse getEntityHistory( + @PathParam("datasetId") String datasetId, + @Valid @NotNull EntityHistoryRequest payload + ) { + datasetService.requireDataset(datasetId); + return entityQueryService.getEntityHistory( + new EntityQueryService.EntityHistoryRequest( + payload.idKind, + payload.entityId, + payload.time, + payload.sources + ) + ); + } + + @POST + @Path("/{datasetId}/queries/resolve-entities") + @Operation( + summary = "Resolve entities from filter values", + description = "Resolves entity ids from selected filter values." + ) + public List> resolveEntities( + @PathParam("datasetId") String datasetId, + @Valid @NotNull List<@Valid FilterValuesRequest> payload + ) { + datasetService.requireDataset(datasetId); + return entityQueryService.resolveEntities( + payload.stream() + .map(request -> new EntityQueryService.FilterValuesRequest(request.filter, request.type, request.value)) + .toList() + ); + } + private static String resolveUserName(Instance identityInstance) { if (identityInstance.isResolvable()) { SecurityIdentity securityIdentity = identityInstance.get(); @@ -229,6 +272,32 @@ public QueryUploadPayload(List format, List> values, String } } + public static final class EntityHistoryRequest { + public final @NotBlank String idKind; + public final @NotBlank String entityId; + public final @NotNull @Valid QuerySubmissionPayload.DateRangePayload time; + public final @NotNull @NotEmpty List<@NotBlank String> sources; + + public EntityHistoryRequest(String idKind, String entityId, QuerySubmissionPayload.DateRangePayload time, List sources) { + this.idKind = idKind; + this.entityId = entityId; + this.time = time; + this.sources = sources; + } + } + + public static final class FilterValuesRequest { + public final @NotBlank String filter; + public final @NotBlank String type; + public final @NotNull @NotEmpty List<@NotBlank String> value; + + public FilterValuesRequest(String filter, String type, List value) { + this.filter = filter; + this.type = type; + this.value = value; + } + } + public record DatasetResponse( String id, String label 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/api/EntityQueryService.java new file mode 100644 index 0000000000..7fc69ee2ac --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/EntityQueryService.java @@ -0,0 +1,131 @@ +package com.bakdata.conquery.quarkus.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.bakdata.conquery.quarkus.api.config.FrontendRuntimeConfig; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class EntityQueryService { + + @Inject + FrontendRuntimeConfig frontendConfig; + + public EntityHistoryResponse getEntityHistory(EntityHistoryRequest request) { + return new EntityHistoryResponse( + List.of(), + List.of(), + List.of( + new EntityInfoResponse( + "entityId", + request.entityId(), + "STRING", + List.of() + ) + ), + List.of() + ); + } + + public List> resolveEntities(List filters) { + String idKind = resolveOutputIdKind(); + return filters.stream() + .flatMap(filter -> filter.value().stream().map(value -> Map.of(idKind, syntheticEntityId(filter.filter(), value)))) + .distinct() + .collect(Collectors.toCollection(ArrayList::new)); + } + + private String resolveOutputIdKind() { + Optional> configured = frontendConfig.queryUpload().ids(); + if (configured.isPresent() && !configured.get().isEmpty()) { + return configured.get().stream() + .filter(FrontendRuntimeConfig.IdColumn::print) + .findFirst() + .map(FrontendRuntimeConfig.IdColumn::name) + .orElse(configured.get().getFirst().name()); + } + return "ID"; + } + + private String syntheticEntityId(String filterId, String filterValue) { + // TODO(quarkus-migration): Replace synthetic IDs with actual entity resolution against dataset data. + // The legacy backend executes a concept query and returns matching entity IDs. + String input = (filterId == null ? "" : filterId) + "|" + (filterValue == null ? "" : filterValue); + return "entity-" + Integer.toUnsignedString(input.hashCode(), 36); + } + + public record EntityHistoryRequest( + String idKind, + String entityId, + QuerySubmissionPayload.DateRangePayload time, + List sources + ) { + } + + public record FilterValuesRequest( + String filter, + String type, + List value + ) { + } + + public record EntityHistoryResponse( + List resultUrls, + List columnDescriptions, + List infos, + List timeStratifiedInfos + ) { + } + + public record ResultUrlWithLabelResponse( + String label, + String url + ) { + } + + public record ColumnDescriptionResponse( + String label, + String description, + String type, + List> semantics, + String defaultLabel, + String selectId, + String userConceptLabel + ) { + } + + public record EntityInfoResponse( + String label, + String value, + String type, + List> semantics + ) { + } + + public record TimeStratifiedInfoResponse( + String label, + String description, + Map totals, + List columns, + List years + ) { + } + + public record TimeStratifiedInfoYearResponse( + int year, + Map values, + List quarters + ) { + } + + public record TimeStratifiedInfoQuarterResponse( + int quarter, + Map values + ) { + } +} From 61bbdc3c53423c8aad8db5e0d897e625ddf262d1 Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:43:05 +0200 Subject: [PATCH 23/45] adds discriminator for filtervalue --- .../quarkus/api/DatasetsResource.java | 162 +++++++++++++++++- 1 file changed, 157 insertions(+), 5 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 285eadb035..42c7a77db9 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 @@ -10,6 +10,8 @@ import com.bakdata.conquery.quarkus.api.config.EntityPreviewRuntimeConfig; import com.bakdata.conquery.quarkus.api.config.FormQueriesRuntimeConfig; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.security.identity.SecurityIdentity; import jakarta.enterprise.inject.Instance; @@ -25,6 +27,7 @@ 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; @Path("/api/datasets") @Produces(MediaType.APPLICATION_JSON) @@ -193,16 +196,38 @@ public EntityQueryService.EntityHistoryResponse getEntityHistory( ) public List> resolveEntities( @PathParam("datasetId") String datasetId, - @Valid @NotNull List<@Valid FilterValuesRequest> payload + @Valid @NotNull List<@Valid FilterValueRequest> payload ) { datasetService.requireDataset(datasetId); return entityQueryService.resolveEntities( payload.stream() - .map(request -> new EntityQueryService.FilterValuesRequest(request.filter, request.type, request.value)) + .map(this::toFilterValuesRequest) .toList() ); } + private EntityQueryService.FilterValuesRequest toFilterValuesRequest(FilterValueRequest request) { + return new EntityQueryService.FilterValuesRequest( + request.filter, + request.type, + extractFilterValues(request) + ); + } + + private List extractFilterValues(FilterValueRequest request) { + return switch (request) { + case MultiSelectFilterValueRequest multiSelect -> multiSelect.value; + case BigMultiSelectFilterValueRequest bigMultiSelect -> bigMultiSelect.value; + case SelectFilterValueRequest select -> List.of(select.value); + case StringFilterValueRequest string -> List.of(string.value); + case IntegerFilterValueRequest integer -> List.of(String.valueOf(integer.value)); + case IntegerRangeFilterValueRequest integerRange -> List.of(String.valueOf(integerRange.value)); + case MoneyRangeFilterValueRequest moneyRange -> List.of(String.valueOf(moneyRange.value)); + case RealFilterValueRequest real -> List.of(String.valueOf(real.value)); + case RealRangeFilterValueRequest realRange -> List.of(String.valueOf(realRange.value)); + }; + } + private static String resolveUserName(Instance identityInstance) { if (identityInstance.isResolvable()) { SecurityIdentity securityIdentity = identityInstance.get(); @@ -286,18 +311,145 @@ public EntityHistoryRequest(String idKind, String entityId, QuerySubmissionPaylo } } - public static final class FilterValuesRequest { + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) + @JsonSubTypes({ + @JsonSubTypes.Type(value = MultiSelectFilterValueRequest.class, name = "MULTI_SELECT"), + @JsonSubTypes.Type(value = BigMultiSelectFilterValueRequest.class, name = "BIG_MULTI_SELECT"), + @JsonSubTypes.Type(value = SelectFilterValueRequest.class, name = "SELECT"), + @JsonSubTypes.Type(value = StringFilterValueRequest.class, name = "STRING"), + @JsonSubTypes.Type(value = IntegerFilterValueRequest.class, name = "INTEGER"), + @JsonSubTypes.Type(value = IntegerRangeFilterValueRequest.class, name = "INTEGER_RANGE"), + @JsonSubTypes.Type(value = MoneyRangeFilterValueRequest.class, name = "MONEY_RANGE"), + @JsonSubTypes.Type(value = RealFilterValueRequest.class, name = "REAL"), + @JsonSubTypes.Type(value = RealRangeFilterValueRequest.class, name = "REAL_RANGE") + }) + @org.eclipse.microprofile.openapi.annotations.media.Schema( + description = "Filter value payload discriminated by `type`.", + discriminatorProperty = "type", + oneOf = { + MultiSelectFilterValueRequest.class, + BigMultiSelectFilterValueRequest.class, + SelectFilterValueRequest.class, + StringFilterValueRequest.class, + IntegerFilterValueRequest.class, + IntegerRangeFilterValueRequest.class, + MoneyRangeFilterValueRequest.class, + RealFilterValueRequest.class, + RealRangeFilterValueRequest.class + }, + discriminatorMapping = { + @DiscriminatorMapping(value = "MULTI_SELECT", schema = MultiSelectFilterValueRequest.class), + @DiscriminatorMapping(value = "BIG_MULTI_SELECT", schema = BigMultiSelectFilterValueRequest.class), + @DiscriminatorMapping(value = "SELECT", schema = SelectFilterValueRequest.class), + @DiscriminatorMapping(value = "STRING", schema = StringFilterValueRequest.class), + @DiscriminatorMapping(value = "INTEGER", schema = IntegerFilterValueRequest.class), + @DiscriminatorMapping(value = "INTEGER_RANGE", schema = IntegerRangeFilterValueRequest.class), + @DiscriminatorMapping(value = "MONEY_RANGE", schema = MoneyRangeFilterValueRequest.class), + @DiscriminatorMapping(value = "REAL", schema = RealFilterValueRequest.class), + @DiscriminatorMapping(value = "REAL_RANGE", schema = RealRangeFilterValueRequest.class) + } + ) + public abstract static class FilterValueRequest { public final @NotBlank String filter; public final @NotBlank String type; - public final @NotNull @NotEmpty List<@NotBlank String> value; - public FilterValuesRequest(String filter, String type, List value) { + protected FilterValueRequest(String filter, String type) { this.filter = filter; this.type = type; + } + } + + public static final class MultiSelectFilterValueRequest extends FilterValueRequest { + public final @NotNull @NotEmpty List<@NotBlank String> value; + + public MultiSelectFilterValueRequest(String filter, List value) { + super(filter, "MULTI_SELECT"); + this.value = value; + } + } + + public static final class BigMultiSelectFilterValueRequest extends FilterValueRequest { + public final @NotNull @NotEmpty List<@NotBlank String> value; + + public BigMultiSelectFilterValueRequest(String filter, List value) { + super(filter, "BIG_MULTI_SELECT"); + this.value = value; + } + } + + public static final class SelectFilterValueRequest extends FilterValueRequest { + public final @NotBlank String value; + + public SelectFilterValueRequest(String filter, String value) { + super(filter, "SELECT"); + this.value = value; + } + } + + public static final class StringFilterValueRequest extends FilterValueRequest { + public final @NotBlank String value; + + public StringFilterValueRequest(String filter, String value) { + super(filter, "STRING"); this.value = value; } } + public static final class IntegerFilterValueRequest extends FilterValueRequest { + public final @NotNull Long value; + + public IntegerFilterValueRequest(String filter, Long value) { + super(filter, "INTEGER"); + this.value = value; + } + } + + public static final class IntegerRangeFilterValueRequest extends FilterValueRequest { + public final @NotNull NumericRangeValue value; + + public IntegerRangeFilterValueRequest(String filter, NumericRangeValue value) { + super(filter, "INTEGER_RANGE"); + this.value = value; + } + } + + public static final class MoneyRangeFilterValueRequest extends FilterValueRequest { + public final @NotNull NumericRangeValue value; + + public MoneyRangeFilterValueRequest(String filter, NumericRangeValue value) { + super(filter, "MONEY_RANGE"); + this.value = value; + } + } + + public static final class RealFilterValueRequest extends FilterValueRequest { + public final @NotNull Double value; + + public RealFilterValueRequest(String filter, Double value) { + super(filter, "REAL"); + this.value = value; + } + } + + public static final class RealRangeFilterValueRequest extends FilterValueRequest { + public final @NotNull DecimalRangeValue value; + + public RealRangeFilterValueRequest(String filter, DecimalRangeValue value) { + super(filter, "REAL_RANGE"); + this.value = value; + } + } + + public static final class NumericRangeValue { + public Long min; + public Long max; + } + + public static final class DecimalRangeValue { + public Double min; + public Double max; + } + public record DatasetResponse( String id, String label From 955728ef5ffa33eb56b7a5cc9dc205714ad350dc Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:00:00 +0200 Subject: [PATCH 24/45] adds form-config endpoints --- .../api/DatasetFormConfigResource.java | 98 +++++++++++ .../quarkus/api/DatasetsResource.java | 17 +- .../quarkus/api/FormConfigResource.java | 93 ++++++++++ .../quarkus/api/FormConfigService.java | 162 ++++++++++++++++++ .../conquery/quarkus/api/MeResource.java | 16 +- .../quarkus/api/SecurityIdentityUtil.java | 24 +++ 6 files changed, 380 insertions(+), 30 deletions(-) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetFormConfigResource.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigResource.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/SecurityIdentityUtil.java 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 new file mode 100644 index 0000000000..820139b59d --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/DatasetFormConfigResource.java @@ -0,0 +1,98 @@ +package com.bakdata.conquery.quarkus.api; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +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; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Path("/api/datasets") +@Produces(MediaType.APPLICATION_JSON) +public class DatasetFormConfigResource { + + @Inject + DatasetService datasetService; + + @Inject + FormConfigService formConfigService; + + @Inject + Instance identity; + + @POST + @Path("/{datasetId}/form-configs") + @Operation( + summary = "Create a form configuration", + description = "Creates a form configuration for the dataset and returns its id." + ) + public PostFormConfigResponse postConfig( + @PathParam("datasetId") String datasetId, + @Valid @NotNull FormConfigCreatePayload payload + ) { + datasetService.requireDataset(datasetId); + return formConfigService.create(datasetId, payload, SecurityIdentityUtil.resolveUserName(identity)); + } + + @GET + @Path("/{datasetId}/form-configs") + @Operation( + summary = "List form configurations", + description = "Returns form configuration metadata for the dataset." + ) + public List getConfigs( + @PathParam("datasetId") String datasetId, + @QueryParam("formType") Set formType + ) { + datasetService.requireDataset(datasetId); + return formConfigService.list(datasetId, formType, SecurityIdentityUtil.resolveUserName(identity)); + } + + public static final class FormConfigCreatePayload { + @NotBlank + public String formType; + + public String label; + + @NotNull + public List<@NotBlank String> tags = List.of(); + + @NotNull + @Schema( + description = "Opaque form values payload saved by the frontend.", + implementation = Object.class + ) + public Map values; + } + + public record PostFormConfigResponse( + String id + ) { + } + + public record FormConfigOverviewResponse( + String id, + String formType, + String label, + List tags, + String ownerName, + String createdAt, + boolean own, + boolean shared, + boolean system + ) { + } +} 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 42c7a77db9..7c89d6a762 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 @@ -147,7 +147,7 @@ public List getQueries(@PathParam("datasetId") String data ) public StartQueryResponse postQueries(@PathParam("datasetId") String datasetId, QuerySubmissionPayload payload) { datasetService.requireDataset(datasetId); - return queryStateService.createQuery(datasetId, payload, resolveUserName(identity)); + return queryStateService.createQuery(datasetId, payload, SecurityIdentityUtil.resolveUserName(identity)); } @POST @@ -225,23 +225,10 @@ private List extractFilterValues(FilterValueRequest request) { case MoneyRangeFilterValueRequest moneyRange -> List.of(String.valueOf(moneyRange.value)); case RealFilterValueRequest real -> List.of(String.valueOf(real.value)); case RealRangeFilterValueRequest realRange -> List.of(String.valueOf(realRange.value)); + default -> throw new IllegalArgumentException("Unsupported filter value type: " + request.getClass().getName()); }; } - 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 Object loadFormResource(String path) { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try (InputStream input = classLoader.getResourceAsStream(path)) { 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 new file mode 100644 index 0000000000..3a7cddbed7 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigResource.java @@ -0,0 +1,93 @@ +package com.bakdata.conquery.quarkus.api; + +import java.util.List; + +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; +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; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Path("/api/form-configs") +@Produces(MediaType.APPLICATION_JSON) +public class FormConfigResource { + + @Inject + FormConfigService formConfigService; + + @Inject + Instance identity; + + @GET + @Path("/{formConfigId}") + @Operation( + summary = "Get a form configuration", + description = "Returns full form configuration metadata and values." + ) + public FormConfigFullResponse getConfig(@PathParam("formConfigId") String formConfigId) { + return formConfigService.get(formConfigId, SecurityIdentityUtil.resolveUserName(identity)); + } + + @PATCH + @Path("/{formConfigId}") + @Operation( + summary = "Patch a form configuration", + description = "Patches mutable form configuration fields." + ) + public FormConfigFullResponse patchConfig( + @PathParam("formConfigId") String formConfigId, + @Valid FormConfigPatchPayload payload + ) { + return formConfigService.patch(formConfigId, payload, SecurityIdentityUtil.resolveUserName(identity)); + } + + @DELETE + @Path("/{formConfigId}") + @Operation( + summary = "Delete a form configuration", + description = "Deletes a stored form configuration." + ) + public void deleteConfig(@PathParam("formConfigId") String formConfigId) { + formConfigService.delete(formConfigId); + } + + public static final class FormConfigPatchPayload { + public String label; + public List<@NotBlank String> tags; + public List<@NotBlank String> groups; + + @Schema( + description = "Opaque form values payload saved by the frontend.", + implementation = Object.class + ) + public Object values; + } + + public record FormConfigFullResponse( + String id, + String formType, + String label, + List tags, + String ownerName, + String createdAt, + boolean own, + boolean shared, + boolean system, + List groups, + @Schema( + description = "Opaque form values payload saved by the frontend.", + implementation = Object.class + ) + Object values + ) { + } +} 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 new file mode 100644 index 0000000000..ec9e3c04a7 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/FormConfigService.java @@ -0,0 +1,162 @@ +package com.bakdata.conquery.quarkus.api; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.NotFoundException; + +@ApplicationScoped +public class FormConfigService { + + private final Map configsById = new ConcurrentHashMap<>(); + + public DatasetFormConfigResource.PostFormConfigResponse create( + String datasetId, + DatasetFormConfigResource.FormConfigCreatePayload payload, + String ownerName + ) { + String id = datasetId + ":" + payload.formType + ":" + UUID.randomUUID(); + StoredFormConfig stored = new StoredFormConfig( + id, + datasetId, + payload.formType, + payload.label, + List.copyOf(payload.tags), + ownerName, + Instant.now(), + false, + List.of(), + payload.values + ); + configsById.put(id, stored); + return new DatasetFormConfigResource.PostFormConfigResponse(id); + } + + public List list( + String datasetId, + Set requestedFormTypes, + String requester + ) { + return configsById.values().stream() + .filter(config -> config.datasetId.equals(datasetId)) + .filter(config -> requestedFormTypes == null + || requestedFormTypes.isEmpty() + || requestedFormTypes.contains(config.formType)) + .sorted((left, right) -> right.createdAt.compareTo(left.createdAt)) + .map(config -> toOverview(config, requester)) + .toList(); + } + + public FormConfigResource.FormConfigFullResponse get(String formConfigId, String requester) { + return toFull(require(formConfigId), requester); + } + + public FormConfigResource.FormConfigFullResponse patch( + String formConfigId, + FormConfigResource.FormConfigPatchPayload payload, + String requester + ) { + StoredFormConfig config = require(formConfigId); + if (payload.label != null) { + config.label = payload.label; + } + if (payload.tags != null) { + config.tags = List.copyOf(payload.tags); + } + if (payload.groups != null) { + config.groups = List.copyOf(payload.groups); + } + if (payload.values != null) { + config.values = payload.values; + } + return toFull(config, requester); + } + + public void delete(String formConfigId) { + StoredFormConfig removed = configsById.remove(formConfigId); + if (removed == null) { + throw new NotFoundException("Unknown form-config: " + 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 + ); + } + + 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 + ); + } + + private StoredFormConfig require(String formConfigId) { + StoredFormConfig config = configsById.get(formConfigId); + if (config == null) { + throw new NotFoundException("Unknown form-config: " + formConfigId); + } + return config; + } + + private static final class StoredFormConfig { + private final String id; + private final String datasetId; + private final String formType; + private String label; + private List tags; + private final String ownerName; + private final Instant createdAt; + private final boolean system; + private List groups; + private Object values; + + private StoredFormConfig( + String id, + String datasetId, + String formType, + String label, + List tags, + String ownerName, + Instant createdAt, + boolean system, + List groups, + Object 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; + } + } +} 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 7a007e80df..a313571534 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 @@ -19,7 +19,7 @@ public class MeResource { @GET public MeResponse me() { - String userName = resolveUserName(identity); + String userName = SecurityIdentityUtil.resolveUserName(identity); return new MeResponse( userName, @@ -29,20 +29,6 @@ public MeResponse me() { ); } - 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, diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/SecurityIdentityUtil.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/SecurityIdentityUtil.java new file mode 100644 index 0000000000..ebe35c9429 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/api/SecurityIdentityUtil.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.api; + +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.inject.Instance; + +public final class SecurityIdentityUtil { + + private SecurityIdentityUtil() { + } + + public 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"; + } +} From 1c2f8dc9ca93a981dc82af9ff93b81dc3eafd00a Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:02:35 +0200 Subject: [PATCH 25/45] adds base for storage abstraction --- .../conquery/quarkus/api/DatasetService.java | 38 ++--- .../quarkus/api/FormConfigResource.java | 5 +- .../quarkus/api/FormConfigService.java | 113 +++++---------- .../quarkus/api/QueryStateService.java | 134 ++---------------- .../ConfigDatasetCatalogRepository.java | 43 ++++++ .../storage/DatasetCatalogRepository.java | 28 ++++ .../quarkus/storage/FormConfigRepository.java | 17 +++ .../storage/InMemoryFormConfigRepository.java | 35 +++++ .../storage/InMemoryQueryRepository.java | 35 +++++ .../quarkus/storage/QueryRepository.java | 17 +++ .../storage/model/StoredFormConfig.java | 99 +++++++++++++ .../quarkus/storage/model/StoredQuery.java | 117 +++++++++++++++ 12 files changed, 452 insertions(+), 229 deletions(-) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/FormConfigRepository.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/QueryRepository.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredFormConfig.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredQuery.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 27e4138a20..5cc1d8d184 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,7 @@ 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 com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.NotFoundException; @@ -16,43 +15,32 @@ public class DatasetService { @Inject - DatasetsRuntimeConfig datasetsConfig; + DatasetCatalogRepository datasetCatalog; - @Inject - ConceptsRuntimeConfig conceptsConfig; - - public List listDatasets() { - return datasetsConfig.datasets(); + public List listDatasets() { + return datasetCatalog.listDatasets(); } - public DatasetsRuntimeConfig.DatasetEntry requireDataset(String datasetId) { - return datasetsConfig.datasets() - .stream() - .filter(dataset -> dataset.id().equals(datasetId)) - .findFirst() + public DatasetCatalogRepository.DatasetRecord requireDataset(String datasetId) { + return datasetCatalog.findDataset(datasetId) .orElseThrow(() -> new NotFoundException("Unknown dataset: " + datasetId)); } - public ConceptsRuntimeConfig.ConceptEntry requireConcept(String conceptId) { - return conceptsConfig.concepts() - .stream() - .filter(concept -> concept.id().equals(conceptId)) - .findFirst() + public DatasetCatalogRepository.ConceptRecord requireConcept(String conceptId) { + return datasetCatalog.findConcept(conceptId) .orElseThrow(() -> new NotFoundException("Unknown concept: " + conceptId)); } - public List listConceptsForDataset(String datasetId) { - return conceptsConfig.concepts() - .stream() - .filter(concept -> concept.dataset().equals(datasetId)) + public List listConceptsForDataset(String datasetId) { + return datasetCatalog.listConcepts().stream().filter(concept -> concept.datasetId().equals(datasetId)) .toList(); } public ConceptCodeResolution resolveConceptCodes(String rootConceptId, List codes) { - ConceptsRuntimeConfig.ConceptEntry rootConcept = requireConcept(rootConceptId); + DatasetCatalogRepository.ConceptRecord rootConcept = requireConcept(rootConceptId); - Map lookupByCode = conceptsConfig.concepts().stream() - .filter(concept -> concept.dataset().equals(rootConcept.dataset())) + Map lookupByCode = datasetCatalog.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()) 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 3a7cddbed7..67736302e2 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 @@ -1,6 +1,7 @@ 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; @@ -69,7 +70,7 @@ public static final class FormConfigPatchPayload { description = "Opaque form values payload saved by the frontend.", implementation = Object.class ) - public Object values; + public Map values; } public record FormConfigFullResponse( @@ -87,7 +88,7 @@ public record FormConfigFullResponse( description = "Opaque form values payload saved by the frontend.", implementation = Object.class ) - Object values + Map values ) { } } 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 ec9e3c04a7..324e1e66f7 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 @@ -1,20 +1,21 @@ package com.bakdata.conquery.quarkus.api; import java.time.Instant; -import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; +import com.bakdata.conquery.quarkus.storage.FormConfigRepository; +import com.bakdata.conquery.quarkus.storage.model.StoredFormConfig; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import jakarta.ws.rs.NotFoundException; @ApplicationScoped public class FormConfigService { - private final Map configsById = new ConcurrentHashMap<>(); + @Inject + FormConfigRepository formConfigRepository; public DatasetFormConfigResource.PostFormConfigResponse create( String datasetId, @@ -34,7 +35,7 @@ public DatasetFormConfigResource.PostFormConfigResponse create( List.of(), payload.values ); - configsById.put(id, stored); + formConfigRepository.save(stored); return new DatasetFormConfigResource.PostFormConfigResponse(id); } @@ -43,12 +44,11 @@ public List list( Set requestedFormTypes, String requester ) { - return configsById.values().stream() - .filter(config -> config.datasetId.equals(datasetId)) + 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.formType())) + .sorted((left, right) -> right.createdAt().compareTo(left.createdAt())) .map(config -> toOverview(config, requester)) .toList(); } @@ -64,99 +64,58 @@ public FormConfigResource.FormConfigFullResponse patch( ) { StoredFormConfig config = require(formConfigId); if (payload.label != null) { - config.label = payload.label; + config.setLabel(payload.label); } if (payload.tags != null) { - config.tags = List.copyOf(payload.tags); + config.setTags(List.copyOf(payload.tags)); } if (payload.groups != null) { - config.groups = List.copyOf(payload.groups); + config.setGroups(List.copyOf(payload.groups)); } if (payload.values != null) { - config.values = payload.values; + config.setValues(payload.values); } + formConfigRepository.save(config); return toFull(config, requester); } public void delete(String formConfigId) { - StoredFormConfig removed = configsById.remove(formConfigId); - if (removed == null) { + if (!formConfigRepository.deleteById(formConfigId)) { throw new NotFoundException("Unknown form-config: " + 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.id(), + config.formType(), + config.label(), + config.tags(), + config.ownerName(), + config.createdAt().toString(), + config.ownerName().equals(requester), + !config.groups().isEmpty(), + config.system() ); } 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.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() ); } private StoredFormConfig require(String formConfigId) { - StoredFormConfig config = configsById.get(formConfigId); - if (config == null) { - throw new NotFoundException("Unknown form-config: " + formConfigId); - } - return config; - } - - private static final class StoredFormConfig { - private final String id; - private final String datasetId; - private final String formType; - private String label; - private List tags; - private final String ownerName; - private final Instant createdAt; - private final boolean system; - private List groups; - private Object values; - - private StoredFormConfig( - String id, - String datasetId, - String formType, - String label, - List tags, - String ownerName, - Instant createdAt, - boolean system, - List groups, - Object 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; - } + return formConfigRepository.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 e5c429d7ea..802363f8c8 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 @@ -5,17 +5,19 @@ 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 com.bakdata.conquery.quarkus.storage.QueryRepository; +import com.bakdata.conquery.quarkus.storage.model.StoredQuery; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import jakarta.ws.rs.NotFoundException; @ApplicationScoped public class QueryStateService { - private final Map queriesById = new ConcurrentHashMap<>(); + @Inject + QueryRepository queryRepository; public DatasetsResource.StartQueryResponse createQuery(String datasetId, QuerySubmissionPayload payload, String ownerName) { String queryId = UUID.randomUUID().toString(); @@ -38,14 +40,12 @@ public DatasetsResource.StartQueryResponse createQuery(String datasetId, QuerySu List.of() ); - queriesById.put(queryId, stored); + queryRepository.save(stored); return new DatasetsResource.StartQueryResponse(queryId); } public List getDatasetQueries(String datasetId) { - return queriesById.values() - .stream() - .filter(query -> query.datasetId().equals(datasetId)) + return queryRepository.listByDataset(datasetId).stream() .sorted(Comparator.comparing(StoredQuery::createdAt).reversed()) .map(this::toSummaryResponse) .toList(); @@ -80,8 +80,7 @@ public void patchQuery(String queryId, QueryPatch patch) { } public void deleteQuery(String queryId) { - StoredQuery removed = queriesById.remove(queryId); - if (removed == null) { + if (!queryRepository.deleteById(queryId)) { throw new NotFoundException("Unknown query: " + queryId); } } @@ -275,128 +274,13 @@ private boolean anyContainsDates(List children } private StoredQuery requireQuery(String queryId) { - StoredQuery query = queriesById.get(queryId); - if (query == null) { - throw new NotFoundException("Unknown query: " + queryId); - } - return query; + return queryRepository.findById(queryId).orElseThrow(() -> new NotFoundException("Unknown query: " + queryId)); } private static String labelFor(String queryId) { return "Query " + queryId; } - private static final class StoredQuery { - private final String id; - private final String datasetId; - private volatile 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; - - private 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; - } - } - public record QueryPatch( String label, List tags, 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 new file mode 100644 index 0000000000..d6dcb66432 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java @@ -0,0 +1,43 @@ +package com.bakdata.conquery.quarkus.storage; + +import java.util.List; +import java.util.Optional; + +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; + +@ApplicationScoped +public class ConfigDatasetCatalogRepository implements DatasetCatalogRepository { + + @Inject + DatasetsRuntimeConfig datasetsConfig; + + @Inject + ConceptsRuntimeConfig conceptsConfig; + + @Override + public List listDatasets() { + return datasetsConfig.datasets().stream() + .map(dataset -> new DatasetRecord(dataset.id(), dataset.label())) + .toList(); + } + + @Override + public Optional findDataset(String datasetId) { + return listDatasets().stream().filter(dataset -> dataset.id().equals(datasetId)).findFirst(); + } + + @Override + public List listConcepts() { + return conceptsConfig.concepts().stream() + .map(concept -> new ConceptRecord(concept.id(), concept.label(), concept.dataset())) + .toList(); + } + + @Override + public Optional findConcept(String conceptId) { + return listConcepts().stream().filter(concept -> concept.id().equals(conceptId)).findFirst(); + } +} 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 new file mode 100644 index 0000000000..c55610453d --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/DatasetCatalogRepository.java @@ -0,0 +1,28 @@ +package com.bakdata.conquery.quarkus.storage; + +import java.util.List; +import java.util.Optional; + +public interface DatasetCatalogRepository { + + List listDatasets(); + + Optional findDataset(String datasetId); + + List listConcepts(); + + Optional findConcept(String conceptId); + + record DatasetRecord( + String id, + String label + ) { + } + + record ConceptRecord( + String id, + String label, + String datasetId + ) { + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/FormConfigRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/FormConfigRepository.java new file mode 100644 index 0000000000..c6dec5a304 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/FormConfigRepository.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.storage; + +import java.util.List; +import java.util.Optional; + +import com.bakdata.conquery.quarkus.storage.model.StoredFormConfig; + +public interface FormConfigRepository { + + void save(StoredFormConfig config); + + Optional findById(String formConfigId); + + List listByDataset(String datasetId); + + boolean deleteById(String formConfigId); +} 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 new file mode 100644 index 0000000000..34ab3c5e2b --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryFormConfigRepository.java @@ -0,0 +1,35 @@ +package com.bakdata.conquery.quarkus.storage; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import com.bakdata.conquery.quarkus.storage.model.StoredFormConfig; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class InMemoryFormConfigRepository implements FormConfigRepository { + + private final Map configsById = new ConcurrentHashMap<>(); + + @Override + public void save(StoredFormConfig config) { + configsById.put(config.id(), config); + } + + @Override + public Optional findById(String formConfigId) { + return Optional.ofNullable(configsById.get(formConfigId)); + } + + @Override + public List listByDataset(String datasetId) { + return configsById.values().stream().filter(config -> config.datasetId().equals(datasetId)).toList(); + } + + @Override + public boolean deleteById(String formConfigId) { + return configsById.remove(formConfigId) != null; + } +} 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 new file mode 100644 index 0000000000..9531e3d3ed --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/InMemoryQueryRepository.java @@ -0,0 +1,35 @@ +package com.bakdata.conquery.quarkus.storage; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import com.bakdata.conquery.quarkus.storage.model.StoredQuery; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class InMemoryQueryRepository implements QueryRepository { + + private final Map queriesById = new ConcurrentHashMap<>(); + + @Override + public void save(StoredQuery query) { + queriesById.put(query.id(), query); + } + + @Override + public Optional findById(String queryId) { + return Optional.ofNullable(queriesById.get(queryId)); + } + + @Override + public List listByDataset(String datasetId) { + return queriesById.values().stream().filter(query -> query.datasetId().equals(datasetId)).toList(); + } + + @Override + public boolean deleteById(String queryId) { + return queriesById.remove(queryId) != null; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/QueryRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/QueryRepository.java new file mode 100644 index 0000000000..e179ffa2b7 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/QueryRepository.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.storage; + +import java.util.List; +import java.util.Optional; + +import com.bakdata.conquery.quarkus.storage.model.StoredQuery; + +public interface QueryRepository { + + void save(StoredQuery query); + + Optional findById(String queryId); + + List listByDataset(String datasetId); + + boolean deleteById(String queryId); +} 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 new file mode 100644 index 0000000000..45fe06cf3b --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredFormConfig.java @@ -0,0 +1,99 @@ +package com.bakdata.conquery.quarkus.storage.model; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class StoredFormConfig { + private final String id; + private final String datasetId; + private final String formType; + private String label; + private List tags; + private final String ownerName; + private final Instant createdAt; + 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 new file mode 100644 index 0000000000..6f5ceb2da7 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/model/StoredQuery.java @@ -0,0 +1,117 @@ +package com.bakdata.conquery.quarkus.storage.model; + +import java.time.Instant; +import java.util.List; + +import com.bakdata.conquery.quarkus.api.QueryResource; + +public class StoredQuery { + private final String id; + private final String datasetId; + private volatile 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; + } +} From e02fef96812ce964186e1d26a41aa444f99a6b6c Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:26:42 +0200 Subject: [PATCH 26/45] adds lombok --- backend-quarkus/pom.xml | 6 + .../quarkus/api/FormConfigService.java | 44 +++---- .../quarkus/api/QueryStateService.java | 120 +++++++++--------- .../storage/InMemoryFormConfigRepository.java | 4 +- .../storage/InMemoryQueryRepository.java | 4 +- .../storage/model/StoredFormConfig.java | 88 +------------ .../quarkus/storage/model/StoredQuery.java | 112 ++-------------- 7 files changed, 110 insertions(+), 268 deletions(-) diff --git a/backend-quarkus/pom.xml b/backend-quarkus/pom.xml index d543150962..00b22531bd 100644 --- a/backend-quarkus/pom.xml +++ b/backend-quarkus/pom.xml @@ -65,6 +65,12 @@ io.quarkus quarkus-hibernate-validator + + 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 selects, @@ -76,7 +79,18 @@ record Connector( // Use internal rep directly as we won't need data mangling List validityDates, boolean isDefault - ){} + ){ + public String datasetId() { + return dataset; + } + + public String tableId() { + return dataset + "." + table; + } + public String id() { + return dataset + "." + concept + "." + name; + } + } record ValidityDate( String column, @@ -85,7 +99,12 @@ record ValidityDate( ) {} interface Select{} - interface Filter{} + interface Filter{ + String id(); + String label(); + String type(); + List options(); + } record ConceptCondition( String type, 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 index 59b33b1a96..8b5ce9bfbe 100644 --- 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 @@ -12,8 +12,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.validation.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; @ApplicationScoped +@Slf4j public class DatasetMetadataFolderLoader { @Inject @@ -22,15 +27,16 @@ public class DatasetMetadataFolderLoader { @Inject ObjectMapper objectMapper; + @Inject + Validator validator; + 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.")); + 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."); } @@ -63,11 +69,8 @@ private LoadedDatasetMetadata loadDatasetFolder(Path folderPath) { Map conceptsById = loadConcepts(folderPath, datasetId); Map tablesById = loadTables(folderPath, datasetId); - return new LoadedDatasetMetadata( - dataset, - Map.copyOf(conceptsById), - Map.copyOf(tablesById) - ); + log.debug("Loaded dataset {} with {} concepts and {} tables", datasetId, conceptsById.size(), tablesById.size()); + return new LoadedDatasetMetadata(dataset, Map.copyOf(conceptsById), Map.copyOf(tablesById)); } private Map loadConcepts(Path folderPath, String datasetId) { @@ -78,76 +81,83 @@ private Map loadConcepts(Path folderPa 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 = conceptName.equals(datasetId) ? conceptName : ensureDatasetScopedId(datasetId, conceptName); - conceptsById.put(conceptId,collectConcept(payload, conceptId)); - }); - } - catch (IOException e) { + 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)); + + Set> constraintViolations = validator.validate(payload); + if (!constraintViolations.isEmpty()) { + throw new ConstraintViolationException("Failed to validate concept payload: %s".formatted(path), constraintViolations); + } + String conceptId = conceptName.equals(datasetId) ? conceptName : ensureDatasetScopedId(datasetId, conceptName); + conceptsById.put(conceptId, collectConcept(payload, conceptId)); + }); + } catch (IOException e) { throw new IllegalStateException("Failed to list concept metadata in " + conceptsDir, e); } + log.debug("Loaded dataset {} with {} concepts", datasetId, conceptsById.size()); return conceptsById; } - private DatasetCatalogRepository.Concept collectConcept( - ConceptPayload payload, - String thisId - ) { + private DatasetCatalogRepository.Concept collectConcept(ConceptPayload payload, String conceptId) { String conceptLabel = firstNonBlank(payload.label()).orElseGet(() -> payload.name); List children = Optional.ofNullable(payload.children()).orElse(List.of()); Map conceptElementsById = new LinkedHashMap<>(); - List directChildIds = children.stream() - .map(child -> childId(thisId, child)) - .toList(); + List directChildIds = children.stream().map(child -> childId(conceptId, child)).toList(); + + children.forEach(child -> collectConceptChildren(child, childId(conceptId, child), conceptId, conceptElementsById)); + + List connectors = payload.connectors().stream().map(p -> { + + String datasetName = ScopedId.extractDatasetId(conceptId).orElseThrow(); + String tableName = p.table; + if (tableName != null && tableName.startsWith(datasetName + ".")) { + // Shorten tableId to name + tableName = tableName.substring(datasetName.length() + 1); + } + String columnName = p.column; + if (columnName != null) { + if (tableName != null && columnName.startsWith(datasetName + "." + tableName + ".")) { + // Shorten columnId to name + columnName = columnName.substring(datasetName.length() + 1 + tableName.length() + 1); + } else { + String[] split = columnName.split("\\."); + if (split.length != 2) { + throw new IllegalArgumentException("Invalid column name: " + columnName); + } + + tableName = split[0]; + columnName = split[1]; + } + } - children.forEach(child -> collectConceptChildren(child, childId(thisId, child), thisId, conceptElementsById)); + return new DatasetCatalogRepository.Connector( + datasetName, tableName, columnName, conceptId, p.label, p.name, List.of(), // Optional.of(p.selects().stream().map(SelectPayload::new).toList()).orElse(List.of()), + Optional.ofNullable(p.filters()).orElse(List.of()).stream().map(DatasetCatalogRepository.Filter.class::cast).toList(), Optional.ofNullable(p.validityDates()).orElse(List.of()), p.isDefault + ); - List connectors = Optional.ofNullable(payload.connectors()).orElse(List.of()).stream().map(p -> new DatasetCatalogRepository.Connector( - p.column, - p.label, - p.name, - Optional.ofNullable(p.selects()).orElse(List.of()), - Optional.ofNullable(p.filters()).orElse(List.of()), - Optional.ofNullable(p.validityDates()).orElse(List.of()), - p.isDefault - )).toList(); + }).toList(); + log.debug("Loaded concept {} with {} children", conceptId, children.size()); - return new DatasetCatalogRepository.Concept(thisId, conceptLabel, payload.description, Map.copyOf(conceptElementsById), directChildIds, connectors); + return new DatasetCatalogRepository.Concept(conceptId, conceptLabel, payload.description, Map.copyOf(conceptElementsById), directChildIds, connectors); } - private void collectConceptChildren( - ConceptElementPayload payload, - String conceptId, - String parentId, - Map conceptElementsById - ) { + private void collectConceptChildren(ConceptElementPayload payload, String conceptId, String parentId, Map conceptElementsById) { String conceptLabel = firstNonBlank(payload.label()).orElse(payload.name); List children = Optional.ofNullable(payload.children()).orElse(List.of()); - List childIds = children.stream() - .map(child -> childId(conceptId, child)) - .toList(); + List childIds = children.stream().map(child -> childId(conceptId, child)).toList(); DatasetCatalogRepository.ConceptCondition condition = toConceptCondition(payload.condition()); - conceptElementsById.put( - conceptId, - new DatasetCatalogRepository.ConceptElement(conceptId, conceptLabel, payload.description, parentId, childIds, condition) - ); + conceptElementsById.put(conceptId, new DatasetCatalogRepository.ConceptElement(conceptId, conceptLabel, payload.description, parentId, childIds, condition)); children.forEach(child -> collectConceptChildren(child, childId(conceptId, child), conceptId, conceptElementsById)); } private String childId(String parentId, ConceptElementPayload child) { - String childName = firstNonBlank(child.name(), child.label()) - .orElseThrow(() -> new IllegalStateException("Concept child of " + parentId + " has no name.")); + String childName = firstNonBlank(child.name(), child.label()).orElseThrow(() -> new IllegalStateException("Concept child of " + parentId + " has no name.")); return parentId + "." + childName; } @@ -155,16 +165,8 @@ private DatasetCatalogRepository.ConceptCondition toConceptCondition(ConditionPa if (condition == null) { return null; } - String type = firstNonBlank(condition.type()) - .orElseThrow(() -> new IllegalStateException("Concept condition is missing required type.")); - return new DatasetCatalogRepository.ConceptCondition( - type, - condition.values(), - condition.column(), - Optional.ofNullable(condition.conditions()).orElse(List.of()).stream() - .map(this::toConceptCondition) - .toList() - ); + String type = firstNonBlank(condition.type()).orElseThrow(() -> new IllegalStateException("Concept condition is missing required type.")); + return new DatasetCatalogRepository.ConceptCondition(type, condition.values(), condition.column(), Optional.ofNullable(condition.conditions()).orElse(List.of()).stream().map(this::toConceptCondition).toList()); } private Map loadTables(Path folderPath, String datasetId) { @@ -175,39 +177,25 @@ private Map loadTables(Path folder 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) { + 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)); + 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); @@ -256,26 +244,22 @@ private Optional loadDatasetPayload(Path folderPath) { private T read(Path path, Class type) { try { return objectMapper.readValue(path.toFile(), type); - } - catch (IOException e) { + } 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.")); + 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.")); + 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) { + } catch (Exception e) { throw new IllegalStateException("Unsupported table column type: " + rawType, e); } } @@ -306,85 +290,67 @@ private String stripSuffix(String fileName, String suffix) { } public record LoadedDatasetMetadata( - DatasetCatalogRepository.DatasetRecord dataset, - Map conceptsById, + DatasetCatalogRepository.DatasetRecord dataset, Map conceptsById, Map tablesById ) { } @JsonIgnoreProperties(ignoreUnknown = true) private record DatasetPayload( - String id, - String label + String id, String label ) { } @JsonIgnoreProperties(ignoreUnknown = true) private record ConceptPayload( - String name, - String label, - String description, - List children, - List connectors + String name, String label, String description, List children, + @NotEmpty @NotNull List connectors ) { } @JsonIgnoreProperties(ignoreUnknown = true) private record ConceptElementPayload( - String name, - String label, - String description, - List children, + String name, String label, String description, List children, ConditionPayload condition ) { } private record ConnectorPayload( - String column, - String label, - String name, - List selects, - List filters, + String table, String column, String label, String name, List selects, + List filters, // Use internal rep directly as we won't need data mangling - List validityDates, - boolean isDefault - ) - { + List validityDates, boolean isDefault + ) { } @JsonIgnoreProperties(ignoreUnknown = true) // TODO implement polymorphism - private record SelectPayload(){} + private record SelectPayload() implements DatasetCatalogRepository.Select { + } @JsonIgnoreProperties(ignoreUnknown = true) // TODO implement polymorphism - private record FilterPayload(){} + private record FilterPayload( + String id, String name, String label, String type, List options, String unit, String tooltip, + boolean allowDropFile, Object defaultValue + ) implements DatasetCatalogRepository.Filter { + } @JsonIgnoreProperties(ignoreUnknown = true) private record ConditionPayload( - String type, - List values, - String column, - List conditions + String type, List values, String column, List conditions ) { } @JsonIgnoreProperties(ignoreUnknown = true) private record TablePayload( - String name, - String label, - String primaryColumn, - List columns + String name, String label, String primaryColumn, List columns ) { } @JsonIgnoreProperties(ignoreUnknown = true) private record ColumnPayload( - String id, - String name, - String label, - String type, - String secondaryId + String id, String name, String label, String type, String secondaryId ) { } 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 index 928fd68692..0a2337217a 100644 --- 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 @@ -17,4 +17,5 @@ public static Optional extractDatasetId(String scopedId) { } return Optional.of(scopedId.substring(0, separator)); } + } diff --git a/backend-quarkus/src/main/resources/application-test.properties b/backend-quarkus/src/main/resources/application-test.properties index 7e8765e991..c5240aa2bb 100644 --- a/backend-quarkus/src/main/resources/application-test.properties +++ b/backend-quarkus/src/main/resources/application-test.properties @@ -1,3 +1,6 @@ +quarkus.log.category."com.bakdata.conquery".level=DEBUG + +# Conquery related conquery.metadata.enabled=true conquery.metadata.root-path=target/test-classes/test-meta-data conquery.metadata.folders[0]=imdb 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 a66b7021d4..04d04d8d25 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 @@ -18,11 +18,7 @@ void conceptEndpointReturnsConceptMap() { .body("imdb.label", equalTo("IMDb")) .body("imdb.detailsAvailable", equalTo(true)) .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)); + .body("imdb.tables[0].id", equalTo("imdb.title")); } @Test 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 index e1ba047c0e..84949257f5 100644 --- 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 @@ -12,16 +12,20 @@ import com.bakdata.conquery.quarkus.config.DatasetMetadataRuntimeConfig; import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.validation.Validator; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +@QuarkusTest class DatasetMetadataFolderLoaderTest { - @TempDir - Path tempDir; + @Inject + Validator validator; @Test - void loadsConfiguredMetadataFolderAndScopesIds() throws Exception { + void loadsConfiguredMetadataFolderAndScopesIds(@TempDir Path tempDir) throws Exception { Path root = tempDir.resolve("gen"); Path demo = root.resolve("demo"); Files.createDirectories(demo.resolve("conceptTrees")); @@ -50,6 +54,13 @@ void loadsConfiguredMetadataFolderAndScopesIds() throws Exception { { "name":"icd", "label":"ICD", + "connectors": [ + { + "label": "kh-diagnose", + "name": "kh-diagnose", + "table": "kh_diagnose" + } + ], "children":[ { "name":"a00", @@ -83,6 +94,7 @@ void loadsConfiguredMetadataFolderAndScopesIds() throws Exception { DatasetMetadataFolderLoader loader = new DatasetMetadataFolderLoader(); loader.objectMapper = new ObjectMapper(); + loader.validator = validator; loader.metadataConfig = new DatasetMetadataRuntimeConfig() { @Override public boolean enabled() { @@ -142,6 +154,7 @@ void loadsDevConfigMetadataFixtures() throws Exception { DatasetMetadataFolderLoader loader = new DatasetMetadataFolderLoader(); loader.objectMapper = new ObjectMapper(); + loader.validator = validator; loader.metadataConfig = new DatasetMetadataRuntimeConfig() { @Override public boolean enabled() { @@ -192,7 +205,7 @@ public Optional> folders() { } @Test - void fallsBackToFolderNameWhenDatasetJsonIsMissing() throws Exception { + void fallsBackToFolderNameWhenDatasetJsonIsMissing(@TempDir Path tempDir) throws Exception { Path root = tempDir.resolve("test-datasets"); String datasetName = "fallback-dataset-name"; Path fallback = root.resolve(datasetName); diff --git a/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json b/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json index 5fdbcb82de..a0b0520c17 100644 --- a/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json +++ b/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json @@ -2,5 +2,11 @@ "name": "imdb", "label": "IMDb", "children": [], - "connectors": [] + "connectors": [ + { + "name": "titles", + "label": "Titles", + "table": "title" + } + ] } From e551cebd1a3d2eadbc14930f0a5602139b6e10d4 Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:01:02 +0200 Subject: [PATCH 33/45] introduce id system that looks like the old one --- .../conquery/quarkus/api/ConceptResource.java | 16 +- .../quarkus/api/DatasetsResource.java | 20 +- .../conquery/quarkus/ids/ColumnId.java | 37 ++++ .../conquery/quarkus/ids/ConceptId.java | 44 +++++ .../conquery/quarkus/ids/ConnectorId.java | 38 ++++ .../conquery/quarkus/ids/DatasetId.java | 26 +++ .../bakdata/conquery/quarkus/ids/IdPart.java | 41 ++++ .../conquery/quarkus/ids/IdPartSanitizer.java | 22 +++ .../bakdata/conquery/quarkus/ids/TableId.java | 33 ++++ .../quarkus/services/DatasetService.java | 43 +++-- .../ConfigDatasetCatalogRepository.java | 66 ++----- .../storage/DatasetCatalogRepository.java | 62 +++--- .../storage/DatasetMetadataFolderLoader.java | 181 +++++++++++++----- .../quarkus/storage/NamespaceStorage.java | 11 +- .../storage/NamespaceStorageRegistry.java | 4 +- .../RepositoryNamespaceStorageRegistry.java | 22 ++- .../xodus/XodusDatasetCatalogRepository.java | 66 +++---- .../XodusDatasetEnvironmentProvider.java | 17 +- .../conquery/quarkus/util/ScopedId.java | 21 -- .../resources/application-test.properties | 4 +- .../quarkus/services/DatasetServiceTest.java | 37 ++-- .../DatasetMetadataFolderLoaderTest.java | 55 +++--- .../conceptTrees/.gitkeep | 0 .../{empty-set => empty_set}/dataset.json | 2 +- .../structure_empty-set.json | 0 .../{empty-set => empty_set}/tables/.gitkeep | 0 26 files changed, 580 insertions(+), 288 deletions(-) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ColumnId.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConceptId.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConnectorId.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/DatasetId.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/IdPart.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/IdPartSanitizer.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/TableId.java delete mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/util/ScopedId.java rename backend-quarkus/src/test/resources/test-meta-data/{empty-set => empty_set}/conceptTrees/.gitkeep (100%) rename backend-quarkus/src/test/resources/test-meta-data/{empty-set => empty_set}/dataset.json (59%) rename backend-quarkus/src/test/resources/test-meta-data/{empty-set => empty_set}/structure_empty-set.json (100%) rename backend-quarkus/src/test/resources/test-meta-data/{empty-set => empty_set}/tables/.gitkeep (100%) 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 170d09398d..92894aac5e 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,9 +2,9 @@ import java.util.*; +import com.bakdata.conquery.quarkus.ids.ConceptId; 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; import com.fasterxml.jackson.annotation.JsonProperty; import io.smallrye.common.constraint.Nullable; @@ -37,27 +37,27 @@ public class ConceptResource { public Map getConcept(@PathParam("conceptId") @NotBlank String conceptId) { DatasetCatalogRepository.Concept concept = datasetService.requireConcept(conceptId); - String datasetId = ScopedId.extractDatasetId(conceptId).orElseThrow(() -> new IllegalStateException("Expected dataset-scoped concept id but got: " + conceptId)); + ConceptId parsedConceptId = ConceptId.parse(conceptId); Map nodes = new HashMap<>(); - Map children = concept.children(); + Map children = concept.children(); // Add main node - ConceptNodeResponse node = new ConceptNodeResponse(concept.label(), concept.description(), true, concept.childrenIds(), 0L, 0L, true, !children.isEmpty(), concept.connectors().stream().map(this::toConnectorResponse).toList(), List.of()); - nodes.put(conceptId, node); + ConceptNodeResponse node = new ConceptNodeResponse(concept.label(), concept.description(), true, concept.childrenIds().stream().map(ConceptId::toString).toList(), 0L, 0L, true, !children.isEmpty(), concept.connectors().stream().map(this::toConnectorResponse).toList(), List.of()); + nodes.put(parsedConceptId.toString(), 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); + ConceptNodeResponse childNode = new ConceptNodeResponse(child.label(), child.description(), true, child.children().stream().map(ConceptId::toString).toList(), 0L, 0L, true, !children.isEmpty(), List.of(), List.of()); + nodes.put(id.toString(), childNode); }); return nodes; } private ConnectorResponse toConnectorResponse(DatasetCatalogRepository.Connector connector) { - return new ConnectorResponse(connector.tableId(), connector.id(), connector.label(), connector.isDefault(), List.of(), List.of(), List.of()); + return new ConnectorResponse(connector.tableId().toString(), connector.id().toString(), connector.label(), connector.isDefault(), List.of(), List.of(), List.of()); } 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 afcd91b318..53c99a2ad9 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,12 +11,12 @@ import com.bakdata.conquery.quarkus.config.EntityPreviewRuntimeConfig; import com.bakdata.conquery.quarkus.config.FormQueriesRuntimeConfig; +import com.bakdata.conquery.quarkus.ids.ConceptId; 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; @@ -66,7 +66,7 @@ public class DatasetsResource { @GET public List getDatasets() { return datasetService.listDatasets().stream() - .map(entry -> new DatasetResponse(entry.id(), entry.label())) + .map(entry -> new DatasetResponse(entry.id().toString(), entry.label())) .toList(); } @@ -102,12 +102,12 @@ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) { java.util.Map concepts = new LinkedHashMap<>(); datasetService.listRootConceptsForDataset(datasetId).forEach(entry -> concepts.put( - entry.id(), + entry.id().toString(), new ConceptsResponse.ConceptSummaryResponse( entry.label(), null, true, - entry.childrenIds(), + entry.childrenIds().stream().map(ConceptId::toString).toList(), 0L, 0L, true, @@ -126,8 +126,7 @@ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) { private ConceptResource.ConnectorResponse toTableResponse(DatasetCatalogRepository.Connector connector) { // TODO combine this with ConceptResource#toConnectorResponse - String tableId = connector.tableId(); - DatasetCatalogRepository.TableRecord tableRecord = datasetService.requireTable(tableId); + DatasetCatalogRepository.TableRecord tableRecord = datasetService.requireTable(connector.tableId()); List supportedSecondaryIds = tableRecord.columns().stream() .map(DatasetCatalogRepository.ColumnRecord::secondaryId) .filter(Objects::nonNull) @@ -135,7 +134,7 @@ private ConceptResource.ConnectorResponse toTableResponse(DatasetCatalogReposito .toList(); return new ConceptResource.ConnectorResponse( - tableId, + connector.tableId().toString(), connector.name(), connector.label(), connector.isDefault(), @@ -163,14 +162,9 @@ private ConceptResource.FilterResponse toFilterResponse(DatasetCatalogRepository ); } - 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.id().toString(), column.label(), column.type(), column.secondaryId() diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ColumnId.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ColumnId.java new file mode 100644 index 0000000000..e928aa2036 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ColumnId.java @@ -0,0 +1,37 @@ +package com.bakdata.conquery.quarkus.ids; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public record ColumnId(TableId tableId, String name) { + + public ColumnId { + if (tableId == null) { + throw new IllegalArgumentException("Column id table must not be null."); + } + name = IdPart.requireValid(name, "Column id name"); + } + + public DatasetId datasetId() { + return tableId.datasetId(); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static ColumnId parse(String value) { + String[] parts = IdPart.split(value, "Column id", 3); + if (parts.length != 3) { + throw new IllegalArgumentException("Column id must have exactly three parts: " + value); + } + return new ColumnId(new TableId(DatasetId.parse(parts[0]), parts[1]), parts[2]); + } + + public static ColumnId valueOf(String value) { + return parse(value); + } + + @Override + @JsonValue + public String toString() { + return tableId + "." + name; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConceptId.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConceptId.java new file mode 100644 index 0000000000..8897b32b1e --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConceptId.java @@ -0,0 +1,44 @@ +package com.bakdata.conquery.quarkus.ids; + +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public record ConceptId(DatasetId datasetId, List path) { + + public ConceptId { + if (datasetId == null) { + throw new IllegalArgumentException("Concept id dataset must not be null."); + } + path = path == null ? List.of() : path.stream() + .map(part -> IdPart.requireValid(part, "Concept id path part")) + .toList(); + } + + public ConceptId child(String name) { + List childPath = new java.util.ArrayList<>(path); + childPath.add(name); + return new ConceptId(datasetId, childPath); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static ConceptId parse(String value) { + String[] parts = IdPart.split(value, "Concept id", 1); + return new ConceptId(DatasetId.parse(parts[0]), Arrays.stream(parts).skip(1).toList()); + } + + public static ConceptId valueOf(String value) { + return parse(value); + } + + @Override + @JsonValue + public String toString() { + if (path.isEmpty()) { + return datasetId.toString(); + } + return datasetId + "." + String.join(".", path); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConnectorId.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConnectorId.java new file mode 100644 index 0000000000..be20615571 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConnectorId.java @@ -0,0 +1,38 @@ +package com.bakdata.conquery.quarkus.ids; + +import java.util.Arrays; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public record ConnectorId(ConceptId conceptId, String name) { + + public ConnectorId { + if (conceptId == null) { + throw new IllegalArgumentException("Connector id concept must not be null."); + } + name = IdPart.requireValid(name, "Connector id name"); + } + + public DatasetId datasetId() { + return conceptId.datasetId(); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static ConnectorId parse(String value) { + String[] parts = IdPart.split(value, "Connector id", 2); + String connectorName = parts[parts.length - 1]; + ConceptId conceptId = new ConceptId(DatasetId.parse(parts[0]), Arrays.stream(parts, 1, parts.length - 1).toList()); + return new ConnectorId(conceptId, connectorName); + } + + public static ConnectorId valueOf(String value) { + return parse(value); + } + + @Override + @JsonValue + public String toString() { + return conceptId + "." + name; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/DatasetId.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/DatasetId.java new file mode 100644 index 0000000000..5a45f3fa3c --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/DatasetId.java @@ -0,0 +1,26 @@ +package com.bakdata.conquery.quarkus.ids; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public record DatasetId(String name) { + + public DatasetId { + name = IdPart.requireValid(name, "Dataset id"); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static DatasetId parse(String value) { + return new DatasetId(IdPart.split(value, "Dataset id", 1)[0]); + } + + public static DatasetId valueOf(String value) { + return parse(value); + } + + @Override + @JsonValue + public String toString() { + return name; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/IdPart.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/IdPart.java new file mode 100644 index 0000000000..d666f20fba --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/IdPart.java @@ -0,0 +1,41 @@ +package com.bakdata.conquery.quarkus.ids; + +import java.util.regex.Pattern; + +final class IdPart { + + public static final String PART_PATTERN = "[A-Za-z0-9äöüß_/-]+"; + private static final Pattern VALID_PART = Pattern.compile(PART_PATTERN); + + private IdPart() { + } + + static String requireValid(String value, String label) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(label + " must not be blank."); + } + String trimmed = value.trim(); + if (!VALID_PART.matcher(trimmed).matches()) { + throw new IllegalArgumentException(label + " must match " + PART_PATTERN + " but was: " + value); + } + return trimmed; + } + + static boolean isValid(String value) { + return value != null && VALID_PART.matcher(value.trim()).matches(); + } + + static String[] split(String value, String label, int minimumParts) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(label + " must not be blank."); + } + String[] parts = value.trim().split("\\.", -1); + if (parts.length < minimumParts) { + throw new IllegalArgumentException(label + " must have at least " + minimumParts + " part(s): " + value); + } + for (int index = 0; index < parts.length; index++) { + parts[index] = requireValid(parts[index], label + " part " + index); + } + return parts; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/IdPartSanitizer.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/IdPartSanitizer.java new file mode 100644 index 0000000000..207e7c2545 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/IdPartSanitizer.java @@ -0,0 +1,22 @@ +package com.bakdata.conquery.quarkus.ids; + +public final class IdPartSanitizer { + + private IdPartSanitizer() { + } + + public static String sanitize(String value, String label) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(label + " must not be blank."); + } + String sanitized = value.trim() + .replace('.', '_') + .replaceAll("[^A-Za-z0-9äöü_/-]+", "_") + .replaceAll("_+", "_") + .replaceAll("^_+|_+$", ""); + if (sanitized.isBlank() || !IdPart.isValid(sanitized)) { + throw new IllegalArgumentException(label + " cannot be sanitized into a valid id part: " + value); + } + return sanitized; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/TableId.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/TableId.java new file mode 100644 index 0000000000..5cc8efed4b --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/TableId.java @@ -0,0 +1,33 @@ +package com.bakdata.conquery.quarkus.ids; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public record TableId(DatasetId datasetId, String name) { + + public TableId { + if (datasetId == null) { + throw new IllegalArgumentException("Table id dataset must not be null."); + } + name = IdPart.requireValid(name, "Table id name"); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static TableId parse(String value) { + String[] parts = IdPart.split(value, "Table id", 2); + if (parts.length != 2) { + throw new IllegalArgumentException("Table id must have exactly two parts: " + value); + } + return new TableId(DatasetId.parse(parts[0]), parts[1]); + } + + public static TableId valueOf(String value) { + return parse(value); + } + + @Override + @JsonValue + public String toString() { + return datasetId + "." + name; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java index 92459fa2a3..99e1bb6a33 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java @@ -6,10 +6,12 @@ import java.util.stream.Stream; import com.bakdata.conquery.quarkus.config.StorageRuntimeConfig; +import com.bakdata.conquery.quarkus.ids.ConceptId; +import com.bakdata.conquery.quarkus.ids.DatasetId; +import com.bakdata.conquery.quarkus.ids.TableId; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; 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; @@ -30,30 +32,51 @@ public List listDatasets() { } public DatasetCatalogRepository.DatasetRecord requireDataset(String datasetId) { + return requireDataset(DatasetId.parse(datasetId)); + } + + public DatasetCatalogRepository.DatasetRecord requireDataset(DatasetId datasetId) { return requireNamespace(datasetId).dataset(); } public DatasetCatalogRepository.Concept requireConcept(String conceptId) { - NamespaceStorage namespace = requireNamespaceForScopedObject(conceptId, "concept"); + return requireConcept(ConceptId.parse(conceptId)); + } + + public DatasetCatalogRepository.Concept requireConcept(ConceptId conceptId) { + NamespaceStorage namespace = requireNamespace(conceptId.datasetId()); return namespace.findConcept(conceptId).orElseThrow(() -> new NotFoundException("Unknown concept: " + conceptId)); } public List listRootConceptsForDataset(String datasetId) { + return listRootConceptsForDataset(DatasetId.parse(datasetId)); + } + + public List listRootConceptsForDataset(DatasetId datasetId) { return requireNamespace(datasetId).listConcepts(); } public List listTablesForDataset(String datasetId) { + return listTablesForDataset(DatasetId.parse(datasetId)); + } + + public List listTablesForDataset(DatasetId datasetId) { return requireNamespace(datasetId).listTables(); } public DatasetCatalogRepository.TableRecord requireTable(String tableId) { - NamespaceStorage namespace = requireNamespaceForScopedObject(tableId, "table"); + return requireTable(TableId.parse(tableId)); + } + + public DatasetCatalogRepository.TableRecord requireTable(TableId tableId) { + NamespaceStorage namespace = requireNamespace(tableId.datasetId()); return namespace.findTable(tableId).orElseThrow(() -> new NotFoundException("Unknown table: " + tableId)); } public ConceptCodeResolution resolveConceptCodes(String rootConceptId, List codes) { - DatasetCatalogRepository.Concept rootConcept = requireConcept(rootConceptId); - NamespaceStorage namespace = requireNamespaceForScopedObject(rootConceptId, "concept"); + ConceptId parsedRootConceptId = ConceptId.parse(rootConceptId); + DatasetCatalogRepository.Concept rootConcept = requireConcept(parsedRootConceptId); + NamespaceStorage namespace = requireNamespace(parsedRootConceptId.datasetId()); // TODO implement with updated concept model throw new NotImplementedYet(); @@ -70,7 +93,7 @@ public ConceptCodeResolution resolveConceptCodes(String rootConceptId, List(resolvedInOrder)), unknownCodes); } - private Stream> resolvableCodeEntries(DatasetCatalogRepository.ConceptElement concept) { + private Stream> resolvableCodeEntries(DatasetCatalogRepository.ConceptElement concept) { if (concept.condition() == null) { return Stream.empty(); } @@ -84,17 +107,11 @@ private String normalizeCode(String value) { return value.trim().toLowerCase(Locale.ROOT); } - private NamespaceStorage requireNamespace(String datasetId) { + private NamespaceStorage requireNamespace(DatasetId 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/storage/ConfigDatasetCatalogRepository.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/storage/ConfigDatasetCatalogRepository.java index 78c0b36e8e..18bb6b213a 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 @@ -2,11 +2,12 @@ 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.util.ScopedId; +import com.bakdata.conquery.quarkus.ids.ConceptId; +import com.bakdata.conquery.quarkus.ids.DatasetId; +import com.bakdata.conquery.quarkus.ids.TableId; import io.quarkus.arc.properties.IfBuildProperty; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; @@ -19,15 +20,15 @@ public class ConfigDatasetCatalogRepository implements DatasetCatalogRepository @Inject DatasetMetadataFolderLoader metadataFolderLoader; - private Map datasetsById = Map.of(); - private Map> conceptsByDatasetId = Map.of(); - private Map> tablesByDatasetId = Map.of(); + 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<>(); + Map datasetIndex = new LinkedHashMap<>(); + Map> conceptDatasetIndex = new LinkedHashMap<>(); + Map> tableDatasetIndex = new LinkedHashMap<>(); metadataFolderLoader.loadConfiguredDatasets().forEach(dataset -> { datasetIndex.put(dataset.dataset().id(), dataset.dataset()); @@ -46,7 +47,7 @@ public List listDatasets() { } @Override - public Optional findDataset(String datasetId) { + public Optional findDataset(DatasetId datasetId) { return Optional.ofNullable(datasetsById.get(datasetId)); } @@ -56,19 +57,18 @@ public void saveDataset(DatasetRecord dataset) { } @Override - public boolean deleteDataset(String datasetId) { + public boolean deleteDataset(DatasetId datasetId) { throw readOnly(); } @Override - public List listConceptsForDataset(String datasetId) { + public List listConceptsForDataset(DatasetId datasetId) { return conceptsByDatasetId.getOrDefault(datasetId, Map.of()).values().stream().toList(); } @Override - public Optional findConcept(String conceptId) { - return ScopedId.extractDatasetId(conceptId) - .flatMap(datasetId -> Optional.ofNullable(conceptsByDatasetId.getOrDefault(datasetId, Map.of()).get(conceptId))); + public Optional findConcept(ConceptId conceptId) { + return Optional.ofNullable(conceptsByDatasetId.getOrDefault(conceptId.datasetId(), Map.of()).get(conceptId)); } @Override @@ -77,18 +77,17 @@ public void saveConcept(Concept concept) { } @Override - public boolean deleteConcept(String conceptId) { + public boolean deleteConcept(ConceptId conceptId) { throw readOnly(); } - public List listTablesForDataset(String datasetId) { + public List listTablesForDataset(DatasetId 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))); + public Optional findTable(TableId tableId) { + return Optional.ofNullable(tablesByDatasetId.getOrDefault(tableId.datasetId(), Map.of()).get(tableId)); } @Override @@ -97,35 +96,12 @@ public void saveTable(TableRecord table) { } @Override - public boolean deleteTable(String tableId) { + public boolean deleteTable(TableId 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<>(); + 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); } 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 e34a172ce3..1a2a06d1d6 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 @@ -2,47 +2,53 @@ import java.util.*; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.ids.ConceptId; +import com.bakdata.conquery.quarkus.ids.ConnectorId; +import com.bakdata.conquery.quarkus.ids.DatasetId; +import com.bakdata.conquery.quarkus.ids.TableId; + public interface DatasetCatalogRepository { List listDatasets(); - Optional findDataset(String datasetId); + Optional findDataset(DatasetId datasetId); void saveDataset(DatasetRecord dataset); - boolean deleteDataset(String datasetId); + boolean deleteDataset(DatasetId datasetId); - List listConceptsForDataset(String datasetId); + List listConceptsForDataset(DatasetId datasetId); - Optional findConcept(String conceptId); + Optional findConcept(ConceptId conceptId); void saveConcept(Concept concept); - boolean deleteConcept(String conceptId); + boolean deleteConcept(ConceptId conceptId); - List listTablesForDataset(String datasetId); + List listTablesForDataset(DatasetId datasetId); - Optional findTable(String tableId); + Optional findTable(TableId tableId); void saveTable(TableRecord table); - boolean deleteTable(String tableId); + boolean deleteTable(TableId tableId); record DatasetRecord( - String id, + DatasetId id, String label ) { } record ConceptElement( - String id, + ConceptId id, String label, String description, - String parentId, - List children, + ConceptId parentId, + List children, ConceptCondition condition ) { - public ConceptElement(String id, String label, String description) { + public ConceptElement(ConceptId id, String label, String description) { this(id, label, description,null, List.of(), null); } @@ -56,22 +62,21 @@ public boolean hasResolvableCodes() { } record Concept( - String id, + ConceptId id, String label, String description, // All Children flat lookup - Map children, + Map children, // Direct children - List childrenIds, + List childrenIds, List connectors ){} record Connector( - String dataset, - String table, - String column, - String concept, + ConnectorId id, + TableId tableId, + ColumnId columnId, String label, String name, List selects, List filters, - // Use internal rep directly as we won't need data mangling + String validityDatesDescription, List validityDates, boolean isDefault ){ @@ -121,9 +133,11 @@ public DatasetId datasetId() { } record ValidityDate( - String column, - String startColumn, - String endColumn + ValidityDateId id, + String label, + ColumnId columnId, + ColumnId startColumnId, + ColumnId endColumnId ) {} record Select( 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 index d359db12a2..f1f773b220 100644 --- 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 @@ -20,6 +20,8 @@ import com.bakdata.conquery.quarkus.ids.IdPartSanitizer; import com.bakdata.conquery.quarkus.ids.StructureNodeId; import com.bakdata.conquery.quarkus.ids.TableId; +import com.bakdata.conquery.quarkus.ids.ValidityDateId; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; @@ -230,6 +232,9 @@ private DatasetCatalogRepository.Concept collectConcept(ConceptPayload payload, } ConnectorId connectorId = new ConnectorId(conceptId, p.name); + List validityDates = assembleValidityDates( + connectorId, tableId, table, p.validityDates(), fallbackLogCollector + ); return new DatasetCatalogRepository.Connector( connectorId, tableId, @@ -238,7 +243,8 @@ private DatasetCatalogRepository.Concept collectConcept(ConceptPayload payload, p.name, selectDefinitionAssembler.assemble(connectorId, tableId, table, p.selects(), fallbackLogCollector::add, metadataConfig.strictSelectTypes()), filterDefinitionAssembler.assemble(connectorId, tableId, table, p.filters(), fallbackLogCollector::add, metadataConfig.strictFilterTypes()), - Optional.ofNullable(p.validityDates()).orElse(List.of()), + p.validityDatesDescription(), + validityDates, p.isDefault == null || p.isDefault ); @@ -247,7 +253,69 @@ private DatasetCatalogRepository.Concept collectConcept(ConceptPayload payload, log.debug("Loaded concept {} with {} children", conceptId, children.size()); fallbackLogCollector.logSummary(); - return new DatasetCatalogRepository.Concept(conceptId, conceptLabel, payload.description, Map.copyOf(conceptElementsById), directChildIds, connectors); + return new DatasetCatalogRepository.Concept( + conceptId, + conceptLabel, + payload.description, + copyAdditionalInfos(payload.additionalInfos()), + Boolean.TRUE.equals(payload.defaultExcludeFromTimeAggregation()), + Map.copyOf(conceptElementsById), + directChildIds, + connectors + ); + } + + private List assembleValidityDates( + ConnectorId connectorId, + TableId tableId, + DatasetCatalogRepository.TableRecord table, + List payloads, + FallbackLogCollector fallbackLogCollector + ) { + Map columnsByName = table.columns().stream() + .collect(Collectors.toMap(column -> column.id().name(), column -> column)); + Set ids = new HashSet<>(); + List validityDates = new ArrayList<>(); + for (ValidityDatePayload payload : Optional.ofNullable(payloads).orElse(List.of())) { + String name = idPartFromPreferredOrFallback( + payload.name(), payload.label(), "validity date id", connectorId, fallbackLogCollector + ); + ValidityDateId id = new ValidityDateId(connectorId, name); + if (!ids.add(id)) { + throw new IllegalStateException("Connector '" + connectorId + "' contains duplicate validity date id '" + id + "'."); + } + + boolean hasColumn = normalizeBlank(payload.column()).isPresent(); + boolean hasStartColumn = normalizeBlank(payload.startColumn()).isPresent(); + boolean hasEndColumn = normalizeBlank(payload.endColumn()).isPresent(); + if (hasColumn == (hasStartColumn || hasEndColumn) || hasStartColumn != hasEndColumn) { + throw new IllegalStateException("Validity date '" + id + "' must define either column or both startColumn and endColumn."); + } + + ColumnId columnId = hasColumn ? requireValidityDateColumn(id, tableId, columnsByName, payload.column()) : null; + ColumnId startColumnId = hasStartColumn ? requireValidityDateColumn(id, tableId, columnsByName, payload.startColumn()) : null; + ColumnId endColumnId = hasEndColumn ? requireValidityDateColumn(id, tableId, columnsByName, payload.endColumn()) : null; + String label = firstNonBlank(payload.label(), payload.name(), name).orElse(name); + validityDates.add(new DatasetCatalogRepository.ValidityDate(id, label, columnId, startColumnId, endColumnId)); + } + return List.copyOf(validityDates); + } + + private ColumnId requireValidityDateColumn( + ValidityDateId validityDateId, + TableId tableId, + Map columnsByName, + String rawColumnName + ) { + String columnName = normalizeLocalColumnName(rawColumnName); + DatasetCatalogRepository.ColumnRecord column = columnsByName.get(columnName); + if (column == null) { + throw new IllegalStateException("Validity date '" + validityDateId + "' references unknown column '" + columnName + "' in table '" + tableId + "'."); + } + if (column.type() != DatasetCatalogRepository.ColumnType.DATE && column.type() != DatasetCatalogRepository.ColumnType.DATE_RANGE) { + throw new IllegalStateException("Validity date '" + validityDateId + "' references non-date column '" + column.id() + "'."); + } + return column.id(); } private void collectConceptChildren(ConceptElementPayload payload, ConceptId conceptId, ConceptId parentId, Map conceptElementsById, FallbackLogCollector fallbackLogCollector) { @@ -256,12 +324,18 @@ private void collectConceptChildren(ConceptElementPayload payload, ConceptId con List childIds = children.stream().map(child -> childId(conceptId, child, fallbackLogCollector)).toList(); DatasetCatalogRepository.ConceptCondition condition = toConceptCondition(payload.condition()); - conceptElementsById.put(conceptId, new DatasetCatalogRepository.ConceptElement(conceptId, conceptLabel, payload.description, parentId, childIds, condition)); + conceptElementsById.put(conceptId, new DatasetCatalogRepository.ConceptElement( + conceptId, conceptLabel, payload.description, copyAdditionalInfos(payload.additionalInfos()), parentId, childIds, condition + )); for (int index = 0; index < children.size(); index++) { collectConceptChildren(children.get(index), childIds.get(index), conceptId, conceptElementsById, fallbackLogCollector); } } + private List copyAdditionalInfos(List additionalInfos) { + return List.copyOf(Optional.ofNullable(additionalInfos).orElse(List.of())); + } + private ConceptId childId(ConceptId parentId, ConceptElementPayload child, FallbackLogCollector fallbackLogCollector) { String childName = idPartFromPreferredOrFallback(child.name(), child.label(), "concept child id", parentId, fallbackLogCollector); @@ -519,14 +593,19 @@ private record DatasetPayload( @JsonIgnoreProperties(ignoreUnknown = true) private record ConceptPayload( - String name, String label, String description, List<@Valid ConceptElementPayload> children, + String name, String label, String description, + List additionalInfos, + Boolean defaultExcludeFromTimeAggregation, + List<@Valid ConceptElementPayload> children, @NotNull @Valid List<@Valid ConnectorPayload> connectors ) { } @JsonIgnoreProperties(ignoreUnknown = true) private record ConceptElementPayload( - String name, String label, String description, List<@Valid ConceptElementPayload> children, + String name, String label, String description, + List additionalInfos, + List<@Valid ConceptElementPayload> children, @Valid ConditionPayload condition ) { } @@ -542,8 +621,14 @@ private record StructureNodePayload( private record ConnectorPayload( String table, String column, String label, String name, List<@Valid SelectDefinition> selects, List<@Valid FilterDefinition> filters, - // Use internal rep directly as we won't need data mangling - List validityDates, @JsonProperty("default") Boolean isDefault + @JsonAlias("validityDatesTooltip") String validityDatesDescription, + List validityDates, @JsonProperty("default") Boolean isDefault + ) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record ValidityDatePayload( + String name, String label, String column, String startColumn, String endColumn ) { } 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 b93ad5506d..59acbd43c6 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 @@ -21,9 +21,17 @@ void conceptEndpointReturnsConceptMap() { .body("imdb.parent", nullValue()) .body("imdb.detailsAvailable", nullValue()) .body("imdb.codeListResolvable", equalTo(false)) + .body("imdb.additionalInfos[0].key", equalTo("Source")) + .body("imdb.additionalInfos[0].value", equalTo("IMDb test metadata")) + .body("imdb.excludeFromTimeAggregation", equalTo(false)) .body("imdb.tables.size()", equalTo(1)) .body("imdb.tables[0].id", equalTo("imdb.title")) .body("imdb.tables[0].default", equalTo(true)) + .body("imdb.tables[0].dateColumn.tooltip", equalTo("Choose the release date")) + .body("imdb.tables[0].dateColumn.defaultValue", equalTo("imdb.titles.Release_date")) + .body("imdb.tables[0].dateColumn.value", nullValue()) + .body("imdb.tables[0].dateColumn.options[0].value", equalTo("imdb.titles.Release_date")) + .body("imdb.tables[0].dateColumn.options[0].label", equalTo("Release date")) .body("imdb.tables[0].selects[0].id", equalTo("imdb.titles.Title")) .body("imdb.tables[0].selects[0].label", equalTo("Title")) .body("imdb.tables[0].selects[0].default", equalTo(true)) @@ -31,7 +39,9 @@ void conceptEndpointReturnsConceptMap() { .body("'imdb.movie'.parent", equalTo("imdb")) .body("'imdb.movie'.active", nullValue()) .body("'imdb.movie'.detailsAvailable", nullValue()) - .body("'imdb.movie'.codeListResolvable", equalTo(false)); + .body("'imdb.movie'.codeListResolvable", equalTo(false)) + .body("'imdb.movie'.additionalInfos[0].key", equalTo("Kind")) + .body("'imdb.movie'.excludeFromTimeAggregation", nullValue()); } @Test 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 bf11a4ada2..2e0075cc75 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 @@ -60,9 +60,13 @@ void conceptsEndpointRespondsWithFrontendCompatibleShape() { .body("concepts.imdb.active", equalTo(true)) .body("concepts.imdb.parent", equalTo("imdb.catalog.Movies")) .body("concepts.imdb.detailsAvailable", equalTo(true)) + .body("concepts.imdb.codeListResolvable", equalTo(true)) + .body("concepts.imdb.additionalInfos[0].key", equalTo("Source")) + .body("concepts.imdb.excludeFromTimeAggregation", equalTo(false)) .body("concepts.imdb.children", equalTo(java.util.List.of("imdb.movie"))) .body("concepts.imdb.tables[0].default", equalTo(true)) .body("concepts.imdb.tables[0].connectorId", equalTo("imdb.titles")) + .body("concepts.imdb.tables[0].dateColumn.defaultValue", equalTo("imdb.titles.Release_date")) .body("concepts.imdb.tables[0].filters[0].id", equalTo("imdb.titles.release_age")) .body("concepts.imdb.tables[0].selects[0].id", equalTo("imdb.titles.Title")) .body("concepts.imdb.tables[0].selects[0].default", equalTo(true)) diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java index a8613e4f22..975121806d 100644 --- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java @@ -23,13 +23,13 @@ class DatasetServiceTest { void resolvesConceptCodesAgainstRequestedConceptSubtree() { DatasetService service = new DatasetService(); service.namespaceStorageRegistry = new TestNamespaceStorageRegistry(List.of( - new DatasetCatalogRepository.Concept(cid("demo.icd"), "ICD", null, Map.of( + new DatasetCatalogRepository.Concept(cid("demo.icd"), "ICD", null, List.of(), false, Map.of( - cid("demo.icd.a00"), new DatasetCatalogRepository.ConceptElement(cid("demo.icd.a00"), "A00",null,cid("demo.icd"), List.of(cid("demo.icd.a00.a00_0")), equal("A00", "A000")), - cid("demo.icd.a00.a00_0"), new DatasetCatalogRepository.ConceptElement(cid("demo.icd.a00.a00_0"), "A00.0",null, cid("demo.icd.a00"), List.of(), equal("A000")) + cid("demo.icd.a00"), new DatasetCatalogRepository.ConceptElement(cid("demo.icd.a00"), "A00", null, List.of(), cid("demo.icd"), List.of(cid("demo.icd.a00.a00_0")), equal("A00", "A000")), + cid("demo.icd.a00.a00_0"), new DatasetCatalogRepository.ConceptElement(cid("demo.icd.a00.a00_0"), "A00.0", null, List.of(), cid("demo.icd.a00"), List.of(), equal("A000")) ), List.of(cid("demo.icd.a00")), null), - new DatasetCatalogRepository.Concept(cid("demo.other"), "Other",null, null, null, List.of()) + new DatasetCatalogRepository.Concept(cid("demo.other"), "Other", null, List.of(), false, null, null, List.of()) )); DatasetService.ConceptCodeResolution resolution = service.resolveConceptCodes("demo.icd", List.of("A00", "A000", "OTHER", "UNKNOWN")); 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 index 2ba971f98d..e16da09e96 100644 --- 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 @@ -21,6 +21,7 @@ import com.bakdata.conquery.quarkus.ids.SelectId; import com.bakdata.conquery.quarkus.ids.StructureNodeId; import com.bakdata.conquery.quarkus.ids.TableId; +import com.bakdata.conquery.quarkus.ids.ValidityDateId; import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; @@ -273,6 +274,12 @@ public boolean strictSelectTypes() { assertEquals("FIRST", titleSelect.implementationType()); assertEquals(DatasetCatalogRepository.SelectResultType.primitive("STRING"), titleSelect.resultType()); assertEquals(List.of(ColumnId.parse("imdb.title.name")), titleSelect.requiredColumns()); + DatasetCatalogRepository.Connector titleConnector = imdb.conceptsById().get(ConceptId.parse("imdb")).connectors().getFirst(); + assertEquals("Choose the release date", titleConnector.validityDatesDescription()); + DatasetCatalogRepository.ValidityDate releaseDate = titleConnector.validityDates().getFirst(); + assertEquals(ValidityDateId.parse("imdb.titles.Release_date"), releaseDate.id()); + assertEquals("Release date", releaseDate.label()); + assertEquals(ColumnId.parse("imdb.title.release_date"), releaseDate.columnId()); assertTrue(imdb.tablesById().containsKey(TableId.parse("imdb.title"))); DatasetCatalogRepository.TableRecord title = imdb.tablesById().get(TableId.parse("imdb.title")); @@ -409,6 +416,30 @@ void rejectsStructureNodeWithUnknownConceptAtStartup(@TempDir Path tempDir) thro assertTrue(error.getMessage().contains("Structure node 'demo.invalid' references unknown concept 'demo.missing'")); } + @Test + void rejectsIncompleteValidityDateAtStartup(@TempDir Path tempDir) throws Exception { + Path dataset = tempDir.resolve("demo"); + Files.createDirectories(dataset.resolve("conceptTrees")); + Files.createDirectories(dataset.resolve("tables")); + Files.writeString(dataset.resolve("tables/events.table.json"), """ + {"name":"events","columns":[{"name":"start","type":"DATE"}]} + """); + Files.writeString(dataset.resolve("conceptTrees/events.concept.json"), """ + { + "name":"events", + "children":[], + "connectors":[{ + "name":"events", + "table":"events", + "validityDates":{"name":"period","startColumn":"start"} + }] + } + """); + + IllegalStateException error = assertThrows(IllegalStateException.class, () -> loader(tempDir, true).loadConfiguredDatasets()); + assertTrue(error.getMessage().contains("Validity date 'demo.events.events.period' must define either column or both startColumn and endColumn")); + } + @Test void reportsCascadedFilterConstraintWithFileAndPropertyPath(@TempDir Path tempDir) throws Exception { Path dataset = tempDir.resolve("demo"); diff --git a/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json b/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json index bc85f2a5a5..e660246516 100644 --- a/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json +++ b/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json @@ -1,10 +1,22 @@ { "name": "imdb", "label": "IMDb", + "additionalInfos": [ + { + "key": "Source", + "value": "IMDb test metadata" + } + ], "children": [ { "name": "movie", "label": "Movie", + "additionalInfos": [ + { + "key": "Kind", + "value": "Feature film" + } + ], "children": [] } ], @@ -13,6 +25,11 @@ "name": "titles", "label": "Titles", "table": "title", + "validityDatesDescription": "Choose the release date", + "validityDates": { + "label": "Release date", + "column": "release_date" + }, "filters": [ { "type": "DATE_DISTANCE", diff --git a/scripts/normalize_concept_connector_columns.py b/scripts/normalize_concept_connector_columns.py index 1d0f5b3d9f..079d735ba7 100755 --- a/scripts/normalize_concept_connector_columns.py +++ b/scripts/normalize_concept_connector_columns.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Normalize connector, filter, and select column references in concept JSON files. +"""Normalize connector, filter, select, and validity-date columns in concept JSON files. Connector columns of the form "table.column" become: "table": "table", @@ -11,6 +11,9 @@ Connector-select column references become local column names as well. This includes column, startColumn, endColumn, subtractColumn, distinctByColumn, distinctBy, and the values in flags maps. + +Validity-date column, startColumn, and endColumn references become local column +names as well. """ from __future__ import annotations @@ -111,50 +114,58 @@ def normalize_select_columns(value: Any) -> int: ) -def normalize_connectors(value: Any) -> tuple[int, int, int]: +def normalize_validity_date_columns(value: Any) -> int: + return normalize_definition_columns(value, ("column", "startColumn", "endColumn"), ()) + + +def normalize_connectors(value: Any) -> tuple[int, int, int, int]: connector_changes = 0 filter_changes = 0 select_changes = 0 + validity_date_changes = 0 if isinstance(value, dict): connectors = value.get("connectors") - if isinstance(connectors, list): - for connector in connectors: - if not isinstance(connector, dict): - continue + connector_definitions = connectors if isinstance(connectors, list) else [connectors] + for connector in connector_definitions: + if not isinstance(connector, dict): + continue - split = split_table_column(connector.get("column")) - if split is not None: - set_connector_table_column(connector, split[0], split[1]) - connector_changes += 1 + split = split_table_column(connector.get("column")) + if split is not None: + set_connector_table_column(connector, split[0], split[1]) + connector_changes += 1 - filter_changes += normalize_filter_columns(connector.get("filters")) - select_changes += normalize_select_columns(connector.get("selects")) + filter_changes += normalize_filter_columns(connector.get("filters")) + select_changes += normalize_select_columns(connector.get("selects")) + validity_date_changes += normalize_validity_date_columns(connector.get("validityDates")) for child in value.values(): - child_connector_changes, child_filter_changes, child_select_changes = normalize_connectors(child) + child_connector_changes, child_filter_changes, child_select_changes, child_validity_date_changes = normalize_connectors(child) connector_changes += child_connector_changes filter_changes += child_filter_changes select_changes += child_select_changes + validity_date_changes += child_validity_date_changes elif isinstance(value, list): for child in value: - child_connector_changes, child_filter_changes, child_select_changes = normalize_connectors(child) + child_connector_changes, child_filter_changes, child_select_changes, child_validity_date_changes = normalize_connectors(child) connector_changes += child_connector_changes filter_changes += child_filter_changes select_changes += child_select_changes + validity_date_changes += child_validity_date_changes - return connector_changes, filter_changes, select_changes + return connector_changes, filter_changes, select_changes, validity_date_changes -def normalize_file(path: Path, dry_run: bool) -> tuple[int, int, int]: +def normalize_file(path: Path, dry_run: bool) -> tuple[int, int, int, int]: data = json.loads(path.read_text(encoding="utf-8")) - connector_changes, filter_changes, select_changes = normalize_connectors(data) + connector_changes, filter_changes, select_changes, validity_date_changes = normalize_connectors(data) - if not dry_run and (connector_changes or filter_changes or select_changes): + if not dry_run and (connector_changes or filter_changes or select_changes or validity_date_changes): path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - return connector_changes, filter_changes, select_changes + return connector_changes, filter_changes, select_changes, validity_date_changes def main() -> int: @@ -164,11 +175,12 @@ def main() -> int: args = parser.parse_args() for path in args.concept_json: - connector_changes, filter_changes, select_changes = normalize_file(path, args.dry_run) + connector_changes, filter_changes, select_changes, validity_date_changes = normalize_file(path, args.dry_run) action = "would update" if args.dry_run else "updated" print( f"{path}: {action} {connector_changes} connector column(s), " - f"{filter_changes} filter column(s), {select_changes} select column(s)" + f"{filter_changes} filter column(s), {select_changes} select column(s), " + f"{validity_date_changes} validity-date column(s)" ) return 0 From f0ed7ddf848c9378305a00083dd31f0762a4495c Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:38:33 +0200 Subject: [PATCH 40/45] adds polymorphism for concept conditions --- backend-quarkus/README.md | 7 +- .../concepts/conditions/ConceptCondition.java | 22 ++ .../conditions/ConceptConditionProvider.java | 25 +++ .../conditions/UnknownConceptCondition.java | 38 ++++ .../definitions/AbstractConceptCondition.java | 16 ++ .../definitions/AndConceptCondition.java | 20 ++ .../ColumnEqualConceptCondition.java | 21 ++ .../definitions/EqualConceptCondition.java | 17 ++ .../definitions/NotConceptCondition.java | 18 ++ .../definitions/OrConceptCondition.java | 20 ++ .../PrefixListConceptCondition.java | 17 ++ .../PrefixRangeConceptCondition.java | 24 +++ .../definitions/PresentConceptCondition.java | 15 ++ .../AbstractConceptConditionProvider.java | 18 ++ .../AndConceptConditionProvider.java | 17 ++ .../ColumnEqualConceptConditionProvider.java | 17 ++ .../EqualConceptConditionProvider.java | 17 ++ .../NotConceptConditionProvider.java | 17 ++ .../providers/OrConceptConditionProvider.java | 17 ++ .../PrefixListConceptConditionProvider.java | 17 ++ .../PrefixRangeConceptConditionProvider.java | 17 ++ .../PresentConceptConditionProvider.java | 17 ++ .../AbstractFilterProvider.java} | 6 +- .../BigMultiSelectFilterProvider.java | 4 +- .../CategoryMaxSumFilterProvider.java | 4 +- .../CountFilterProvider.java | 4 +- .../CountQuartersFilterProvider.java | 4 +- .../DateDistanceFilterProvider.java | 4 +- .../DurationSumFilterProvider.java | 4 +- .../FlagsFilterProvider.java | 4 +- .../NumberFilterProvider.java | 4 +- .../PrefixTextFilterProvider.java | 4 +- .../QuartersInYearFilterProvider.java | 4 +- .../SelectFilterProvider.java | 4 +- .../SingleSelectFilterProvider.java | 4 +- .../SumFilterProvider.java | 4 +- .../AbstractMappableSelectProvider.java | 26 +++ .../AbstractSelectProvider.java} | 6 +- .../AbstractSingleColumnSelectProvider.java | 20 ++ .../CountQuartersSelectProvider.java | 29 +++ .../providers/CountSelectProvider.java | 31 +++ .../providers/DateDistanceSelectProvider.java | 29 +++ .../providers/DateUnionSelectProvider.java | 29 +++ .../providers/DistinctSelectProvider.java | 34 +++ .../providers/DurationSumSelectProvider.java | 29 +++ .../providers/FirstSelectProvider.java | 24 +++ .../providers/FlagsSelectProvider.java | 29 +++ .../selects/providers/LastSelectProvider.java | 24 +++ .../providers/PrefixSelectProvider.java | 29 +++ .../QuartersInYearSelectProvider.java | 29 +++ .../providers/RandomSelectProvider.java | 24 +++ .../selects/providers/SumSelectProvider.java | 46 +++++ .../specific/BuiltinSelectProviders.java | 193 ------------------ .../quarkus/services/DatasetService.java | 17 -- .../storage/DatasetCatalogRepository.java | 28 +-- .../storage/DatasetMetadataFolderLoader.java | 21 +- .../ConceptConditionDeserializationTest.java | 52 +++++ .../SelectDefinitionDeserializationTest.java | 2 +- .../models/PolymorphicModelsOpenApiTest.java | 10 + .../quarkus/services/DatasetServiceTest.java | 9 +- .../DatasetMetadataFolderLoaderTest.java | 46 ++++- 61 files changed, 988 insertions(+), 300 deletions(-) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/UnknownConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AbstractConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AndConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/ColumnEqualConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/EqualConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/NotConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/OrConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixListConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixRangeConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PresentConceptCondition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AbstractConceptConditionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AndConceptConditionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/ColumnEqualConceptConditionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/EqualConceptConditionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/NotConceptConditionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/OrConceptConditionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixListConceptConditionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixRangeConceptConditionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PresentConceptConditionProvider.java rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific/AbstractBuiltinFilterProvider.java => providers/AbstractFilterProvider.java} (94%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/BigMultiSelectFilterProvider.java (80%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/CategoryMaxSumFilterProvider.java (87%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/CountFilterProvider.java (83%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/CountQuartersFilterProvider.java (80%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/DateDistanceFilterProvider.java (81%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/DurationSumFilterProvider.java (83%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/FlagsFilterProvider.java (80%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/NumberFilterProvider.java (83%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/PrefixTextFilterProvider.java (81%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/QuartersInYearFilterProvider.java (80%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/SelectFilterProvider.java (83%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/SingleSelectFilterProvider.java (80%) rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/{specific => providers}/SumFilterProvider.java (87%) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractMappableSelectProvider.java rename backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/{specific/AbstractBuiltinSelectProvider.java => providers/AbstractSelectProvider.java} (94%) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSingleColumnSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountQuartersSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateDistanceSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateUnionSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DistinctSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DurationSumSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FirstSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FlagsSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/LastSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/PrefixSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/QuartersInYearSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/RandomSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/SumSelectProvider.java delete mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/specific/BuiltinSelectProviders.java create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionDeserializationTest.java diff --git a/backend-quarkus/README.md b/backend-quarkus/README.md index 34ab764af7..9f113d4357 100644 --- a/backend-quarkus/README.md +++ b/backend-quarkus/README.md @@ -59,7 +59,7 @@ Metadata models are code-first. Java model classes, Jackson annotations, Bean Va `@Schema` annotations are used both for loading metadata files and for generating their schemas. Registered model families are exposed in the OpenAPI document at `GET /q/openapi` as `oneOf` schemas with discriminator mappings. -Filter and select implementations are extensible from another CDI-enabled JAR. An extension provides: +Filter, select, and concept-condition implementations are extensible from another CDI-enabled JAR. A filter extension provides: 1. A concrete class implementing `FilterDefinition`, annotated with OpenAPI `@Schema`. 2. A CDI bean implementing `FilterDefinitionProvider` for its type id, model class, and business-model conversion. @@ -68,6 +68,11 @@ Connector selects use the equivalent `SelectDefinition` and `SelectDefinitionPro types fail startup by default; `conquery.metadata.strict-filter-types` and `conquery.metadata.strict-select-types` can independently switch their family to warning-and-skip behavior. +Concept-tree conditions use `ConceptCondition` and `ConceptConditionProvider`. Because skipping a condition would +change the meaning of a concept node, unknown condition types always fail metadata validation. The built-in condition +types are `EQUAL`, `PREFIX_LIST`, `PREFIX_RANGE`, `COLUMN_EQUAL`, `PRESENT`, `AND`, `OR`, and `NOT`. The old `GROOVY` +condition is intentionally not registered because script execution requires a separate security and runtime decision. + The generic `PolymorphicModelRegistry` validates duplicate type ids and base/model compatibility at startup. Its registrations are also installed into Jackson and projected into OpenAPI. Extension JARs must be discoverable by Quarkus bean and Jandex indexing so their provider and annotated model class are available during application assembly. diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptCondition.java new file mode 100644 index 0000000000..7cc4342b02 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptCondition.java @@ -0,0 +1,22 @@ +package com.bakdata.conquery.quarkus.concepts.conditions; + +import com.bakdata.conquery.quarkus.models.PolymorphicModelBase; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = UnknownConceptCondition.class +) +@Schema(name = "MetadataConceptCondition", description = "Condition assigned to a concept-tree node. The concrete model is selected by `type`.", discriminatorProperty = "type") +@PolymorphicModelBase( + schemaName = "MetadataConceptCondition", + description = "Condition assigned to a concept-tree node. The concrete model is selected by `type`." +) +public interface ConceptCondition { + + String getType(); +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionProvider.java new file mode 100644 index 0000000000..db9dc21768 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionProvider.java @@ -0,0 +1,25 @@ +package com.bakdata.conquery.quarkus.concepts.conditions; + +import com.bakdata.conquery.quarkus.models.PolymorphicModelTypeProvider; + +public interface ConceptConditionProvider extends PolymorphicModelTypeProvider { + + String type(); + + Class conditionType(); + + @Override + default Class baseType() { + return ConceptCondition.class; + } + + @Override + default String typeId() { + return type(); + } + + @Override + default Class modelType() { + return conditionType(); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/UnknownConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/UnknownConceptCondition.java new file mode 100644 index 0000000000..5bf2a256fc --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/UnknownConceptCondition.java @@ -0,0 +1,38 @@ +package com.bakdata.conquery.quarkus.concepts.conditions; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import jakarta.validation.constraints.AssertTrue; + +public final class UnknownConceptCondition implements ConceptCondition { + + private String type; + private final Map properties = new LinkedHashMap<>(); + + @Override + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @JsonAnySetter + public void put(String name, Object value) { + properties.put(name, value); + } + + @JsonAnyGetter + public Map properties() { + return Map.copyOf(properties); + } + + @AssertTrue(message = "must use a registered concept condition type") + public boolean isRegisteredType() { + return false; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AbstractConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AbstractConceptCondition.java new file mode 100644 index 0000000000..9cf0553226 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AbstractConceptCondition.java @@ -0,0 +1,16 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.definitions; + +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class AbstractConceptCondition implements ConceptCondition { + + @Schema(description = "Concept-condition implementation discriminator.") + private String type; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AndConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AndConceptCondition.java new file mode 100644 index 0000000000..cc33b00dc2 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AndConceptCondition.java @@ -0,0 +1,20 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.definitions; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataAndConceptCondition", description = "Matches when all nested conditions match.") +public final class AndConceptCondition extends AbstractConceptCondition { + + @Valid + @NotEmpty + private List conditions; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/ColumnEqualConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/ColumnEqualConceptCondition.java new file mode 100644 index 0000000000..024356eb0b --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/ColumnEqualConceptCondition.java @@ -0,0 +1,21 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.definitions; + +import java.util.List; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataColumnEqualConceptCondition", description = "Matches when another column of a record/event has one of the configured values.") +public final class ColumnEqualConceptCondition extends AbstractConceptCondition { + + @NotEmpty + private List values; + + @NotBlank + private String column; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/EqualConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/EqualConceptCondition.java new file mode 100644 index 0000000000..ca5dd2c6f4 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/EqualConceptCondition.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.definitions; + +import java.util.List; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataEqualConceptCondition", description = "Matches when a value is contained exactly in this list.") +public final class EqualConceptCondition extends AbstractConceptCondition { + + @NotEmpty + private List values; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/NotConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/NotConceptCondition.java new file mode 100644 index 0000000000..cf96cc397b --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/NotConceptCondition.java @@ -0,0 +1,18 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.definitions; + +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataNotConceptCondition", description = "Matches when its nested condition does not match.") +public final class NotConceptCondition extends AbstractConceptCondition { + + @Valid + @NotNull + private ConceptCondition condition; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/OrConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/OrConceptCondition.java new file mode 100644 index 0000000000..205375476e --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/OrConceptCondition.java @@ -0,0 +1,20 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.definitions; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataOrConceptCondition", description = "Matches when at least one nested condition matches.") +public final class OrConceptCondition extends AbstractConceptCondition { + + @Valid + @NotEmpty + private List conditions; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixListConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixListConceptCondition.java new file mode 100644 index 0000000000..31b6079c38 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixListConceptCondition.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.definitions; + +import java.util.List; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataPrefixListConceptCondition", description = "Matches values beginning with one of the configured prefixes.") +public final class PrefixListConceptCondition extends AbstractConceptCondition { + + @NotEmpty + private List prefixes; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixRangeConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixRangeConceptCondition.java new file mode 100644 index 0000000000..117fe8a467 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixRangeConceptCondition.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.definitions; + +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataPrefixRangeConceptCondition", description = "Matches fixed-length prefixes inside an inclusive lexical range.") +public final class PrefixRangeConceptCondition extends AbstractConceptCondition { + + @NotBlank + private String min; + + @NotBlank + private String max; + + @AssertTrue(message = "min and max must have equal lengths and min must be smaller than or equal to max") + public boolean isValidRange() { + return min == null || max == null || min.length() == max.length() && min.compareTo(max) <= 0; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PresentConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PresentConceptCondition.java new file mode 100644 index 0000000000..f60f1f4fb5 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PresentConceptCondition.java @@ -0,0 +1,15 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.definitions; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataPresentConceptCondition", description = "Matches when the configured column in the same record/event has a value present.") +public final class PresentConceptCondition extends AbstractConceptCondition { + + @NotBlank + private String column; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AbstractConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AbstractConceptConditionProvider.java new file mode 100644 index 0000000000..76ebe73f40 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AbstractConceptConditionProvider.java @@ -0,0 +1,18 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.providers; + +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptConditionProvider; + +abstract class AbstractConceptConditionProvider implements ConceptConditionProvider { + + private final Class conditionType; + + protected AbstractConceptConditionProvider(Class conditionType) { + this.conditionType = conditionType; + } + + @Override + public Class conditionType() { + return conditionType; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AndConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AndConceptConditionProvider.java new file mode 100644 index 0000000000..678d0371c5 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AndConceptConditionProvider.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.providers; + +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.AndConceptCondition; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class AndConceptConditionProvider extends AbstractConceptConditionProvider { + + public AndConceptConditionProvider() { + super(AndConceptCondition.class); + } + + @Override + public String type() { + return "AND"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/ColumnEqualConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/ColumnEqualConceptConditionProvider.java new file mode 100644 index 0000000000..b6ae5f3d35 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/ColumnEqualConceptConditionProvider.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.providers; + +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.ColumnEqualConceptCondition; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ColumnEqualConceptConditionProvider extends AbstractConceptConditionProvider { + + public ColumnEqualConceptConditionProvider() { + super(ColumnEqualConceptCondition.class); + } + + @Override + public String type() { + return "COLUMN_EQUAL"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/EqualConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/EqualConceptConditionProvider.java new file mode 100644 index 0000000000..00e6d6575b --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/EqualConceptConditionProvider.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.providers; + +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.EqualConceptCondition; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class EqualConceptConditionProvider extends AbstractConceptConditionProvider { + + public EqualConceptConditionProvider() { + super(EqualConceptCondition.class); + } + + @Override + public String type() { + return "EQUAL"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/NotConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/NotConceptConditionProvider.java new file mode 100644 index 0000000000..c384c52ff7 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/NotConceptConditionProvider.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.providers; + +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.NotConceptCondition; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class NotConceptConditionProvider extends AbstractConceptConditionProvider { + + public NotConceptConditionProvider() { + super(NotConceptCondition.class); + } + + @Override + public String type() { + return "NOT"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/OrConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/OrConceptConditionProvider.java new file mode 100644 index 0000000000..15b8ba6415 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/OrConceptConditionProvider.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.providers; + +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.OrConceptCondition; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class OrConceptConditionProvider extends AbstractConceptConditionProvider { + + public OrConceptConditionProvider() { + super(OrConceptCondition.class); + } + + @Override + public String type() { + return "OR"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixListConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixListConceptConditionProvider.java new file mode 100644 index 0000000000..b772367d72 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixListConceptConditionProvider.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.providers; + +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.PrefixListConceptCondition; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class PrefixListConceptConditionProvider extends AbstractConceptConditionProvider { + + public PrefixListConceptConditionProvider() { + super(PrefixListConceptCondition.class); + } + + @Override + public String type() { + return "PREFIX_LIST"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixRangeConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixRangeConceptConditionProvider.java new file mode 100644 index 0000000000..b6591d7674 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixRangeConceptConditionProvider.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.providers; + +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.PrefixRangeConceptCondition; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class PrefixRangeConceptConditionProvider extends AbstractConceptConditionProvider { + + public PrefixRangeConceptConditionProvider() { + super(PrefixRangeConceptCondition.class); + } + + @Override + public String type() { + return "PREFIX_RANGE"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PresentConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PresentConceptConditionProvider.java new file mode 100644 index 0000000000..672a693cd0 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PresentConceptConditionProvider.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.conditions.providers; + +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.PresentConceptCondition; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class PresentConceptConditionProvider extends AbstractConceptConditionProvider { + + public PresentConceptConditionProvider() { + super(PresentConceptCondition.class); + } + + @Override + public String type() { + return "PRESENT"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/AbstractBuiltinFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java similarity index 94% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/AbstractBuiltinFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java index a3e3b2d219..f0009993b8 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/AbstractBuiltinFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinitionProvider; @@ -13,11 +13,11 @@ import java.util.List; import java.util.Map; -abstract class AbstractBuiltinFilterProvider implements FilterDefinitionProvider { +abstract class AbstractFilterProvider implements FilterDefinitionProvider { private final Class payloadType; - protected AbstractBuiltinFilterProvider(Class payloadType) { + protected AbstractFilterProvider(Class payloadType) { this.payloadType = payloadType; } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/BigMultiSelectFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java similarity index 80% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/BigMultiSelectFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java index cb015efb41..bf95bd6d21 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/BigMultiSelectFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.List; @@ -8,7 +8,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class BigMultiSelectFilterProvider extends AbstractBuiltinFilterProvider { +public class BigMultiSelectFilterProvider extends AbstractFilterProvider { public BigMultiSelectFilterProvider() { super(BigMultiSelectFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/CategoryMaxSumFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CategoryMaxSumFilterProvider.java similarity index 87% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/CategoryMaxSumFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CategoryMaxSumFilterProvider.java index 471dd492fc..f34a28dbcc 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/CategoryMaxSumFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CategoryMaxSumFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.ArrayList; import java.util.List; @@ -10,7 +10,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class CategoryMaxSumFilterProvider extends AbstractBuiltinFilterProvider { +public class CategoryMaxSumFilterProvider extends AbstractFilterProvider { public CategoryMaxSumFilterProvider() { super(CategoryMaxSumFilterDefinition.class); diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/CountFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java similarity index 83% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/CountFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java index c2f38f5ec8..af58fcceb5 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/CountFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.CountFilterDefinition; @@ -7,7 +7,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class CountFilterProvider extends AbstractBuiltinFilterProvider { +public class CountFilterProvider extends AbstractFilterProvider { public CountFilterProvider() { super(CountFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/CountQuartersFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountQuartersFilterProvider.java similarity index 80% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/CountQuartersFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountQuartersFilterProvider.java index d5d965e134..64efec5022 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/CountQuartersFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountQuartersFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.List; @@ -8,7 +8,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class CountQuartersFilterProvider extends AbstractBuiltinFilterProvider { +public class CountQuartersFilterProvider extends AbstractFilterProvider { public CountQuartersFilterProvider() { super(CountQuartersFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/DateDistanceFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DateDistanceFilterProvider.java similarity index 81% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/DateDistanceFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DateDistanceFilterProvider.java index e8b0f53568..34ee70dc57 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/DateDistanceFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DateDistanceFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.List; @@ -8,7 +8,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class DateDistanceFilterProvider extends AbstractBuiltinFilterProvider { +public class DateDistanceFilterProvider extends AbstractFilterProvider { public DateDistanceFilterProvider() { super(DateDistanceFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/DurationSumFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DurationSumFilterProvider.java similarity index 83% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/DurationSumFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DurationSumFilterProvider.java index 6610cce64a..a137af4cf8 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/DurationSumFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DurationSumFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.DurationSumFilterDefinition; @@ -7,7 +7,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class DurationSumFilterProvider extends AbstractBuiltinFilterProvider { +public class DurationSumFilterProvider extends AbstractFilterProvider { public DurationSumFilterProvider() { super(DurationSumFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/FlagsFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/FlagsFilterProvider.java similarity index 80% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/FlagsFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/FlagsFilterProvider.java index 437eaf7600..07bd053bce 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/FlagsFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/FlagsFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.FlagsFilterDefinition; @@ -6,7 +6,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class FlagsFilterProvider extends AbstractBuiltinFilterProvider { +public class FlagsFilterProvider extends AbstractFilterProvider { public FlagsFilterProvider() { super(FlagsFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/NumberFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/NumberFilterProvider.java similarity index 83% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/NumberFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/NumberFilterProvider.java index 14e854b3b9..374d7a967b 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/NumberFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/NumberFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.List; @@ -9,7 +9,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class NumberFilterProvider extends AbstractBuiltinFilterProvider { +public class NumberFilterProvider extends AbstractFilterProvider { public NumberFilterProvider() { super(NumberFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/PrefixTextFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/PrefixTextFilterProvider.java similarity index 81% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/PrefixTextFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/PrefixTextFilterProvider.java index 2960caf7e5..89b271d590 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/PrefixTextFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/PrefixTextFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.List; @@ -8,7 +8,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class PrefixTextFilterProvider extends AbstractBuiltinFilterProvider { +public class PrefixTextFilterProvider extends AbstractFilterProvider { public PrefixTextFilterProvider() { super(PrefixTextFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/QuartersInYearFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/QuartersInYearFilterProvider.java similarity index 80% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/QuartersInYearFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/QuartersInYearFilterProvider.java index 03fed9c52a..d4bddb6921 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/QuartersInYearFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/QuartersInYearFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.List; @@ -8,7 +8,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class QuartersInYearFilterProvider extends AbstractBuiltinFilterProvider { +public class QuartersInYearFilterProvider extends AbstractFilterProvider { public QuartersInYearFilterProvider() { super(QuartersInYearFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/SelectFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SelectFilterProvider.java similarity index 83% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/SelectFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SelectFilterProvider.java index 7e3c26169d..af0d8cbb03 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/SelectFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SelectFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.List; @@ -8,7 +8,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class SelectFilterProvider extends AbstractBuiltinFilterProvider { +public class SelectFilterProvider extends AbstractFilterProvider { public SelectFilterProvider() { super(MultiSelectFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/SingleSelectFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SingleSelectFilterProvider.java similarity index 80% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/SingleSelectFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SingleSelectFilterProvider.java index 178e0a86a9..e8cbff4272 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/SingleSelectFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SingleSelectFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.List; @@ -8,7 +8,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class SingleSelectFilterProvider extends AbstractBuiltinFilterProvider { +public class SingleSelectFilterProvider extends AbstractFilterProvider { public SingleSelectFilterProvider() { super(SingleSelectFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/SumFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SumFilterProvider.java similarity index 87% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/SumFilterProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SumFilterProvider.java index 708212a742..a0779c3669 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/specific/SumFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SumFilterProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.filters.specific; +package com.bakdata.conquery.quarkus.concepts.filters.providers; import java.util.ArrayList; import java.util.List; @@ -10,7 +10,7 @@ import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped -public class SumFilterProvider extends AbstractBuiltinFilterProvider { +public class SumFilterProvider extends AbstractFilterProvider { public SumFilterProvider() { super(SumFilterDefinition.class); } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractMappableSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractMappableSelectProvider.java new file mode 100644 index 0000000000..e7f1df578e --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractMappableSelectProvider.java @@ -0,0 +1,26 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.MappableSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; + +abstract class AbstractMappableSelectProvider extends AbstractSingleColumnSelectProvider { + + protected AbstractMappableSelectProvider(Class payloadType) { + super(payloadType); + } + + protected DatasetCatalogRepository.Select convertMapped(SelectConversionContext context, T payload) { + ColumnId column = context.columnId(payload.getColumn()); + if (payload.getMapping() != null || payload.getSubstring() != null) { + requireColumnType(context, column, DatasetCatalogRepository.ColumnType.STRING); + } + DatasetCatalogRepository.SelectResultType result = payload.getMapping() == null + ? resultType(context, column) + : primitive("STRING"); + return select(context, payload, result, List.of(column)); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/specific/AbstractBuiltinSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSelectProvider.java similarity index 94% rename from backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/specific/AbstractBuiltinSelectProvider.java rename to backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSelectProvider.java index 026032f5da..c6654e56f0 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/specific/AbstractBuiltinSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSelectProvider.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.quarkus.concepts.selects.specific; +package com.bakdata.conquery.quarkus.concepts.selects.providers; import java.util.ArrayList; import java.util.List; @@ -13,11 +13,11 @@ import com.bakdata.conquery.quarkus.ids.SelectId; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; -abstract class AbstractBuiltinSelectProvider implements SelectDefinitionProvider { +abstract class AbstractSelectProvider implements SelectDefinitionProvider { private final Class payloadType; - protected AbstractBuiltinSelectProvider(Class payloadType) { + protected AbstractSelectProvider(Class payloadType) { this.payloadType = payloadType; } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSingleColumnSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSingleColumnSelectProvider.java new file mode 100644 index 0000000000..75b6e18aa8 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSingleColumnSelectProvider.java @@ -0,0 +1,20 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.SingleColumnSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; + +abstract class AbstractSingleColumnSelectProvider extends AbstractSelectProvider { + + protected AbstractSingleColumnSelectProvider(Class payloadType) { + super(payloadType); + } + + protected DatasetCatalogRepository.Select convertColumn(SelectConversionContext context, T payload) { + ColumnId column = context.columnId(payload.getColumn()); + return select(context, payload, resultType(context, column), List.of(column)); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountQuartersSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountQuartersSelectProvider.java new file mode 100644 index 0000000000..ddb36d2cd3 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountQuartersSelectProvider.java @@ -0,0 +1,29 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.CountQuartersSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CountQuartersSelectProvider extends AbstractSelectProvider { + + public CountQuartersSelectProvider() { + super(CountQuartersSelectDefinition.class); + } + + @Override + public String type() { + return "COUNT_QUARTERS"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, CountQuartersSelectDefinition payload) { + List columns = dateRangeColumns(context, payload); + requireDateRangeTypes(context, columns); + return select(context, payload, primitive("INTEGER"), columns); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountSelectProvider.java new file mode 100644 index 0000000000..f2ac8802b2 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountSelectProvider.java @@ -0,0 +1,31 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.ArrayList; +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.CountSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CountSelectProvider extends AbstractSelectProvider { + + public CountSelectProvider() { + super(CountSelectDefinition.class); + } + + @Override + public String type() { + return "COUNT"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, CountSelectDefinition payload) { + List columns = new ArrayList<>(); + columns.add(context.columnId(payload.getColumn())); + columns.addAll(optionalColumns(context, payload.getDistinctByColumn())); + return select(context, payload, primitive("INTEGER"), columns); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateDistanceSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateDistanceSelectProvider.java new file mode 100644 index 0000000000..d4659382fc --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateDistanceSelectProvider.java @@ -0,0 +1,29 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.DateDistanceSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class DateDistanceSelectProvider extends AbstractSingleColumnSelectProvider { + + public DateDistanceSelectProvider() { + super(DateDistanceSelectDefinition.class); + } + + @Override + public String type() { + return "DATE_DISTANCE"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, DateDistanceSelectDefinition payload) { + ColumnId column = context.columnId(payload.getColumn()); + requireColumnType(context, column, DatasetCatalogRepository.ColumnType.DATE, DatasetCatalogRepository.ColumnType.DATE_RANGE); + return select(context, payload, primitive("INTEGER"), List.of(column)); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateUnionSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateUnionSelectProvider.java new file mode 100644 index 0000000000..ac18109fa8 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateUnionSelectProvider.java @@ -0,0 +1,29 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.DateUnionSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class DateUnionSelectProvider extends AbstractSelectProvider { + + public DateUnionSelectProvider() { + super(DateUnionSelectDefinition.class); + } + + @Override + public String type() { + return "DATE_UNION"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, DateUnionSelectDefinition payload) { + List columns = dateRangeColumns(context, payload); + requireDateRangeTypes(context, columns); + return select(context, payload, list("DATE_RANGE"), columns); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DistinctSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DistinctSelectProvider.java new file mode 100644 index 0000000000..4af8a1704e --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DistinctSelectProvider.java @@ -0,0 +1,34 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.DistinctSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class DistinctSelectProvider extends AbstractMappableSelectProvider { + + public DistinctSelectProvider() { + super(DistinctSelectDefinition.class); + } + + @Override + public String type() { + return "DISTINCT"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, DistinctSelectDefinition payload) { + ColumnId column = context.columnId(payload.getColumn()); + if (payload.getMapping() != null || payload.getSubstring() != null) { + requireColumnType(context, column, DatasetCatalogRepository.ColumnType.STRING); + } + DatasetCatalogRepository.SelectResultType elementType = payload.getMapping() == null + ? resultType(context, column) + : primitive("STRING"); + return select(context, payload, DatasetCatalogRepository.SelectResultType.list(elementType), List.of(column)); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DurationSumSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DurationSumSelectProvider.java new file mode 100644 index 0000000000..f0e5ef224f --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DurationSumSelectProvider.java @@ -0,0 +1,29 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.DurationSumSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class DurationSumSelectProvider extends AbstractSelectProvider { + + public DurationSumSelectProvider() { + super(DurationSumSelectDefinition.class); + } + + @Override + public String type() { + return "DURATION_SUM"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, DurationSumSelectDefinition payload) { + List rangeColumns = dateRangeColumns(context, payload); + requireDateRangeTypes(context, rangeColumns); + return select(context, payload, primitive("INTEGER"), append(rangeColumns, optionalColumns(context, payload.getDistinctBy()))); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FirstSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FirstSelectProvider.java new file mode 100644 index 0000000000..fd02809741 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FirstSelectProvider.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.FirstSelectDefinition; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class FirstSelectProvider extends AbstractMappableSelectProvider { + + public FirstSelectProvider() { + super(FirstSelectDefinition.class); + } + + @Override + public String type() { + return "FIRST"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, FirstSelectDefinition payload) { + return convertMapped(context, payload); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FlagsSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FlagsSelectProvider.java new file mode 100644 index 0000000000..e1c0a9e449 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FlagsSelectProvider.java @@ -0,0 +1,29 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.FlagsSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class FlagsSelectProvider extends AbstractSelectProvider { + + public FlagsSelectProvider() { + super(FlagsSelectDefinition.class); + } + + @Override + public String type() { + return "FLAGS"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, FlagsSelectDefinition payload) { + List columns = payload.getFlags().values().stream().map(context::columnId).toList(); + columns.forEach(column -> requireColumnType(context, column, DatasetCatalogRepository.ColumnType.BOOLEAN)); + return select(context, payload, list("STRING"), columns); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/LastSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/LastSelectProvider.java new file mode 100644 index 0000000000..7eea1514ce --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/LastSelectProvider.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.LastSelectDefinition; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class LastSelectProvider extends AbstractMappableSelectProvider { + + public LastSelectProvider() { + super(LastSelectDefinition.class); + } + + @Override + public String type() { + return "LAST"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, LastSelectDefinition payload) { + return convertMapped(context, payload); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/PrefixSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/PrefixSelectProvider.java new file mode 100644 index 0000000000..fe87c0e820 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/PrefixSelectProvider.java @@ -0,0 +1,29 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.PrefixSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class PrefixSelectProvider extends AbstractSingleColumnSelectProvider { + + public PrefixSelectProvider() { + super(PrefixSelectDefinition.class); + } + + @Override + public String type() { + return "PREFIX"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, PrefixSelectDefinition payload) { + ColumnId column = context.columnId(payload.getColumn()); + requireColumnType(context, column, DatasetCatalogRepository.ColumnType.STRING); + return select(context, payload, list("STRING"), List.of(column)); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/QuartersInYearSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/QuartersInYearSelectProvider.java new file mode 100644 index 0000000000..a8c54715fb --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/QuartersInYearSelectProvider.java @@ -0,0 +1,29 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.QuartersInYearSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class QuartersInYearSelectProvider extends AbstractSingleColumnSelectProvider { + + public QuartersInYearSelectProvider() { + super(QuartersInYearSelectDefinition.class); + } + + @Override + public String type() { + return "QUARTERS_IN_YEAR"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, QuartersInYearSelectDefinition payload) { + ColumnId column = context.columnId(payload.getColumn()); + requireColumnType(context, column, DatasetCatalogRepository.ColumnType.DATE, DatasetCatalogRepository.ColumnType.DATE_RANGE); + return select(context, payload, primitive("INTEGER"), List.of(column)); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/RandomSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/RandomSelectProvider.java new file mode 100644 index 0000000000..f8e023fddb --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/RandomSelectProvider.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.RandomSelectDefinition; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class RandomSelectProvider extends AbstractMappableSelectProvider { + + public RandomSelectProvider() { + super(RandomSelectDefinition.class); + } + + @Override + public String type() { + return "RANDOM"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, RandomSelectDefinition payload) { + return convertMapped(context, payload); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/SumSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/SumSelectProvider.java new file mode 100644 index 0000000000..970dbf91af --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/SumSelectProvider.java @@ -0,0 +1,46 @@ +package com.bakdata.conquery.quarkus.concepts.selects.providers; + +import java.util.ArrayList; +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.definitions.SumSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ColumnId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class SumSelectProvider extends AbstractSingleColumnSelectProvider { + + public SumSelectProvider() { + super(SumSelectDefinition.class); + } + + @Override + public String type() { + return "SUM"; + } + + @Override + public DatasetCatalogRepository.Select convert(SelectConversionContext context, SumSelectDefinition payload) { + ColumnId column = context.columnId(payload.getColumn()); + requireColumnType( + context, + column, + DatasetCatalogRepository.ColumnType.INTEGER, + DatasetCatalogRepository.ColumnType.MONEY, + DatasetCatalogRepository.ColumnType.DECIMAL, + DatasetCatalogRepository.ColumnType.REAL + ); + List columns = new ArrayList<>(List.of(column)); + if (payload.getSubtractColumn() != null && !payload.getSubtractColumn().isBlank()) { + ColumnId subtractColumn = context.columnId(payload.getSubtractColumn()); + if (context.columnType(subtractColumn) != context.columnType(column)) { + throw new IllegalArgumentException("Select SUM columns must have the same type."); + } + columns.add(subtractColumn); + } + columns.addAll(optionalColumns(context, payload.getDistinctByColumn())); + return select(context, payload, resultType(context, column), columns); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/specific/BuiltinSelectProviders.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/specific/BuiltinSelectProviders.java deleted file mode 100644 index 8fe6ad2c87..0000000000 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/specific/BuiltinSelectProviders.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.bakdata.conquery.quarkus.concepts.selects.specific; - -import java.util.ArrayList; -import java.util.List; - -import com.bakdata.conquery.quarkus.concepts.selects.SelectConversionContext; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.CountQuartersSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.CountSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.DateDistanceSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.DateUnionSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.DistinctSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.DurationSumSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.FirstSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.FlagsSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.LastSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.MappableSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.PrefixSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.QuartersInYearSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.RandomSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.SingleColumnSelectDefinition; -import com.bakdata.conquery.quarkus.concepts.selects.definitions.SumSelectDefinition; -import com.bakdata.conquery.quarkus.ids.ColumnId; -import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; -import jakarta.enterprise.context.ApplicationScoped; - -public final class BuiltinSelectProviders { - - private BuiltinSelectProviders() { - } - - private abstract static class SingleColumnProvider extends AbstractBuiltinSelectProvider { - protected SingleColumnProvider(Class payloadType) { - super(payloadType); - } - - protected DatasetCatalogRepository.Select convertColumn(SelectConversionContext context, T payload) { - ColumnId column = context.columnId(payload.getColumn()); - return select(context, payload, resultType(context, column), List.of(column)); - } - } - - private abstract static class MappableProvider extends SingleColumnProvider { - protected MappableProvider(Class payloadType) { - super(payloadType); - } - - protected DatasetCatalogRepository.Select convertMapped(SelectConversionContext context, T payload) { - ColumnId column = context.columnId(payload.getColumn()); - if (payload.getMapping() != null || payload.getSubstring() != null) requireColumnType(context, column, DatasetCatalogRepository.ColumnType.STRING); - DatasetCatalogRepository.SelectResultType result = payload.getMapping() == null ? resultType(context, column) : primitive("STRING"); - return select(context, payload, result, List.of(column)); - } - } - - @ApplicationScoped - public static class DistinctProvider extends MappableProvider { - public DistinctProvider() { super(DistinctSelectDefinition.class); } - @Override public String type() { return "DISTINCT"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, DistinctSelectDefinition payload) { - ColumnId column = context.columnId(payload.getColumn()); - if (payload.getMapping() != null || payload.getSubstring() != null) requireColumnType(context, column, DatasetCatalogRepository.ColumnType.STRING); - return select(context, payload, DatasetCatalogRepository.SelectResultType.list(payload.getMapping() == null ? resultType(context, column) : primitive("STRING")), List.of(column)); - } - } - - @ApplicationScoped - public static class FirstProvider extends MappableProvider { - public FirstProvider() { super(FirstSelectDefinition.class); } - @Override public String type() { return "FIRST"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, FirstSelectDefinition payload) { return convertMapped(context, payload); } - } - - @ApplicationScoped - public static class LastProvider extends MappableProvider { - public LastProvider() { super(LastSelectDefinition.class); } - @Override public String type() { return "LAST"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, LastSelectDefinition payload) { return convertMapped(context, payload); } - } - - @ApplicationScoped - public static class RandomProvider extends MappableProvider { - public RandomProvider() { super(RandomSelectDefinition.class); } - @Override public String type() { return "RANDOM"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, RandomSelectDefinition payload) { return convertMapped(context, payload); } - } - - @ApplicationScoped - public static class CountProvider extends AbstractBuiltinSelectProvider { - public CountProvider() { super(CountSelectDefinition.class); } - @Override public String type() { return "COUNT"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, CountSelectDefinition payload) { - List columns = new ArrayList<>(); - columns.add(context.columnId(payload.getColumn())); - columns.addAll(optionalColumns(context, payload.getDistinctByColumn())); - return select(context, payload, primitive("INTEGER"), columns); - } - } - - @ApplicationScoped - public static class CountQuartersProvider extends AbstractBuiltinSelectProvider { - public CountQuartersProvider() { super(CountQuartersSelectDefinition.class); } - @Override public String type() { return "COUNT_QUARTERS"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, CountQuartersSelectDefinition payload) { - List columns = dateRangeColumns(context, payload); - requireDateRangeTypes(context, columns); - return select(context, payload, primitive("INTEGER"), columns); - } - } - - @ApplicationScoped - public static class DateDistanceProvider extends SingleColumnProvider { - public DateDistanceProvider() { super(DateDistanceSelectDefinition.class); } - @Override public String type() { return "DATE_DISTANCE"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, DateDistanceSelectDefinition payload) { - ColumnId column = context.columnId(payload.getColumn()); - requireColumnType(context, column, DatasetCatalogRepository.ColumnType.DATE, DatasetCatalogRepository.ColumnType.DATE_RANGE); - return select(context, payload, primitive("INTEGER"), List.of(column)); - } - } - - @ApplicationScoped - public static class DateUnionProvider extends AbstractBuiltinSelectProvider { - public DateUnionProvider() { super(DateUnionSelectDefinition.class); } - @Override public String type() { return "DATE_UNION"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, DateUnionSelectDefinition payload) { - List columns = dateRangeColumns(context, payload); - requireDateRangeTypes(context, columns); - return select(context, payload, list("DATE_RANGE"), columns); - } - } - - @ApplicationScoped - public static class DurationSumProvider extends AbstractBuiltinSelectProvider { - public DurationSumProvider() { super(DurationSumSelectDefinition.class); } - @Override public String type() { return "DURATION_SUM"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, DurationSumSelectDefinition payload) { - List rangeColumns = dateRangeColumns(context, payload); - requireDateRangeTypes(context, rangeColumns); - return select(context, payload, primitive("INTEGER"), append(rangeColumns, optionalColumns(context, payload.getDistinctBy()))); - } - } - - @ApplicationScoped - public static class FlagsProvider extends AbstractBuiltinSelectProvider { - public FlagsProvider() { super(FlagsSelectDefinition.class); } - @Override public String type() { return "FLAGS"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, FlagsSelectDefinition payload) { - List columns = payload.getFlags().values().stream().map(context::columnId).toList(); - columns.forEach(column -> requireColumnType(context, column, DatasetCatalogRepository.ColumnType.BOOLEAN)); - return select(context, payload, list("STRING"), columns); - } - } - - @ApplicationScoped - public static class PrefixProvider extends SingleColumnProvider { - public PrefixProvider() { super(PrefixSelectDefinition.class); } - @Override public String type() { return "PREFIX"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, PrefixSelectDefinition payload) { - ColumnId column = context.columnId(payload.getColumn()); - requireColumnType(context, column, DatasetCatalogRepository.ColumnType.STRING); - return select(context, payload, list("STRING"), List.of(column)); - } - } - - @ApplicationScoped - public static class QuartersInYearProvider extends SingleColumnProvider { - public QuartersInYearProvider() { super(QuartersInYearSelectDefinition.class); } - @Override public String type() { return "QUARTERS_IN_YEAR"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, QuartersInYearSelectDefinition payload) { - ColumnId column = context.columnId(payload.getColumn()); - requireColumnType(context, column, DatasetCatalogRepository.ColumnType.DATE, DatasetCatalogRepository.ColumnType.DATE_RANGE); - return select(context, payload, primitive("INTEGER"), List.of(column)); - } - } - - @ApplicationScoped - public static class SumProvider extends SingleColumnProvider { - public SumProvider() { super(SumSelectDefinition.class); } - @Override public String type() { return "SUM"; } - @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, SumSelectDefinition payload) { - ColumnId column = context.columnId(payload.getColumn()); - requireColumnType(context, column, DatasetCatalogRepository.ColumnType.INTEGER, DatasetCatalogRepository.ColumnType.MONEY, DatasetCatalogRepository.ColumnType.DECIMAL, DatasetCatalogRepository.ColumnType.REAL); - List columns = new ArrayList<>(List.of(column)); - if (payload.getSubtractColumn() != null && !payload.getSubtractColumn().isBlank()) { - ColumnId subtractColumn = context.columnId(payload.getSubtractColumn()); - if (context.columnType(subtractColumn) != context.columnType(column)) throw new IllegalArgumentException("Select SUM columns must have the same type."); - columns.add(subtractColumn); - } - columns.addAll(optionalColumns(context, payload.getDistinctByColumn())); - return select(context, payload, resultType(context, column), columns); - } - } -} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java index 1c7e64b6ef..1c79e85b25 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/DatasetService.java @@ -1,9 +1,6 @@ package com.bakdata.conquery.quarkus.services; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.stream.Stream; import com.bakdata.conquery.quarkus.config.StorageRuntimeConfig; import com.bakdata.conquery.quarkus.ids.ConceptId; @@ -97,20 +94,6 @@ public ConceptCodeResolution resolveConceptCodes(String rootConceptId, List(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); - } - private NamespaceStorage requireNamespace(DatasetId datasetId) { return namespaceStorageRegistry.findNamespace(datasetId) .orElseThrow(() -> new NotFoundException("Unknown dataset: " + datasetId)); 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 cd9dee4273..9f5dd86f0f 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 @@ -2,6 +2,7 @@ import java.util.*; +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.ids.ConceptId; import com.bakdata.conquery.quarkus.ids.ConnectorId; @@ -70,9 +71,6 @@ public ConceptElement(ConceptId id, String label, String description) { children = children == null ? List.of() : List.copyOf(children); } - public boolean hasResolvableCodes() { - return condition != null && !condition.connectorValues().isEmpty(); - } } record Concept( @@ -191,30 +189,6 @@ record FrontendValue( ) { } - record ConceptCondition( - String type, - List values, - String column, - List conditions - ) { - public ConceptCondition { - type = type == null ? null : type.trim().toUpperCase(Locale.ROOT); - values = values == null ? List.of() : List.copyOf(values); - conditions = conditions == null ? List.of() : List.copyOf(conditions); - } - - public List connectorValues() { - // TODO This method is wrong, conditions don't work that way, that you can simply collect a final list of values from them - return switch (type == null ? "" : type) { - case "EQUAL" -> values; - case "AND" -> conditions.stream() - .flatMap(condition -> condition.connectorValues().stream()) - .toList(); - default -> List.of(); - }; - } - } - record TableRecord( TableId id, String label, 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 index f1f773b220..13edcab848 100644 --- 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 @@ -9,6 +9,7 @@ import java.util.stream.Collectors; import com.bakdata.conquery.quarkus.config.DatasetMetadataRuntimeConfig; +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinitionAssembler; import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; @@ -322,10 +323,8 @@ private void collectConceptChildren(ConceptElementPayload payload, ConceptId con String conceptLabel = firstNonBlank(payload.label()).orElse(payload.name); List children = Optional.ofNullable(payload.children()).orElse(List.of()); List childIds = children.stream().map(child -> childId(conceptId, child, fallbackLogCollector)).toList(); - DatasetCatalogRepository.ConceptCondition condition = toConceptCondition(payload.condition()); - conceptElementsById.put(conceptId, new DatasetCatalogRepository.ConceptElement( - conceptId, conceptLabel, payload.description, copyAdditionalInfos(payload.additionalInfos()), parentId, childIds, condition + conceptId, conceptLabel, payload.description, copyAdditionalInfos(payload.additionalInfos()), parentId, childIds, payload.condition() )); for (int index = 0; index < children.size(); index++) { collectConceptChildren(children.get(index), childIds.get(index), conceptId, conceptElementsById, fallbackLogCollector); @@ -342,14 +341,6 @@ private ConceptId childId(ConceptId parentId, ConceptElementPayload child, Fallb return parentId.child(childName); } - private DatasetCatalogRepository.ConceptCondition toConceptCondition(ConditionPayload condition) { - if (condition == null) { - return null; - } - String type = firstNonBlank(condition.type()).orElseThrow(() -> new IllegalStateException("Concept condition is missing required type.")); - return new DatasetCatalogRepository.ConceptCondition(type, condition.values(), condition.column(), Optional.ofNullable(condition.conditions()).orElse(List.of()).stream().map(this::toConceptCondition).toList()); - } - private TableId toConnectorTableId(DatasetId datasetId, String rawTableName) { String tableName = rawTableName; if (tableName.startsWith(datasetId + ".")) { @@ -606,7 +597,7 @@ private record ConceptElementPayload( String name, String label, String description, List additionalInfos, List<@Valid ConceptElementPayload> children, - @Valid ConditionPayload condition + @Valid ConceptCondition condition ) { } @@ -632,12 +623,6 @@ private record ValidityDatePayload( ) { } - @JsonIgnoreProperties(ignoreUnknown = true) - private record ConditionPayload( - String type, List values, String column, List<@Valid ConditionPayload> conditions - ) { - } - @JsonIgnoreProperties(ignoreUnknown = true) private record TablePayload( String name, String label, String primaryColumn, List columns diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionDeserializationTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionDeserializationTest.java new file mode 100644 index 0000000000..5d10940342 --- /dev/null +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionDeserializationTest.java @@ -0,0 +1,52 @@ +package com.bakdata.conquery.quarkus.concepts.conditions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.AndConceptCondition; +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.EqualConceptCondition; +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.NotConceptCondition; +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.PresentConceptCondition; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class ConceptConditionDeserializationTest { + + @Inject + ObjectMapper objectMapper; + + @Test + void deserializesNestedRegisteredConditions() throws Exception { + ConceptCondition condition = objectMapper.readValue(""" + { + "type":"AND", + "conditions":[ + {"type":"EQUAL","values":["A00","A01"]}, + {"type":"NOT","condition":{"type":"PRESENT","column":"deleted_at"}} + ] + } + """, ConceptCondition.class); + + AndConceptCondition and = assertInstanceOf(AndConceptCondition.class, condition); + EqualConceptCondition equal = assertInstanceOf(EqualConceptCondition.class, and.getConditions().getFirst()); + assertEquals("EQUAL", equal.getType()); + assertEquals(java.util.List.of("A00", "A01"), equal.getValues()); + NotConceptCondition not = assertInstanceOf(NotConceptCondition.class, and.getConditions().get(1)); + PresentConceptCondition present = assertInstanceOf(PresentConceptCondition.class, not.getCondition()); + assertEquals("deleted_at", present.getColumn()); + } + + @Test + void keepsUnknownTypeAsExplicitFallbackModel() throws Exception { + ConceptCondition condition = objectMapper.readValue(""" + {"type":"EXTERNAL_CONDITION","expression":"value != null"} + """, ConceptCondition.class); + + UnknownConceptCondition unknown = assertInstanceOf(UnknownConceptCondition.class, condition); + assertEquals("EXTERNAL_CONDITION", unknown.getType()); + assertEquals("value != null", unknown.properties().get("expression")); + } +} diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionDeserializationTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionDeserializationTest.java index 67db26e8ee..37e84aa0ee 100644 --- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionDeserializationTest.java +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionDeserializationTest.java @@ -23,7 +23,7 @@ class SelectDefinitionDeserializationTest { @ParameterizedTest @ValueSource(strings = {"DISTINCT", "FIRST", "LAST", "RANDOM", "COUNT", "COUNT_QUARTERS", "DATE_DISTANCE", "DATE_UNION", "DURATION_SUM", "FLAGS", "PREFIX", "QUARTERS_IN_YEAR", "SUM"}) - void deserializesEveryBuiltinType(String type) throws Exception { + void deserializesEveryRegisteredType(String type) throws Exception { SelectDefinition definition = objectMapper.readValue("{\"type\":\"" + type + "\"}", SelectDefinition.class); assertFalse(definition instanceof UnknownSelectDefinition, type); diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java index 01c1581fb9..325379b3d7 100644 --- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java @@ -47,6 +47,16 @@ void exposesRegisteredFilterModelsAsDiscriminatedSchema() throws Exception { assertEquals("COUNT", findProperty(countSelectSchema, "type").path("const").asText()); assertNotNull(findProperty(countSelectSchema, "column")); assertNotNull(findProperty(countSelectSchema, "distinctByColumn")); + + JsonNode conditionSchema = schemas.path("MetadataConceptCondition"); + assertEquals("type", conditionSchema.path("discriminator").path("propertyName").asText()); + assertEquals("#/components/schemas/MetadataEqualConceptCondition", conditionSchema.path("discriminator").path("mapping").path("EQUAL").asText()); + assertEquals("#/components/schemas/MetadataAndConceptCondition", conditionSchema.path("discriminator").path("mapping").path("AND").asText()); + assertEquals(8, conditionSchema.path("oneOf").size()); + + JsonNode andConditionSchema = schemas.path("MetadataAndConceptCondition"); + assertEquals("AND", findProperty(andConditionSchema, "type").path("const").asText()); + assertNotNull(findProperty(andConditionSchema, "conditions")); } private JsonNode findProperty(JsonNode schema, String name) { diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java index 975121806d..6301fdd482 100644 --- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java @@ -6,6 +6,8 @@ import java.util.Map; import java.util.Optional; +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.EqualConceptCondition; import com.bakdata.conquery.quarkus.ids.ConceptId; import com.bakdata.conquery.quarkus.ids.DatasetId; import com.bakdata.conquery.quarkus.ids.TableId; @@ -38,8 +40,11 @@ void resolvesConceptCodesAgainstRequestedConceptSubtree() { assertEquals(List.of("OTHER", "UNKNOWN"), resolution.unknownCodes()); } - private DatasetCatalogRepository.ConceptCondition equal(String... values) { - return new DatasetCatalogRepository.ConceptCondition("EQUAL", List.of(values), null, List.of()); + private ConceptCondition equal(String... values) { + EqualConceptCondition condition = new EqualConceptCondition(); + condition.setType("EQUAL"); + condition.setValues(List.of(values)); + return condition; } private ConceptId cid(String value) { 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 index e16da09e96..4bb49d9bca 100644 --- 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 @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,6 +13,9 @@ import java.util.Optional; import com.bakdata.conquery.quarkus.config.DatasetMetadataRuntimeConfig; +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.AndConceptCondition; +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.ColumnEqualConceptCondition; +import com.bakdata.conquery.quarkus.concepts.conditions.definitions.EqualConceptCondition; import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinitionAssembler; import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinitionAssembler; import com.bakdata.conquery.quarkus.ids.ColumnId; @@ -197,19 +201,19 @@ public boolean strictSelectTypes() { DatasetCatalogRepository.ConceptElement child = concept.children().get(ConceptId.parse("fdb_demo.icd.a00")); assertEquals(ConceptId.parse("fdb_demo.icd"), child.parentId()); - assertEquals("AND", child.condition().type()); - assertEquals(2, child.condition().conditions().size()); - assertEquals(List.of("A00", "A000"), child.condition().connectorValues()); - DatasetCatalogRepository.ConceptCondition columnCondition = child.condition().conditions().get(1); - assertEquals("COLUMN_EQUAL", columnCondition.type()); - assertEquals("aufnahmeart", columnCondition.column()); - assertEquals(List.of("stationaer"), columnCondition.values()); + AndConceptCondition andCondition = assertInstanceOf(AndConceptCondition.class, child.condition()); + assertEquals(2, andCondition.getConditions().size()); + EqualConceptCondition equalCondition = assertInstanceOf(EqualConceptCondition.class, andCondition.getConditions().getFirst()); + assertEquals(List.of("A00", "A000"), equalCondition.getValues()); + ColumnEqualConceptCondition columnCondition = assertInstanceOf(ColumnEqualConceptCondition.class, andCondition.getConditions().get(1)); + assertEquals("aufnahmeart", columnCondition.getColumn()); + assertEquals(List.of("stationaer"), columnCondition.getValues()); assertEquals(List.of(ConceptId.parse("fdb_demo.icd.a00.A00_0")), child.children()); DatasetCatalogRepository.ConceptElement leaf = concept.children().get(ConceptId.parse("fdb_demo.icd.a00.A00_0")); assertEquals(ConceptId.parse("fdb_demo.icd.a00"), leaf.parentId()); - assertEquals("EQUAL", leaf.condition().type()); - assertEquals(List.of("A000"), leaf.condition().connectorValues()); + EqualConceptCondition leafCondition = assertInstanceOf(EqualConceptCondition.class, leaf.condition()); + assertEquals(List.of("A000"), leafCondition.getValues()); assertTrue(dataset.tablesById().containsKey(TableId.parse("fdb_demo.kh_diagnose"))); DatasetCatalogRepository.TableRecord table = dataset.tablesById().get(TableId.parse("fdb_demo.kh_diagnose")); @@ -440,6 +444,30 @@ void rejectsIncompleteValidityDateAtStartup(@TempDir Path tempDir) throws Except assertTrue(error.getMessage().contains("Validity date 'demo.events.events.period' must define either column or both startColumn and endColumn")); } + @Test + void rejectsUnknownConceptConditionAtStartup(@TempDir Path tempDir) throws Exception { + Path dataset = tempDir.resolve("demo"); + Files.createDirectories(dataset.resolve("conceptTrees")); + Files.createDirectories(dataset.resolve("tables")); + Files.writeString(dataset.resolve("conceptTrees/events.concept.json"), """ + { + "name":"events", + "connectors":[], + "children":[{ + "name":"invalid", + "condition":{"type":"EXTERNAL_CONDITION","expression":"value != null"} + }] + } + """); + + ConstraintViolationException error = assertThrows( + ConstraintViolationException.class, + () -> loader(tempDir, true).loadConfiguredDatasets() + ); + assertTrue(error.getMessage().contains("children[0].condition.registeredType")); + assertTrue(error.getMessage().contains("must use a registered concept condition type")); + } + @Test void reportsCascadedFilterConstraintWithFileAndPropertyPath(@TempDir Path tempDir) throws Exception { Path dataset = tempDir.resolve("demo"); From d12dc3198f9212de817e4e61777b835149edc819 Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:18:17 +0200 Subject: [PATCH 41/45] adds concept selects --- backend-quarkus/README.md | 5 +- .../conquery/quarkus/api/ConceptResource.java | 12 +++- .../quarkus/api/DatasetsResource.java | 12 +++- .../ConceptSelectConversionContext.java | 33 +++++++++ .../concept/ConceptSelectDefinition.java | 22 ++++++ .../ConceptSelectDefinitionAssembler.java | 59 ++++++++++++++++ .../ConceptSelectDefinitionProvider.java | 28 ++++++++ .../ConceptSelectDefinitionRegistry.java | 35 ++++++++++ .../UnknownConceptSelectDefinition.java | 32 +++++++++ .../AbstractConceptSelectDefinition.java | 28 ++++++++ .../ConceptValuesConceptSelectDefinition.java | 14 ++++ ...EventDateUnionConceptSelectDefinition.java | 7 ++ ...entDurationSumConceptSelectDefinition.java | 7 ++ .../ExistsConceptSelectDefinition.java | 7 ++ .../QuarterConceptSelectDefinition.java | 21 ++++++ .../AbstractConceptSelectProvider.java | 47 +++++++++++++ .../ConceptValuesConceptSelectProvider.java | 24 +++++++ .../EventDateUnionConceptSelectProvider.java | 24 +++++++ ...EventDurationSumConceptSelectProvider.java | 24 +++++++ .../ExistsConceptSelectProvider.java | 24 +++++++ .../QuarterConceptSelectProvider.java | 24 +++++++ .../conquery/quarkus/ids/ConceptSelectId.java | 38 ++++++++++ .../storage/DatasetCatalogRepository.java | 17 ++++- .../storage/DatasetMetadataFolderLoader.java | 15 +++- .../quarkus/api/ConceptResourceTest.java | 4 ++ .../quarkus/api/DatasetsResourceTest.java | 3 + ...ptSelectDefinitionDeserializationTest.java | 69 +++++++++++++++++++ .../models/PolymorphicModelsOpenApiTest.java | 9 +++ .../quarkus/services/DatasetServiceTest.java | 4 +- .../DatasetMetadataFolderLoaderTest.java | 34 +++++++++ .../imdb/conceptTrees/imdb.concept.json | 7 ++ 31 files changed, 678 insertions(+), 11 deletions(-) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectConversionContext.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionAssembler.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionRegistry.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/UnknownConceptSelectDefinition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/AbstractConceptSelectDefinition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ConceptValuesConceptSelectDefinition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDateUnionConceptSelectDefinition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDurationSumConceptSelectDefinition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ExistsConceptSelectDefinition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/QuarterConceptSelectDefinition.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/AbstractConceptSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ConceptValuesConceptSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDateUnionConceptSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDurationSumConceptSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ExistsConceptSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/QuarterConceptSelectProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConceptSelectId.java create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionDeserializationTest.java diff --git a/backend-quarkus/README.md b/backend-quarkus/README.md index 9f113d4357..ae2d85ff44 100644 --- a/backend-quarkus/README.md +++ b/backend-quarkus/README.md @@ -64,8 +64,9 @@ Filter, select, and concept-condition implementations are extensible from anothe 1. A concrete class implementing `FilterDefinition`, annotated with OpenAPI `@Schema`. 2. A CDI bean implementing `FilterDefinitionProvider` for its type id, model class, and business-model conversion. -Connector selects use the equivalent `SelectDefinition` and `SelectDefinitionProvider` extension points. Unknown -types fail startup by default; `conquery.metadata.strict-filter-types` and +Connector selects use the equivalent `SelectDefinition` and `SelectDefinitionProvider` extension points. Concept-level +selects form a separate family with `ConceptSelectDefinition` and `ConceptSelectDefinitionProvider`. Unknown types +fail startup by default; `conquery.metadata.strict-filter-types` and `conquery.metadata.strict-select-types` can independently switch their family to warning-and-skip behavior. Concept-tree conditions use `ConceptCondition` and `ConceptConditionProvider`. Because skipping a condition would 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 294ca80961..e44423d00e 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 @@ -44,7 +44,7 @@ public Map getConcept(@PathParam("conceptId") @NotB Map children = concept.children(); // TODO Populate matching statistics and their derived date ranges once imported event statistics are available. - ConceptNodeResponse node = new ConceptNodeResponse(concept.label(), concept.description(), null, null, concept.childrenIds().stream().map(ConceptId::toString).toList(), 0L, 0L, null, false, concept.additionalInfos(), excludeFromTimeAggregation(concept), concept.connectors().stream().map(this::toConnectorResponse).toList(), List.of()); + ConceptNodeResponse node = new ConceptNodeResponse(concept.label(), concept.description(), null, null, concept.childrenIds().stream().map(ConceptId::toString).toList(), 0L, 0L, null, false, concept.additionalInfos(), excludeFromTimeAggregation(concept), concept.connectors().stream().map(this::toConnectorResponse).toList(), concept.selects().stream().map(this::toSelectResponse).toList()); nodes.put(parsedConceptId.toString(), node); // Add children @@ -100,6 +100,16 @@ private SelectResponse toSelectResponse(DatasetCatalogRepository.Select select) ); } + private SelectResponse toSelectResponse(DatasetCatalogRepository.ConceptSelect select) { + return new SelectResponse( + select.id().toString(), + select.label(), + select.description(), + select.defaultSelected(), + toSelectResultTypeResponse(select.resultType()) + ); + } + private SelectResultTypeResponse toSelectResultTypeResponse(DatasetCatalogRepository.SelectResultType resultType) { return new SelectResultTypeResponse( resultType.type(), 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 2895897470..e6dd88d44f 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 @@ -126,7 +126,7 @@ public ConceptsResponse getConcepts(@PathParam("datasetId") String datasetId) { excludeFromTimeAggregation(entry), // TODO Remove detailed connector filters/selects from this summary once the frontend loads them via /api/concepts/{conceptId}. entry.connectors().stream().map(this::toTableResponse).toList(), - List.of() + entry.selects().stream().map(this::toSelectResponse).toList() ) )); @@ -227,6 +227,16 @@ private ConceptResource.SelectResponse toSelectResponse(DatasetCatalogRepository ); } + private ConceptResource.SelectResponse toSelectResponse(DatasetCatalogRepository.ConceptSelect select) { + return new ConceptResource.SelectResponse( + select.id().toString(), + select.label(), + select.description(), + select.defaultSelected(), + toSelectResultTypeResponse(select.resultType()) + ); + } + private ConceptResource.SelectResultTypeResponse toSelectResultTypeResponse(DatasetCatalogRepository.SelectResultType resultType) { return new ConceptResource.SelectResultTypeResponse( resultType.type(), diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectConversionContext.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectConversionContext.java new file mode 100644 index 0000000000..5fcaaefe2d --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectConversionContext.java @@ -0,0 +1,33 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept; + +import java.util.Optional; + +import com.bakdata.conquery.quarkus.ids.ConceptId; +import com.bakdata.conquery.quarkus.ids.ConceptSelectId; +import com.bakdata.conquery.quarkus.ids.IdPartSanitizer; + +public record ConceptSelectConversionContext( + ConceptId conceptId, + FallbackReporter fallbackReporter +) { + + public ConceptSelectId selectId(String name) { + return new ConceptSelectId(conceptId, name); + } + + public String idPartFromPreferredOrFallback(String preferred, String fallback, String type) { + if (preferred != null && !preferred.isBlank()) { + return preferred.trim(); + } + String source = Optional.ofNullable(fallback).filter(value -> !value.isBlank()) + .orElseThrow(() -> new IllegalArgumentException("Concept select " + type + " must define name or label.")); + String sanitized = IdPartSanitizer.sanitize(source, "concept select id fallback"); + fallbackReporter.record("concept select id", type, source, sanitized); + return sanitized; + } + + @FunctionalInterface + public interface FallbackReporter { + void record(String idType, Object context, String fallbackValue, String sanitized); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinition.java new file mode 100644 index 0000000000..7a38f3ddf9 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinition.java @@ -0,0 +1,22 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept; + +import com.bakdata.conquery.quarkus.models.PolymorphicModelBase; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true, + defaultImpl = UnknownConceptSelectDefinition.class +) +@Schema(name = "MetadataConceptSelectDefinition", description = "Select definition used at concept level. The concrete model is selected by `type`.", discriminatorProperty = "type") +@PolymorphicModelBase( + schemaName = "MetadataConceptSelectDefinition", + description = "Select definition used at concept level. The concrete model is selected by `type`." +) +public interface ConceptSelectDefinition { + + String getType(); +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionAssembler.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionAssembler.java new file mode 100644 index 0000000000..52c17ccd5e --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionAssembler.java @@ -0,0 +1,59 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.bakdata.conquery.quarkus.ids.ConceptId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; + +@ApplicationScoped +@Slf4j +public class ConceptSelectDefinitionAssembler { + + @Inject + ConceptSelectDefinitionRegistry registry; + + @Inject + ObjectMapper objectMapper; + + public List assemble( + ConceptId conceptId, + List definitions, + ConceptSelectConversionContext.FallbackReporter fallbackReporter, + boolean strictSelectTypes + ) { + if (definitions == null) { + return List.of(); + } + ConceptSelectConversionContext context = new ConceptSelectConversionContext(conceptId, fallbackReporter); + List selects = new ArrayList<>(); + for (ConceptSelectDefinition definition : definitions) { + assemble(context, definition, strictSelectTypes).ifPresent(selects::add); + } + return List.copyOf(selects); + } + + private Optional assemble(ConceptSelectConversionContext context, ConceptSelectDefinition definition, boolean strictSelectTypes) { + Optional> provider = registry.find(definition); + if (provider.isPresent()) { + return Optional.of(convert(context, definition, provider.get())); + } + String type = definition.getType(); + String reason = type == null || type.isBlank() ? "missing concept select type" : "unknown concept select type '" + type + "'"; + String message = "Skipping select for concept '" + context.conceptId() + "' because of " + reason + ": " + objectMapper.valueToTree(definition); + if (strictSelectTypes) { + throw new IllegalStateException(message); + } + log.warn("{}", message); + return Optional.empty(); + } + + private DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, ConceptSelectDefinition definition, ConceptSelectDefinitionProvider provider) { + return provider.convert(context, provider.payloadType().cast(definition)); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionProvider.java new file mode 100644 index 0000000000..05bf3ed7bd --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionProvider.java @@ -0,0 +1,28 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept; + +import com.bakdata.conquery.quarkus.models.PolymorphicModelTypeProvider; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; + +public interface ConceptSelectDefinitionProvider extends PolymorphicModelTypeProvider { + + String type(); + + Class payloadType(); + + DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, T payload); + + @Override + default Class baseType() { + return ConceptSelectDefinition.class; + } + + @Override + default String typeId() { + return type(); + } + + @Override + default Class modelType() { + return payloadType(); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionRegistry.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionRegistry.java new file mode 100644 index 0000000000..dad1d3233d --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionRegistry.java @@ -0,0 +1,35 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +@ApplicationScoped +public class ConceptSelectDefinitionRegistry { + + @Inject + Instance> providers; + + private Map, ConceptSelectDefinitionProvider> providersByModelType = Map.of(); + + @PostConstruct + void init() { + Map, ConceptSelectDefinitionProvider> index = new LinkedHashMap<>(); + for (ConceptSelectDefinitionProvider provider : providers) { + ConceptSelectDefinitionProvider previous = index.put(provider.payloadType(), provider); + if (previous != null) { + throw new IllegalStateException("Duplicate concept select provider for model type " + provider.payloadType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); + } + } + providersByModelType = Map.copyOf(index); + } + + public Optional> find(ConceptSelectDefinition definition) { + return Optional.ofNullable(providersByModelType.get(definition.getClass())); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/UnknownConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/UnknownConceptSelectDefinition.java new file mode 100644 index 0000000000..0c150c47ed --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/UnknownConceptSelectDefinition.java @@ -0,0 +1,32 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; + +public final class UnknownConceptSelectDefinition implements ConceptSelectDefinition { + + private String type; + private final Map properties = new LinkedHashMap<>(); + + @Override + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @JsonAnySetter + public void put(String name, Object value) { + properties.put(name, value); + } + + @JsonAnyGetter + public Map properties() { + return Map.copyOf(properties); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/AbstractConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/AbstractConceptSelectDefinition.java new file mode 100644 index 0000000000..403e495e31 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/AbstractConceptSelectDefinition.java @@ -0,0 +1,28 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; + +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinition; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class AbstractConceptSelectDefinition implements ConceptSelectDefinition { + + @Schema(description = "Concept select implementation discriminator.") + private String type; + + @Schema(description = "Stable local name used to form the select id. Falls back to the label when omitted.", pattern = "^\\w+$") + private String name; + + @Schema(description = "Label displayed to users.") + private String label; + + @Schema(description = "Additional explanation displayed for the select.") + private String description; + + @Schema(description = "Whether the frontend preselects this select.") + private boolean isDefault; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ConceptValuesConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ConceptValuesConceptSelectDefinition.java new file mode 100644 index 0000000000..03da2f540c --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ConceptValuesConceptSelectDefinition.java @@ -0,0 +1,14 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; + +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataConceptValuesConceptSelect", description = "Returns values of matching concept nodes.") +public final class ConceptValuesConceptSelectDefinition extends AbstractConceptSelectDefinition { + + @Schema(description = "Whether concept ids are returned instead of labels.") + private boolean asIds; +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDateUnionConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDateUnionConceptSelectDefinition.java new file mode 100644 index 0000000000..d8dd842c89 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDateUnionConceptSelectDefinition.java @@ -0,0 +1,7 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "MetadataEventDateUnionConceptSelect", description = "Returns the union of matching event dates.") +public final class EventDateUnionConceptSelectDefinition extends AbstractConceptSelectDefinition { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDurationSumConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDurationSumConceptSelectDefinition.java new file mode 100644 index 0000000000..4cedbe32b5 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDurationSumConceptSelectDefinition.java @@ -0,0 +1,7 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "MetadataEventDurationSumConceptSelect", description = "Returns the total duration of matching events.") +public final class EventDurationSumConceptSelectDefinition extends AbstractConceptSelectDefinition { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ExistsConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ExistsConceptSelectDefinition.java new file mode 100644 index 0000000000..99e48d4cac --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ExistsConceptSelectDefinition.java @@ -0,0 +1,7 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "MetadataExistsConceptSelect", description = "Returns whether the concept has any matching event.") +public final class ExistsConceptSelectDefinition extends AbstractConceptSelectDefinition { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/QuarterConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/QuarterConceptSelectDefinition.java new file mode 100644 index 0000000000..f394b4e38a --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/QuarterConceptSelectDefinition.java @@ -0,0 +1,21 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Getter +@Setter +@Schema(name = "MetadataQuarterConceptSelect", description = "Returns the quarter containing a sampled event date.") +public final class QuarterConceptSelectDefinition extends AbstractConceptSelectDefinition { + + @NotNull + private TemporalSampler sample; + + public enum TemporalSampler { + EARLIEST, + LATEST, + RANDOM + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/AbstractConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/AbstractConceptSelectProvider.java new file mode 100644 index 0000000000..302a0709c3 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/AbstractConceptSelectProvider.java @@ -0,0 +1,47 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.providers; + +import java.util.Optional; + +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinitionProvider; +import com.bakdata.conquery.quarkus.concepts.selects.concept.definitions.AbstractConceptSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ConceptSelectId; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; + +abstract class AbstractConceptSelectProvider implements ConceptSelectDefinitionProvider { + + private final Class payloadType; + + protected AbstractConceptSelectProvider(Class payloadType) { + this.payloadType = payloadType; + } + + @Override + public Class payloadType() { + return payloadType; + } + + protected DatasetCatalogRepository.ConceptSelect select(ConceptSelectConversionContext context, T payload, DatasetCatalogRepository.SelectResultType resultType) { + String name = context.idPartFromPreferredOrFallback(payload.getName(), payload.getLabel(), type()); + String label = firstNonBlank(payload.getLabel(), payload.getName()).orElse(name); + ConceptSelectId id = context.selectId(name); + return new DatasetCatalogRepository.ConceptSelect(id, label, payload.getDescription(), payload.isDefault(), type(), resultType); + } + + protected DatasetCatalogRepository.SelectResultType primitive(String type) { + return DatasetCatalogRepository.SelectResultType.primitive(type); + } + + protected DatasetCatalogRepository.SelectResultType list(String elementType) { + return DatasetCatalogRepository.SelectResultType.list(primitive(elementType)); + } + + private Optional firstNonBlank(String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return Optional.of(value.trim()); + } + } + return Optional.empty(); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ConceptValuesConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ConceptValuesConceptSelectProvider.java new file mode 100644 index 0000000000..81a3eea746 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ConceptValuesConceptSelectProvider.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.providers; + +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.concept.definitions.ConceptValuesConceptSelectDefinition; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ConceptValuesConceptSelectProvider extends AbstractConceptSelectProvider { + + public ConceptValuesConceptSelectProvider() { + super(ConceptValuesConceptSelectDefinition.class); + } + + @Override + public String type() { + return "CONCEPT_VALUES"; + } + + @Override + public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, ConceptValuesConceptSelectDefinition payload) { + return select(context, payload, list("STRING")); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDateUnionConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDateUnionConceptSelectProvider.java new file mode 100644 index 0000000000..07e9fe2b1b --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDateUnionConceptSelectProvider.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.providers; + +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.concept.definitions.EventDateUnionConceptSelectDefinition; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class EventDateUnionConceptSelectProvider extends AbstractConceptSelectProvider { + + public EventDateUnionConceptSelectProvider() { + super(EventDateUnionConceptSelectDefinition.class); + } + + @Override + public String type() { + return "EVENT_DATE_UNION"; + } + + @Override + public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, EventDateUnionConceptSelectDefinition payload) { + return select(context, payload, list("DATE_RANGE")); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDurationSumConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDurationSumConceptSelectProvider.java new file mode 100644 index 0000000000..1d8f58c48d --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDurationSumConceptSelectProvider.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.providers; + +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.concept.definitions.EventDurationSumConceptSelectDefinition; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class EventDurationSumConceptSelectProvider extends AbstractConceptSelectProvider { + + public EventDurationSumConceptSelectProvider() { + super(EventDurationSumConceptSelectDefinition.class); + } + + @Override + public String type() { + return "EVENT_DURATION_SUM"; + } + + @Override + public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, EventDurationSumConceptSelectDefinition payload) { + return select(context, payload, primitive("INTEGER")); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ExistsConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ExistsConceptSelectProvider.java new file mode 100644 index 0000000000..dd3e1b4e52 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ExistsConceptSelectProvider.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.providers; + +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.concept.definitions.ExistsConceptSelectDefinition; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ExistsConceptSelectProvider extends AbstractConceptSelectProvider { + + public ExistsConceptSelectProvider() { + super(ExistsConceptSelectDefinition.class); + } + + @Override + public String type() { + return "EXISTS"; + } + + @Override + public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, ExistsConceptSelectDefinition payload) { + return select(context, payload, primitive("BOOLEAN")); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/QuarterConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/QuarterConceptSelectProvider.java new file mode 100644 index 0000000000..a76a592de1 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/QuarterConceptSelectProvider.java @@ -0,0 +1,24 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept.providers; + +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectConversionContext; +import com.bakdata.conquery.quarkus.concepts.selects.concept.definitions.QuarterConceptSelectDefinition; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class QuarterConceptSelectProvider extends AbstractConceptSelectProvider { + + public QuarterConceptSelectProvider() { + super(QuarterConceptSelectDefinition.class); + } + + @Override + public String type() { + return "QUARTER"; + } + + @Override + public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, QuarterConceptSelectDefinition payload) { + return select(context, payload, primitive("STRING")); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConceptSelectId.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConceptSelectId.java new file mode 100644 index 0000000000..f9ecf1996e --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/ids/ConceptSelectId.java @@ -0,0 +1,38 @@ +package com.bakdata.conquery.quarkus.ids; + +import java.util.Arrays; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public record ConceptSelectId(ConceptId conceptId, String name) { + + public ConceptSelectId { + if (conceptId == null) { + throw new IllegalArgumentException("Concept select id concept must not be null."); + } + name = IdPart.requireValid(name, "Concept select id name"); + } + + public DatasetId datasetId() { + return conceptId.datasetId(); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static ConceptSelectId parse(String value) { + String[] parts = IdPart.split(value, "Concept select id", 2); + String selectName = parts[parts.length - 1]; + ConceptId conceptId = new ConceptId(DatasetId.parse(parts[0]), Arrays.stream(parts, 1, parts.length - 1).toList()); + return new ConceptSelectId(conceptId, selectName); + } + + public static ConceptSelectId valueOf(String value) { + return parse(value); + } + + @Override + @JsonValue + public String toString() { + return conceptId + "." + name; + } +} 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 9f5dd86f0f..81d0dd63d5 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 @@ -5,6 +5,7 @@ import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.ids.ConceptId; +import com.bakdata.conquery.quarkus.ids.ConceptSelectId; import com.bakdata.conquery.quarkus.ids.ConnectorId; import com.bakdata.conquery.quarkus.ids.DatasetId; import com.bakdata.conquery.quarkus.ids.FilterId; @@ -83,14 +84,14 @@ record Concept( Map children, // Direct children List childrenIds, - List connectors + List connectors, + List selects ){ public Concept { additionalInfos = additionalInfos == null ? List.of() : List.copyOf(additionalInfos); + selects = selects == null ? List.of() : List.copyOf(selects); } - - // TODO Add concept-level selects (for example EXISTS) once their separate provider and ID hierarchy is migrated. } record AdditionalInfo(String key, String value) { @@ -152,6 +153,16 @@ record Select( } } + record ConceptSelect( + ConceptSelectId id, + String label, + String description, + boolean defaultSelected, + String implementationType, + SelectResultType resultType + ) { + } + record SelectResultType(String type, SelectResultType elementType) { public static SelectResultType primitive(String type) { return new SelectResultType(type, null); 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 index 13edcab848..403f0e5421 100644 --- 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 @@ -14,6 +14,8 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinitionAssembler; import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinitionAssembler; +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinition; +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinitionAssembler; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.ids.ConceptId; import com.bakdata.conquery.quarkus.ids.ConnectorId; @@ -51,6 +53,9 @@ public class DatasetMetadataFolderLoader { @Inject SelectDefinitionAssembler selectDefinitionAssembler; + @Inject + ConceptSelectDefinitionAssembler conceptSelectDefinitionAssembler; + public List loadConfiguredDatasets() { if (!metadataConfig.enabled()) { return List.of(); @@ -251,6 +256,10 @@ private DatasetCatalogRepository.Concept collectConcept(ConceptPayload payload, }).toList(); + List selects = conceptSelectDefinitionAssembler.assemble( + conceptId, payload.selects(), fallbackLogCollector::add, metadataConfig.strictSelectTypes() + ); + log.debug("Loaded concept {} with {} children", conceptId, children.size()); fallbackLogCollector.logSummary(); @@ -262,7 +271,8 @@ private DatasetCatalogRepository.Concept collectConcept(ConceptPayload payload, Boolean.TRUE.equals(payload.defaultExcludeFromTimeAggregation()), Map.copyOf(conceptElementsById), directChildIds, - connectors + connectors, + selects ); } @@ -588,7 +598,8 @@ private record ConceptPayload( List additionalInfos, Boolean defaultExcludeFromTimeAggregation, List<@Valid ConceptElementPayload> children, - @NotNull @Valid List<@Valid ConnectorPayload> connectors + @NotNull @Valid List<@Valid ConnectorPayload> connectors, + List<@Valid ConceptSelectDefinition> selects ) { } 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 59acbd43c6..cf8c210f60 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 @@ -36,6 +36,10 @@ void conceptEndpointReturnsConceptMap() { .body("imdb.tables[0].selects[0].label", equalTo("Title")) .body("imdb.tables[0].selects[0].default", equalTo(true)) .body("imdb.tables[0].selects[0].resultType.type", equalTo("STRING")) + .body("imdb.selects[0].id", equalTo("imdb.exists")) + .body("imdb.selects[0].label", equalTo("Exists")) + .body("imdb.selects[0].default", equalTo(true)) + .body("imdb.selects[0].resultType.type", equalTo("BOOLEAN")) .body("'imdb.movie'.parent", equalTo("imdb")) .body("'imdb.movie'.active", nullValue()) .body("'imdb.movie'.detailsAvailable", nullValue()) 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 2e0075cc75..77f07f7316 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 @@ -71,6 +71,9 @@ void conceptsEndpointRespondsWithFrontendCompatibleShape() { .body("concepts.imdb.tables[0].selects[0].id", equalTo("imdb.titles.Title")) .body("concepts.imdb.tables[0].selects[0].default", equalTo(true)) .body("concepts.imdb.tables[0].selects[0].resultType.type", equalTo("STRING")) + .body("concepts.imdb.selects[0].id", equalTo("imdb.exists")) + .body("concepts.imdb.selects[0].default", equalTo(true)) + .body("concepts.imdb.selects[0].resultType.type", equalTo("BOOLEAN")) .body("concepts.'imdb.catalog'.label", equalTo("Catalog")) .body("concepts.'imdb.catalog'.active", equalTo(false)) .body("concepts.'imdb.catalog'.detailsAvailable", equalTo(false)) diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionDeserializationTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionDeserializationTest.java new file mode 100644 index 0000000000..bdf87e6c6b --- /dev/null +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionDeserializationTest.java @@ -0,0 +1,69 @@ +package com.bakdata.conquery.quarkus.concepts.selects.concept; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import java.util.List; + +import com.bakdata.conquery.quarkus.concepts.selects.concept.definitions.QuarterConceptSelectDefinition; +import com.bakdata.conquery.quarkus.ids.ConceptSelectId; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +class ConceptSelectDefinitionDeserializationTest { + + @Inject + ObjectMapper objectMapper; + + @ParameterizedTest + @ValueSource(strings = {"EXISTS", "QUARTER", "EVENT_DATE_UNION", "EVENT_DURATION_SUM", "CONCEPT_VALUES"}) + void deserializesEveryRegisteredType(String type) throws Exception { + String sample = type.equals("QUARTER") ? ",\"sample\":\"EARLIEST\"" : ""; + ConceptSelectDefinition definition = objectMapper.readValue("{\"type\":\"" + type + "\"" + sample + "}", ConceptSelectDefinition.class); + + assertFalse(definition instanceof UnknownConceptSelectDefinition, type); + assertEquals(type, definition.getType()); + } + + @Test + void acceptsScalarWhereConceptSelectListIsExpected() throws Exception { + ConceptSelectList payload = objectMapper.readValue(""" + {"selects":{"type":"QUARTER","sample":"LATEST"}} + """, ConceptSelectList.class); + + QuarterConceptSelectDefinition quarter = assertInstanceOf(QuarterConceptSelectDefinition.class, payload.selects().getFirst()); + assertEquals(QuarterConceptSelectDefinition.TemporalSampler.LATEST, quarter.getSample()); + } + + @Test + void keepsUnknownTypeAsExplicitFallbackModel() throws Exception { + ConceptSelectDefinition definition = objectMapper.readValue(""" + {"type":"EXTENSION_CONCEPT_SELECT","option":true} + """, ConceptSelectDefinition.class); + + UnknownConceptSelectDefinition unknown = assertInstanceOf(UnknownConceptSelectDefinition.class, definition); + assertEquals(true, unknown.properties().get("option")); + } + + @Test + void serializesTypedConceptSelectIdAsString() throws Exception { + ConceptSelectId nested = ConceptSelectId.parse("demo.concept.select"); + ConceptSelectId root = ConceptSelectId.parse("demo.select"); + + assertEquals("demo.concept.select", nested.toString()); + assertEquals("demo.concept", nested.conceptId().toString()); + assertEquals("demo.select", root.toString()); + assertEquals("demo", root.conceptId().toString()); + assertEquals("\"demo.concept.select\"", objectMapper.writeValueAsString(nested)); + assertEquals(nested, objectMapper.readValue("\"demo.concept.select\"", ConceptSelectId.class)); + } + + private record ConceptSelectList(List selects) { + } +} diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java index 325379b3d7..1c67945136 100644 --- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java @@ -48,6 +48,15 @@ void exposesRegisteredFilterModelsAsDiscriminatedSchema() throws Exception { assertNotNull(findProperty(countSelectSchema, "column")); assertNotNull(findProperty(countSelectSchema, "distinctByColumn")); + JsonNode conceptSelectSchema = schemas.path("MetadataConceptSelectDefinition"); + assertEquals("type", conceptSelectSchema.path("discriminator").path("propertyName").asText()); + assertEquals("#/components/schemas/MetadataExistsConceptSelect", conceptSelectSchema.path("discriminator").path("mapping").path("EXISTS").asText()); + assertEquals(5, conceptSelectSchema.path("oneOf").size()); + + JsonNode quarterConceptSelectSchema = schemas.path("MetadataQuarterConceptSelect"); + assertEquals("QUARTER", findProperty(quarterConceptSelectSchema, "type").path("const").asText()); + assertNotNull(findProperty(quarterConceptSelectSchema, "sample")); + JsonNode conditionSchema = schemas.path("MetadataConceptCondition"); assertEquals("type", conditionSchema.path("discriminator").path("propertyName").asText()); assertEquals("#/components/schemas/MetadataEqualConceptCondition", conditionSchema.path("discriminator").path("mapping").path("EQUAL").asText()); diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java index 6301fdd482..c8e78180ac 100644 --- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/services/DatasetServiceTest.java @@ -30,8 +30,8 @@ void resolvesConceptCodesAgainstRequestedConceptSubtree() { cid("demo.icd.a00"), new DatasetCatalogRepository.ConceptElement(cid("demo.icd.a00"), "A00", null, List.of(), cid("demo.icd"), List.of(cid("demo.icd.a00.a00_0")), equal("A00", "A000")), cid("demo.icd.a00.a00_0"), new DatasetCatalogRepository.ConceptElement(cid("demo.icd.a00.a00_0"), "A00.0", null, List.of(), cid("demo.icd.a00"), List.of(), equal("A000")) - ), List.of(cid("demo.icd.a00")), null), - new DatasetCatalogRepository.Concept(cid("demo.other"), "Other", null, List.of(), false, null, null, List.of()) + ), List.of(cid("demo.icd.a00")), null, List.of()), + new DatasetCatalogRepository.Concept(cid("demo.other"), "Other", null, List.of(), false, null, null, List.of(), List.of()) )); DatasetService.ConceptCodeResolution resolution = service.resolveConceptCodes("demo.icd", List.of("A00", "A000", "OTHER", "UNKNOWN")); 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 index 4bb49d9bca..3f1788cff8 100644 --- 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 @@ -18,8 +18,10 @@ import com.bakdata.conquery.quarkus.concepts.conditions.definitions.EqualConceptCondition; import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinitionAssembler; import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinitionAssembler; +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinitionAssembler; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.ids.ConceptId; +import com.bakdata.conquery.quarkus.ids.ConceptSelectId; import com.bakdata.conquery.quarkus.ids.DatasetId; import com.bakdata.conquery.quarkus.ids.FilterId; import com.bakdata.conquery.quarkus.ids.SelectId; @@ -46,6 +48,9 @@ class DatasetMetadataFolderLoaderTest { @Inject SelectDefinitionAssembler selectDefinitionAssembler; + @Inject + ConceptSelectDefinitionAssembler conceptSelectDefinitionAssembler; + @Inject ObjectMapper objectMapper; @@ -143,6 +148,7 @@ void loadsConfiguredMetadataFolderAndScopesIds(@TempDir Path tempDir) throws Exc loader.validator = validator; loader.filterDefinitionAssembler = filterDefinitionAssembler; loader.selectDefinitionAssembler = selectDefinitionAssembler; + loader.conceptSelectDefinitionAssembler = conceptSelectDefinitionAssembler; loader.metadataConfig = new DatasetMetadataRuntimeConfig() { @Override public boolean enabled() { @@ -232,6 +238,7 @@ void loadsDevConfigMetadataFixtures() throws Exception { loader.validator = validator; loader.filterDefinitionAssembler = filterDefinitionAssembler; loader.selectDefinitionAssembler = selectDefinitionAssembler; + loader.conceptSelectDefinitionAssembler = conceptSelectDefinitionAssembler; loader.metadataConfig = new DatasetMetadataRuntimeConfig() { @Override public boolean enabled() { @@ -278,6 +285,10 @@ public boolean strictSelectTypes() { assertEquals("FIRST", titleSelect.implementationType()); assertEquals(DatasetCatalogRepository.SelectResultType.primitive("STRING"), titleSelect.resultType()); assertEquals(List.of(ColumnId.parse("imdb.title.name")), titleSelect.requiredColumns()); + DatasetCatalogRepository.ConceptSelect conceptSelect = imdb.conceptsById().get(ConceptId.parse("imdb")).selects().getFirst(); + assertEquals(ConceptSelectId.parse("imdb.exists"), conceptSelect.id()); + assertEquals("EXISTS", conceptSelect.implementationType()); + assertEquals(DatasetCatalogRepository.SelectResultType.primitive("BOOLEAN"), conceptSelect.resultType()); DatasetCatalogRepository.Connector titleConnector = imdb.conceptsById().get(ConceptId.parse("imdb")).connectors().getFirst(); assertEquals("Choose the release date", titleConnector.validityDatesDescription()); DatasetCatalogRepository.ValidityDate releaseDate = titleConnector.validityDates().getFirst(); @@ -319,6 +330,7 @@ void fallsBackToFolderNameWhenDatasetJsonIsMissing(@TempDir Path tempDir) throws loader.validator = validator; loader.filterDefinitionAssembler = filterDefinitionAssembler; loader.selectDefinitionAssembler = selectDefinitionAssembler; + loader.conceptSelectDefinitionAssembler = conceptSelectDefinitionAssembler; loader.metadataConfig = new DatasetMetadataRuntimeConfig() { @Override public boolean enabled() { @@ -407,6 +419,27 @@ void skipsUnknownSelectInLenientModeAndRejectsItInStrictMode(@TempDir Path tempD assertTrue(error.getMessage().contains("unknown select type 'EXTERNAL_SELECT'")); } + @Test + void skipsUnknownConceptSelectInLenientModeAndRejectsItInStrictMode(@TempDir Path tempDir) throws Exception { + Path dataset = tempDir.resolve("demo"); + Files.createDirectories(dataset.resolve("conceptTrees")); + Files.createDirectories(dataset.resolve("tables")); + Files.writeString(dataset.resolve("conceptTrees/events.concept.json"), """ + { + "name":"events", + "children":[], + "connectors":[], + "selects":{"type":"EXTERNAL_CONCEPT_SELECT","name":"external"} + } + """); + + DatasetCatalogRepository.Concept concept = loader(tempDir, false).loadConfiguredDatasets().getFirst().conceptsById().get(ConceptId.parse("demo.events")); + assertTrue(concept.selects().isEmpty()); + + IllegalStateException error = assertThrows(IllegalStateException.class, () -> loader(tempDir, true).loadConfiguredDatasets()); + assertTrue(error.getMessage().contains("unknown concept select type 'EXTERNAL_CONCEPT_SELECT'")); + } + @Test void rejectsStructureNodeWithUnknownConceptAtStartup(@TempDir Path tempDir) throws Exception { Path dataset = tempDir.resolve("demo"); @@ -505,6 +538,7 @@ private DatasetMetadataFolderLoader loader(Path root, boolean strictFilterTypes) loader.validator = validator; loader.filterDefinitionAssembler = filterDefinitionAssembler; loader.selectDefinitionAssembler = selectDefinitionAssembler; + loader.conceptSelectDefinitionAssembler = conceptSelectDefinitionAssembler; loader.metadataConfig = new DatasetMetadataRuntimeConfig() { @Override public boolean enabled() { diff --git a/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json b/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json index e660246516..0eca43e71b 100644 --- a/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json +++ b/backend-quarkus/src/test/resources/test-meta-data/imdb/conceptTrees/imdb.concept.json @@ -7,6 +7,13 @@ "value": "IMDb test metadata" } ], + "selects": { + "type": "EXISTS", + "name": "exists", + "label": "Exists", + "description": "Whether any title exists", + "default": true + }, "children": [ { "name": "movie", From ccb468d2364e0d32bee42272d5c1890be5f2a170 Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:47:32 +0200 Subject: [PATCH 42/45] polymorph filtervalues --- backend-quarkus/README.md | 8 + .../quarkus/api/DatasetsResource.java | 180 +----------------- .../quarkus/api/QuerySubmissionPayload.java | 21 +- .../filters/FilterDefinitionAssembler.java | 18 +- .../filters/FilterDefinitionProvider.java | 5 + .../filters/FilterDefinitionRegistry.java | 8 + .../providers/AbstractFilterProvider.java | 43 ++++- .../BigMultiSelectFilterProvider.java | 5 +- .../CategoryMaxSumFilterProvider.java | 7 +- .../providers/CountFilterProvider.java | 5 +- .../CountQuartersFilterProvider.java | 5 +- .../providers/DateDistanceFilterProvider.java | 5 +- .../providers/DurationSumFilterProvider.java | 5 +- .../providers/FlagsFilterProvider.java | 5 +- .../providers/NumberFilterProvider.java | 7 +- .../providers/PrefixTextFilterProvider.java | 5 +- .../QuartersInYearFilterProvider.java | 5 +- .../providers/SelectFilterProvider.java | 12 +- .../providers/SingleSelectFilterProvider.java | 6 +- .../filters/providers/SumFilterProvider.java | 7 +- .../concepts/filters/values/FilterValue.java | 17 ++ .../filters/values/FilterValueProvider.java | 25 +++ .../filters/values/FilterValueRegistry.java | 42 ++++ .../BigMultiSelectFilterValue.java | 13 ++ .../definitions/IntegerFilterRange.java | 7 + .../definitions/IntegerFilterValue.java | 10 + .../definitions/IntegerRangeFilterValue.java | 11 ++ .../definitions/MoneyRangeFilterValue.java | 11 ++ .../definitions/MultiSelectFilterValue.java | 13 ++ .../values/definitions/RealFilterRange.java | 9 + .../values/definitions/RealFilterValue.java | 12 ++ .../definitions/RealRangeFilterValue.java | 11 ++ .../values/definitions/SelectFilterValue.java | 10 + .../values/definitions/StringFilterValue.java | 10 + .../AbstractFilterValueProvider.java | 18 ++ .../BigMultiSelectFilterValueProvider.java | 16 ++ .../providers/IntegerFilterValueProvider.java | 16 ++ .../IntegerRangeFilterValueProvider.java | 16 ++ .../MoneyRangeFilterValueProvider.java | 16 ++ .../MultiSelectFilterValueProvider.java | 16 ++ .../providers/RealFilterValueProvider.java | 16 ++ .../RealRangeFilterValueProvider.java | 16 ++ .../providers/SelectFilterValueProvider.java | 17 ++ .../providers/StringFilterValueProvider.java | 16 ++ .../quarkus/services/EntityQueryService.java | 60 +----- .../quarkus/api/DatasetsResourceTest.java | 31 +++ .../quarkus/api/QueryResourceTest.java | 54 ++++++ ...ilterDefinitionProviderValueTypesTest.java | 60 ++++++ .../FilterValueDeserializationTest.java | 87 +++++++++ .../models/PolymorphicModelsOpenApiTest.java | 11 ++ 50 files changed, 760 insertions(+), 269 deletions(-) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueRegistry.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/BigMultiSelectFilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterRange.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerRangeFilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MoneyRangeFilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MultiSelectFilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterRange.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealRangeFilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/SelectFilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/StringFilterValue.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/AbstractFilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/BigMultiSelectFilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerFilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerRangeFilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MoneyRangeFilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MultiSelectFilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealFilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealRangeFilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/SelectFilterValueProvider.java create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/StringFilterValueProvider.java create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProviderValueTypesTest.java create mode 100644 backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueDeserializationTest.java diff --git a/backend-quarkus/README.md b/backend-quarkus/README.md index ae2d85ff44..d5e89b5577 100644 --- a/backend-quarkus/README.md +++ b/backend-quarkus/README.md @@ -63,6 +63,7 @@ Filter, select, and concept-condition implementations are extensible from anothe 1. A concrete class implementing `FilterDefinition`, annotated with OpenAPI `@Schema`. 2. A CDI bean implementing `FilterDefinitionProvider` for its type id, model class, and business-model conversion. +3. The filter value model types accepted by that provider. Each model type needs a registered `FilterValueProvider`. Connector selects use the equivalent `SelectDefinition` and `SelectDefinitionProvider` extension points. Concept-level selects form a separate family with `ConceptSelectDefinition` and `ConceptSelectDefinitionProvider`. Unknown types @@ -74,6 +75,13 @@ change the meaning of a concept node, unknown condition types always fail metada types are `EQUAL`, `PREFIX_LIST`, `PREFIX_RANGE`, `COLUMN_EQUAL`, `PRESENT`, `AND`, `OR`, and `NOT`. The old `GROOVY` condition is intentionally not registered because script execution requires a separate security and runtime decision. +Query filter values use the separate `FilterValue` and `FilterValueProvider` family. Their discriminator ids may +overlap metadata filter definitions (for example `SELECT`) because polymorphic registrations are scoped by base type. +Filter providers declare their accepted `FilterValue` model classes through `acceptedValueTypes()`. Startup verifies +that each declared model has a registered value provider, and metadata conversion verifies the emitted value type. +Entity-query execution is intentionally not implemented yet. The filter value and entity-history API models are kept, +but the entity-history and entity-resolution endpoints return `501 Not Implemented` until their business semantics are migrated. + The generic `PolymorphicModelRegistry` validates duplicate type ids and base/model compatibility at startup. Its registrations are also installed into Jackson and projected into OpenAPI. Extension JARs must be discoverable by Quarkus bean and Jandex indexing so their provider and annotated model class are available during application assembly. 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 e6dd88d44f..560affa4a6 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.bakdata.conquery.quarkus.config.EntityPreviewRuntimeConfig; import com.bakdata.conquery.quarkus.config.FormQueriesRuntimeConfig; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.ConceptId; import com.bakdata.conquery.quarkus.ids.StructureNodeId; import com.bakdata.conquery.quarkus.services.DatasetService; @@ -20,8 +21,6 @@ import com.bakdata.conquery.quarkus.services.QueryStateService; import com.bakdata.conquery.quarkus.services.QueryUploadService; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.security.identity.SecurityIdentity; import jakarta.enterprise.inject.Instance; @@ -37,7 +36,6 @@ 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; @Path("/api/datasets") @Produces(MediaType.APPLICATION_JSON) @@ -301,7 +299,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, QuerySubmissionPayload payload) { + public StartQueryResponse postQueries(@PathParam("datasetId") String datasetId, @Valid @NotNull QuerySubmissionPayload payload) { datasetService.requireDataset(datasetId); return queryStateService.createQuery(datasetId, payload, SecurityIdentityUtil.resolveUserName(identity)); } @@ -327,7 +325,7 @@ public UploadQueryResponse uploadQueries( @Path("/{datasetId}/queries/entity") @Operation( summary = "Get entity history", - description = "Returns history data for a single entity." + description = "Not implemented yet in the Quarkus backend." ) public EntityQueryService.EntityHistoryResponse getEntityHistory( @PathParam("datasetId") String datasetId, @@ -348,41 +346,14 @@ public EntityQueryService.EntityHistoryResponse getEntityHistory( @Path("/{datasetId}/queries/resolve-entities") @Operation( summary = "Resolve entities from filter values", - description = "Resolves entity ids from selected filter values." + description = "Not implemented yet in the Quarkus backend." ) public List> resolveEntities( @PathParam("datasetId") String datasetId, - @Valid @NotNull List<@Valid FilterValueRequest> payload + @Valid @NotNull List<@Valid FilterValue> payload ) { datasetService.requireDataset(datasetId); - return entityQueryService.resolveEntities( - payload.stream() - .map(this::toFilterValuesRequest) - .toList() - ); - } - - private EntityQueryService.FilterValuesRequest toFilterValuesRequest(FilterValueRequest request) { - return new EntityQueryService.FilterValuesRequest( - request.filter, - request.type, - extractFilterValues(request) - ); - } - - private List extractFilterValues(FilterValueRequest request) { - return switch (request) { - case MultiSelectFilterValueRequest multiSelect -> multiSelect.value; - case BigMultiSelectFilterValueRequest bigMultiSelect -> bigMultiSelect.value; - case SelectFilterValueRequest select -> List.of(select.value); - case StringFilterValueRequest string -> List.of(string.value); - case IntegerFilterValueRequest integer -> List.of(String.valueOf(integer.value)); - case IntegerRangeFilterValueRequest integerRange -> List.of(String.valueOf(integerRange.value)); - case MoneyRangeFilterValueRequest moneyRange -> List.of(String.valueOf(moneyRange.value)); - case RealFilterValueRequest real -> List.of(String.valueOf(real.value)); - case RealRangeFilterValueRequest realRange -> List.of(String.valueOf(realRange.value)); - default -> throw new IllegalArgumentException("Unsupported filter value type: " + request.getClass().getName()); - }; + return entityQueryService.resolveEntities(payload); } private Object loadFormResource(String path) { @@ -454,145 +425,6 @@ public EntityHistoryRequest(String idKind, String entityId, QuerySubmissionPaylo } } - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) - @JsonSubTypes({ - @JsonSubTypes.Type(value = MultiSelectFilterValueRequest.class, name = "MULTI_SELECT"), - @JsonSubTypes.Type(value = BigMultiSelectFilterValueRequest.class, name = "BIG_MULTI_SELECT"), - @JsonSubTypes.Type(value = SelectFilterValueRequest.class, name = "SELECT"), - @JsonSubTypes.Type(value = StringFilterValueRequest.class, name = "STRING"), - @JsonSubTypes.Type(value = IntegerFilterValueRequest.class, name = "INTEGER"), - @JsonSubTypes.Type(value = IntegerRangeFilterValueRequest.class, name = "INTEGER_RANGE"), - @JsonSubTypes.Type(value = MoneyRangeFilterValueRequest.class, name = "MONEY_RANGE"), - @JsonSubTypes.Type(value = RealFilterValueRequest.class, name = "REAL"), - @JsonSubTypes.Type(value = RealRangeFilterValueRequest.class, name = "REAL_RANGE") - }) - @org.eclipse.microprofile.openapi.annotations.media.Schema( - description = "Filter value payload discriminated by `type`.", - discriminatorProperty = "type", - oneOf = { - MultiSelectFilterValueRequest.class, - BigMultiSelectFilterValueRequest.class, - SelectFilterValueRequest.class, - StringFilterValueRequest.class, - IntegerFilterValueRequest.class, - IntegerRangeFilterValueRequest.class, - MoneyRangeFilterValueRequest.class, - RealFilterValueRequest.class, - RealRangeFilterValueRequest.class - }, - discriminatorMapping = { - @DiscriminatorMapping(value = "MULTI_SELECT", schema = MultiSelectFilterValueRequest.class), - @DiscriminatorMapping(value = "BIG_MULTI_SELECT", schema = BigMultiSelectFilterValueRequest.class), - @DiscriminatorMapping(value = "SELECT", schema = SelectFilterValueRequest.class), - @DiscriminatorMapping(value = "STRING", schema = StringFilterValueRequest.class), - @DiscriminatorMapping(value = "INTEGER", schema = IntegerFilterValueRequest.class), - @DiscriminatorMapping(value = "INTEGER_RANGE", schema = IntegerRangeFilterValueRequest.class), - @DiscriminatorMapping(value = "MONEY_RANGE", schema = MoneyRangeFilterValueRequest.class), - @DiscriminatorMapping(value = "REAL", schema = RealFilterValueRequest.class), - @DiscriminatorMapping(value = "REAL_RANGE", schema = RealRangeFilterValueRequest.class) - } - ) - public abstract static class FilterValueRequest { - public final @NotBlank String filter; - public final @NotBlank String type; - - protected FilterValueRequest(String filter, String type) { - this.filter = filter; - this.type = type; - } - } - - public static final class MultiSelectFilterValueRequest extends FilterValueRequest { - public final @NotNull @NotEmpty List<@NotBlank String> value; - - public MultiSelectFilterValueRequest(String filter, List value) { - super(filter, "MULTI_SELECT"); - this.value = value; - } - } - - public static final class BigMultiSelectFilterValueRequest extends FilterValueRequest { - public final @NotNull @NotEmpty List<@NotBlank String> value; - - public BigMultiSelectFilterValueRequest(String filter, List value) { - super(filter, "BIG_MULTI_SELECT"); - this.value = value; - } - } - - public static final class SelectFilterValueRequest extends FilterValueRequest { - public final @NotBlank String value; - - public SelectFilterValueRequest(String filter, String value) { - super(filter, "SELECT"); - this.value = value; - } - } - - public static final class StringFilterValueRequest extends FilterValueRequest { - public final @NotBlank String value; - - public StringFilterValueRequest(String filter, String value) { - super(filter, "STRING"); - this.value = value; - } - } - - public static final class IntegerFilterValueRequest extends FilterValueRequest { - public final @NotNull Long value; - - public IntegerFilterValueRequest(String filter, Long value) { - super(filter, "INTEGER"); - this.value = value; - } - } - - public static final class IntegerRangeFilterValueRequest extends FilterValueRequest { - public final @NotNull NumericRangeValue value; - - public IntegerRangeFilterValueRequest(String filter, NumericRangeValue value) { - super(filter, "INTEGER_RANGE"); - this.value = value; - } - } - - public static final class MoneyRangeFilterValueRequest extends FilterValueRequest { - public final @NotNull NumericRangeValue value; - - public MoneyRangeFilterValueRequest(String filter, NumericRangeValue value) { - super(filter, "MONEY_RANGE"); - this.value = value; - } - } - - public static final class RealFilterValueRequest extends FilterValueRequest { - public final @NotNull Double value; - - public RealFilterValueRequest(String filter, Double value) { - super(filter, "REAL"); - this.value = value; - } - } - - public static final class RealRangeFilterValueRequest extends FilterValueRequest { - public final @NotNull DecimalRangeValue value; - - public RealRangeFilterValueRequest(String filter, DecimalRangeValue value) { - super(filter, "REAL_RANGE"); - this.value = value; - } - } - - public static final class NumericRangeValue { - public Long min; - public Long max; - } - - public static final class DecimalRangeValue { - public Double min; - public Double max; - } - public record DatasetResponse( String id, String label 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 index b216b5ed3b..bedb995c53 100644 --- 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 @@ -4,8 +4,10 @@ import java.util.List; import java.util.Map; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.validation.Valid; import org.eclipse.microprofile.openapi.annotations.media.DiscriminatorMapping; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -40,6 +42,7 @@ public abstract class QuerySubmissionPayload { public static final class ConceptQuerySubmissionPayload extends QuerySubmissionPayload { @Schema(description = "Query root node. Polymorphic and discriminated by field `type`.") + @Valid public QueryNode root; public ConceptQuerySubmissionPayload() { @@ -50,6 +53,7 @@ public ConceptQuerySubmissionPayload() { public static final class SecondaryIdQuerySubmissionPayload extends QuerySubmissionPayload { public String secondaryId; @Schema(description = "Query root node. Polymorphic and discriminated by field `type`.") + @Valid public QueryNode root; public SecondaryIdQuerySubmissionPayload() { @@ -104,22 +108,25 @@ public abstract static class QueryNode { @Schema(name = "AndQueryNode") public static class AndNode extends QueryNode { - public List children; + public List<@Valid QueryNode> children; } @Schema(name = "OrQueryNode") public static class OrNode extends QueryNode { - public List children; + public List<@Valid QueryNode> children; } @Schema(name = "NegationQueryNode") public static class NegationNode extends QueryNode { + @Valid public QueryNode child; } @Schema(name = "DateRestrictionQueryNode") public static class DateRestrictionNode extends QueryNode { + @Valid public DateRangePayload dateRange; + @Valid public QueryNode child; } @@ -129,7 +136,7 @@ public static class ConceptNode extends QueryNode { public String label; public Boolean excludeFromTimeAggregation; public Boolean excludeFromSecondaryId; - public List tables; + public List<@Valid TableConfigPayload> tables; public List selects; } @@ -232,18 +239,14 @@ public static class DateRangePayload { public static class TableConfigPayload { public String id; + @Valid public DateColumnConfigPayload dateColumn; public List selects; - public List filters; + public List<@Valid FilterValue> filters; } public static class DateColumnConfigPayload { public String value; } - public static class FilterConfigPayload { - public String filter; - public String type; - public Object value; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionAssembler.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionAssembler.java index 971745ba75..c383fd0448 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionAssembler.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionAssembler.java @@ -3,7 +3,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValueProvider; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValueRegistry; import com.bakdata.conquery.quarkus.ids.ConnectorId; import com.bakdata.conquery.quarkus.ids.TableId; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; @@ -22,6 +26,9 @@ public class FilterDefinitionAssembler { @Inject ObjectMapper objectMapper; + @Inject + FilterValueRegistry filterValueRegistry; + public List assemble( ConnectorId connectorId, TableId tableId, @@ -60,6 +67,15 @@ private Optional assemble(FilterConversionConte private DatasetCatalogRepository.Filter convert(FilterConversionContext context, FilterDefinition definition, FilterDefinitionProvider provider) { T payload = provider.payloadType().cast(definition); - return provider.convert(context, payload); + DatasetCatalogRepository.Filter filter = provider.convert(context, payload); + Set acceptedTypes = provider.acceptedValueTypes().stream() + .map(filterValueRegistry::require) + .map(FilterValueProvider::type) + .collect(Collectors.toUnmodifiableSet()); + if (!acceptedTypes.contains(filter.type())) { + throw new IllegalStateException("Filter provider " + provider.getClass().getName() + + " produced filter value type '" + filter.type() + "' but accepts " + acceptedTypes); + } + return filter; } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProvider.java index f05df0de27..881a1da51e 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProvider.java @@ -1,5 +1,8 @@ package com.bakdata.conquery.quarkus.concepts.filters; +import java.util.Set; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import com.bakdata.conquery.quarkus.models.PolymorphicModelTypeProvider; @@ -9,6 +12,8 @@ public interface FilterDefinitionProvider extends Po Class payloadType(); + Set> acceptedValueTypes(); + DatasetCatalogRepository.Filter convert(FilterConversionContext context, T payload); @Override diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionRegistry.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionRegistry.java index a70be1c114..d5bc43f590 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionRegistry.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionRegistry.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Optional; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValueRegistry; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; @@ -15,12 +16,19 @@ public class FilterDefinitionRegistry { @Inject Instance> providers; + @Inject + FilterValueRegistry filterValueRegistry; + private Map, FilterDefinitionProvider> providersByModelType = Map.of(); @PostConstruct void init() { Map, FilterDefinitionProvider> index = new LinkedHashMap<>(); for (FilterDefinitionProvider provider : providers) { + if (provider.acceptedValueTypes().isEmpty()) { + throw new IllegalStateException("Filter provider " + provider.getClass().getName() + " does not declare any accepted filter value types"); + } + provider.acceptedValueTypes().forEach(filterValueRegistry::require); FilterDefinitionProvider previous = index.put(provider.payloadType(), provider); if (previous != null) { throw new IllegalStateException("Duplicate filter provider for model type " + provider.payloadType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java index f0009993b8..644905b2ff 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java @@ -5,6 +5,11 @@ import com.bakdata.conquery.quarkus.concepts.filters.definitions.AbstractFilterDefinition; import com.bakdata.conquery.quarkus.concepts.filters.definitions.SelectFilterDefinition; import com.bakdata.conquery.quarkus.concepts.filters.definitions.SingleColumnFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValueRegistry; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MoneyRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.RealRangeFilterValue; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.ids.FilterId; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; @@ -12,13 +17,22 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; + +import jakarta.inject.Inject; abstract class AbstractFilterProvider implements FilterDefinitionProvider { private final Class payloadType; + private final Set> acceptedValueTypes; + + @Inject + FilterValueRegistry filterValueRegistry; - protected AbstractFilterProvider(Class payloadType) { + @SafeVarargs + protected AbstractFilterProvider(Class payloadType, Class... acceptedValueTypes) { this.payloadType = payloadType; + this.acceptedValueTypes = Set.of(acceptedValueTypes); } @Override @@ -26,10 +40,15 @@ public Class payloadType() { return payloadType; } + @Override + public Set> acceptedValueTypes() { + return acceptedValueTypes; + } + protected DatasetCatalogRepository.Filter filter( FilterConversionContext context, T payload, - String frontendType, + Class valueType, Integer min, Integer max, boolean creatable, @@ -38,6 +57,7 @@ protected DatasetCatalogRepository.Filter filter( String name = context.idPartFromPreferredOrFallback(payload.getName(), payload.getLabel(), "filter id", type()); String label = firstNonBlank(payload.getLabel(), payload.getName()).orElse(name); FilterId id = context.filterId(name); + String frontendType = frontendType(valueType); return new DatasetCatalogRepository.Filter( id, label, @@ -55,6 +75,14 @@ protected DatasetCatalogRepository.Filter filter( ); } + protected String frontendType(Class valueType) { + if (!acceptedValueTypes.contains(valueType)) { + throw new IllegalArgumentException("Filter provider " + getClass().getName() + + " does not accept filter value type " + valueType.getName()); + } + return filterValueRegistry.require(valueType).type(); + } + protected ColumnId requiredColumn(FilterConversionContext context, SingleColumnFilterDefinition payload) { String column = firstNonBlank(payload.getColumn()) .orElseThrow(() -> new IllegalArgumentException("Filter " + type() + " must define column.")); @@ -105,12 +133,13 @@ protected List flagColumns(FilterConversionContext context, Map numericRangeValueType(FilterConversionContext context, ColumnId column) { return switch (context.columnType(column)) { - case MONEY -> "MONEY_RANGE"; - case INTEGER -> "INTEGER_RANGE"; - case DECIMAL, REAL -> "REAL_RANGE"; - default -> "REAL_RANGE"; + case MONEY -> MoneyRangeFilterValue.class; + case INTEGER -> IntegerRangeFilterValue.class; + case DECIMAL, REAL -> RealRangeFilterValue.class; + // TODO may fail here + default -> RealRangeFilterValue.class; }; } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java index bf95bd6d21..a1a89f65dd 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java @@ -4,13 +4,14 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.BigMultiSelectFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.BigMultiSelectFilterValue; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class BigMultiSelectFilterProvider extends AbstractFilterProvider { public BigMultiSelectFilterProvider() { - super(BigMultiSelectFilterDefinition.class); + super(BigMultiSelectFilterDefinition.class, BigMultiSelectFilterValue.class); } @Override public String type() { @@ -19,6 +20,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, BigMultiSelectFilterDefinition payload) { - return filter(context, payload, "BIG_MULTI_SELECT", null, null, true, List.of(requiredColumn(context, payload))); + return filter(context, payload, BigMultiSelectFilterValue.class, null, null, true, List.of(requiredColumn(context, payload))); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CategoryMaxSumFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CategoryMaxSumFilterProvider.java index f34a28dbcc..40e87fe683 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CategoryMaxSumFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CategoryMaxSumFilterProvider.java @@ -5,6 +5,9 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.CategoryMaxSumFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MoneyRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.RealRangeFilterValue; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @@ -13,7 +16,7 @@ public class CategoryMaxSumFilterProvider extends AbstractFilterProvider { public CategoryMaxSumFilterProvider() { - super(CategoryMaxSumFilterDefinition.class); + super(CategoryMaxSumFilterDefinition.class, IntegerRangeFilterValue.class, MoneyRangeFilterValue.class, RealRangeFilterValue.class); } @Override @@ -27,7 +30,7 @@ public DatasetCatalogRepository.Filter convert(FilterConversionContext context, ColumnId valueColumnId = context.columnId(payload.getValueColumn()); required.add(valueColumnId); required.add(context.columnId(payload.getCategoryColumn())); - return filter(context, payload, numericFrontendType(context, valueColumnId), null, null, false, List.copyOf(required)); + return filter(context, payload, numericRangeValueType(context, valueColumnId), null, null, false, List.copyOf(required)); } private void addOptional(FilterConversionContext context, List columns, String value) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java index af58fcceb5..7a90be0206 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java @@ -2,6 +2,7 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.CountFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @@ -9,7 +10,7 @@ @ApplicationScoped public class CountFilterProvider extends AbstractFilterProvider { public CountFilterProvider() { - super(CountFilterDefinition.class); + super(CountFilterDefinition.class, IntegerRangeFilterValue.class); } @Override public String type() { @@ -19,6 +20,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, CountFilterDefinition payload) { ColumnId column = requiredColumn(context, payload); - return filter(context, payload, "INTEGER_RANGE", 1, null, false, columns(column, optionalColumns(context, payload.getDistinctByColumn()))); + return filter(context, payload, IntegerRangeFilterValue.class, 1, null, false, columns(column, optionalColumns(context, payload.getDistinctByColumn()))); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountQuartersFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountQuartersFilterProvider.java index 64efec5022..8d184780a2 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountQuartersFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountQuartersFilterProvider.java @@ -4,13 +4,14 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.CountQuartersFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class CountQuartersFilterProvider extends AbstractFilterProvider { public CountQuartersFilterProvider() { - super(CountQuartersFilterDefinition.class); + super(CountQuartersFilterDefinition.class, IntegerRangeFilterValue.class); } @Override public String type() { @@ -19,6 +20,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, CountQuartersFilterDefinition payload) { - return filter(context, payload, "INTEGER_RANGE", 1, null, false, List.of(requiredColumn(context, payload))); + return filter(context, payload, IntegerRangeFilterValue.class, 1, null, false, List.of(requiredColumn(context, payload))); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DateDistanceFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DateDistanceFilterProvider.java index 34ee70dc57..7ccf0b360a 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DateDistanceFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DateDistanceFilterProvider.java @@ -4,13 +4,14 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.DateDistanceFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class DateDistanceFilterProvider extends AbstractFilterProvider { public DateDistanceFilterProvider() { - super(DateDistanceFilterDefinition.class); + super(DateDistanceFilterDefinition.class, IntegerRangeFilterValue.class); } @Override public String type() { @@ -19,6 +20,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, DateDistanceFilterDefinition payload) { - return filter(context, payload, "INTEGER_RANGE", null, null, false, List.of(requiredColumn(context, payload))); + return filter(context, payload, IntegerRangeFilterValue.class, null, null, false, List.of(requiredColumn(context, payload))); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DurationSumFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DurationSumFilterProvider.java index a137af4cf8..9f0aa14b36 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DurationSumFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/DurationSumFilterProvider.java @@ -2,6 +2,7 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.DurationSumFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @@ -9,7 +10,7 @@ @ApplicationScoped public class DurationSumFilterProvider extends AbstractFilterProvider { public DurationSumFilterProvider() { - super(DurationSumFilterDefinition.class); + super(DurationSumFilterDefinition.class, IntegerRangeFilterValue.class); } @Override public String type() { @@ -19,6 +20,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, DurationSumFilterDefinition payload) { ColumnId column = requiredColumn(context, payload); - return filter(context, payload, "INTEGER_RANGE", 0, null, false, columns(column, optionalColumns(context, payload.getDistinctBy()))); + return filter(context, payload, IntegerRangeFilterValue.class, 0, null, false, columns(column, optionalColumns(context, payload.getDistinctBy()))); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/FlagsFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/FlagsFilterProvider.java index 07bd053bce..7c9e835414 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/FlagsFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/FlagsFilterProvider.java @@ -2,13 +2,14 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.FlagsFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MultiSelectFilterValue; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class FlagsFilterProvider extends AbstractFilterProvider { public FlagsFilterProvider() { - super(FlagsFilterDefinition.class); + super(FlagsFilterDefinition.class, MultiSelectFilterValue.class); } @Override public String type() { @@ -17,6 +18,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, FlagsFilterDefinition payload) { - return filter(context, payload, "MULTI_SELECT", null, null, false, flagColumns(context, payload.getFlags())); + return filter(context, payload, MultiSelectFilterValue.class, null, null, false, flagColumns(context, payload.getFlags())); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/NumberFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/NumberFilterProvider.java index 374d7a967b..8e1d52fd0e 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/NumberFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/NumberFilterProvider.java @@ -4,6 +4,9 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.NumberFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MoneyRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.RealRangeFilterValue; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @@ -11,7 +14,7 @@ @ApplicationScoped public class NumberFilterProvider extends AbstractFilterProvider { public NumberFilterProvider() { - super(NumberFilterDefinition.class); + super(NumberFilterDefinition.class, IntegerRangeFilterValue.class, MoneyRangeFilterValue.class, RealRangeFilterValue.class); } @Override public String type() { @@ -21,6 +24,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, NumberFilterDefinition payload) { ColumnId column = requiredColumn(context, payload); - return filter(context, payload, numericFrontendType(context, column), null, null, false, List.of(column)); + return filter(context, payload, numericRangeValueType(context, column), null, null, false, List.of(column)); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/PrefixTextFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/PrefixTextFilterProvider.java index 89b271d590..bef4c05169 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/PrefixTextFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/PrefixTextFilterProvider.java @@ -4,13 +4,14 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.PrefixTextFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.StringFilterValue; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class PrefixTextFilterProvider extends AbstractFilterProvider { public PrefixTextFilterProvider() { - super(PrefixTextFilterDefinition.class); + super(PrefixTextFilterDefinition.class, StringFilterValue.class); } @Override public String type() { @@ -19,6 +20,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, PrefixTextFilterDefinition payload) { - return filter(context, payload, "STRING", null, null, false, List.of(requiredColumn(context, payload))); + return filter(context, payload, StringFilterValue.class, null, null, false, List.of(requiredColumn(context, payload))); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/QuartersInYearFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/QuartersInYearFilterProvider.java index d4bddb6921..2146af23c0 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/QuartersInYearFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/QuartersInYearFilterProvider.java @@ -4,13 +4,14 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.QuartersInYearFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class QuartersInYearFilterProvider extends AbstractFilterProvider { public QuartersInYearFilterProvider() { - super(QuartersInYearFilterDefinition.class); + super(QuartersInYearFilterDefinition.class, IntegerRangeFilterValue.class); } @Override public String type() { @@ -19,6 +20,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, QuartersInYearFilterDefinition payload) { - return filter(context, payload, "INTEGER_RANGE", 1, 4, false, List.of(requiredColumn(context, payload))); + return filter(context, payload, IntegerRangeFilterValue.class, 1, 4, false, List.of(requiredColumn(context, payload))); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SelectFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SelectFilterProvider.java index af0d8cbb03..041321ff51 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SelectFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SelectFilterProvider.java @@ -4,13 +4,16 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.MultiSelectFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.BigMultiSelectFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MultiSelectFilterValue; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class SelectFilterProvider extends AbstractFilterProvider { public SelectFilterProvider() { - super(MultiSelectFilterDefinition.class); + super(MultiSelectFilterDefinition.class, MultiSelectFilterValue.class, BigMultiSelectFilterValue.class); } @Override public String type() { @@ -19,7 +22,10 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, MultiSelectFilterDefinition payload) { - String frontendType = options(payload).isEmpty() ? "BIG_MULTI_SELECT" : "MULTI_SELECT"; - return filter(context, payload, frontendType, null, null, options(payload).isEmpty(), List.of(requiredColumn(context, payload))); + boolean hasOptions = !options(payload).isEmpty(); + Class valueType = hasOptions + ? MultiSelectFilterValue.class + : BigMultiSelectFilterValue.class; + return filter(context, payload, valueType, null, null, !hasOptions, List.of(requiredColumn(context, payload))); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SingleSelectFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SingleSelectFilterProvider.java index e8cbff4272..bf459a6bf9 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SingleSelectFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SingleSelectFilterProvider.java @@ -4,13 +4,15 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.SingleSelectFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.SelectFilterValue; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; +// TODO remove this filter or migrate it's FilterValue to StringFilterValue @ApplicationScoped public class SingleSelectFilterProvider extends AbstractFilterProvider { public SingleSelectFilterProvider() { - super(SingleSelectFilterDefinition.class); + super(SingleSelectFilterDefinition.class, SelectFilterValue.class); } @Override public String type() { @@ -19,6 +21,6 @@ public String type() { @Override public DatasetCatalogRepository.Filter convert(FilterConversionContext context, SingleSelectFilterDefinition payload) { - return filter(context, payload, "SELECT", null, null, false, List.of(requiredColumn(context, payload))); + return filter(context, payload, SelectFilterValue.class, null, null, false, List.of(requiredColumn(context, payload))); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SumFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SumFilterProvider.java index a0779c3669..4de0deee79 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SumFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/SumFilterProvider.java @@ -5,6 +5,9 @@ import com.bakdata.conquery.quarkus.concepts.filters.FilterConversionContext; import com.bakdata.conquery.quarkus.concepts.filters.definitions.SumFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MoneyRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.RealRangeFilterValue; import com.bakdata.conquery.quarkus.ids.ColumnId; import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import jakarta.enterprise.context.ApplicationScoped; @@ -12,7 +15,7 @@ @ApplicationScoped public class SumFilterProvider extends AbstractFilterProvider { public SumFilterProvider() { - super(SumFilterDefinition.class); + super(SumFilterDefinition.class, IntegerRangeFilterValue.class, MoneyRangeFilterValue.class, RealRangeFilterValue.class); } @Override public String type() { @@ -26,6 +29,6 @@ public DatasetCatalogRepository.Filter convert(FilterConversionContext context, if (payload.getSubtractColumn() != null && !payload.getSubtractColumn().isBlank()) { required.add(context.columnId(payload.getSubtractColumn())); } - return filter(context, payload, numericFrontendType(context, column), null, null, false, required); + return filter(context, payload, numericRangeValueType(context, column), null, null, false, required); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValue.java new file mode 100644 index 0000000000..9a2fad27dd --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValue.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values; + +import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelBase; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +@Schema(name = "QueryFilterValue", description = "Value assigned to a concept filter in a query. The concrete value model is selected by `type`.", discriminatorProperty = "type") +@PolymorphicModelBase( + schemaName = "QueryFilterValue", + description = "Value assigned to a concept filter in a query. The concrete value model is selected by `type`." +) +public interface FilterValue { + + FilterId filter(); +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueProvider.java new file mode 100644 index 0000000000..0bfa5ea777 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueProvider.java @@ -0,0 +1,25 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values; + +import com.bakdata.conquery.quarkus.models.PolymorphicModelTypeProvider; + +public interface FilterValueProvider extends PolymorphicModelTypeProvider { + + String type(); + + Class payloadType(); + + @Override + default Class baseType() { + return FilterValue.class; + } + + @Override + default String typeId() { + return type(); + } + + @Override + default Class modelType() { + return payloadType(); + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueRegistry.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueRegistry.java new file mode 100644 index 0000000000..ef47065c86 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueRegistry.java @@ -0,0 +1,42 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values; + +import java.util.LinkedHashMap; +import java.util.Map; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +@ApplicationScoped +public class FilterValueRegistry { + + @Inject + Instance> providers; + + private Map, FilterValueProvider> providersByModelType = Map.of(); + + @PostConstruct + void init() { + Map, FilterValueProvider> index = new LinkedHashMap<>(); + for (FilterValueProvider provider : providers) { + FilterValueProvider previous = index.put(provider.payloadType(), provider); + if (previous != null) { + throw new IllegalStateException("Duplicate filter value provider for model type " + provider.payloadType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); + } + } + providersByModelType = Map.copyOf(index); + } + + public FilterValueProvider require(FilterValue value) { + return require(value.getClass()); + } + + public FilterValueProvider require(Class modelType) { + FilterValueProvider provider = providersByModelType.get(modelType); + if (provider == null) { + throw new IllegalArgumentException("No filter value provider registered for " + modelType.getName()); + } + return provider; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/BigMultiSelectFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/BigMultiSelectFilterValue.java new file mode 100644 index 0000000000..c5fa6cc06f --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/BigMultiSelectFilterValue.java @@ -0,0 +1,13 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import java.util.Set; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.ids.FilterId; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryBigMultiSelectFilterValue", description = "A potentially large set of selected filter values.") +public record BigMultiSelectFilterValue(@NotNull FilterId filter, @NotNull @NotEmpty Set<@NotNull String> value) implements FilterValue { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterRange.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterRange.java new file mode 100644 index 0000000000..e01658daea --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterRange.java @@ -0,0 +1,7 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryIntegerFilterRange", description = "Inclusive integer range. Either boundary may be omitted.") +public record IntegerFilterRange(Long min, Long max) { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterValue.java new file mode 100644 index 0000000000..a632b60be5 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterValue.java @@ -0,0 +1,10 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.ids.FilterId; +import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryIntegerFilterValue", description = "A single integer filter value.") +public record IntegerFilterValue(@NotNull FilterId filter, @NotNull Long value) implements FilterValue { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerRangeFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerRangeFilterValue.java new file mode 100644 index 0000000000..b7b7a94054 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerRangeFilterValue.java @@ -0,0 +1,11 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.ids.FilterId; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryIntegerRangeFilterValue", description = "An integer range filter value.") +public record IntegerRangeFilterValue(@NotNull FilterId filter, @Valid @NotNull IntegerFilterRange value) implements FilterValue { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MoneyRangeFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MoneyRangeFilterValue.java new file mode 100644 index 0000000000..b64097041b --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MoneyRangeFilterValue.java @@ -0,0 +1,11 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.ids.FilterId; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryMoneyRangeFilterValue", description = "A monetary range represented in the configured minor currency unit.") +public record MoneyRangeFilterValue(@NotNull FilterId filter, @Valid @NotNull IntegerFilterRange value) implements FilterValue { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MultiSelectFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MultiSelectFilterValue.java new file mode 100644 index 0000000000..e91977833c --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MultiSelectFilterValue.java @@ -0,0 +1,13 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import java.util.Set; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.ids.FilterId; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryMultiSelectFilterValue", description = "A set of selected filter values.") +public record MultiSelectFilterValue(@NotNull FilterId filter, @NotNull @NotEmpty Set<@NotNull String> value) implements FilterValue { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterRange.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterRange.java new file mode 100644 index 0000000000..19acd84701 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterRange.java @@ -0,0 +1,9 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import java.math.BigDecimal; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryRealFilterRange", description = "Inclusive real-number range. Either boundary may be omitted.") +public record RealFilterRange(BigDecimal min, BigDecimal max) { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterValue.java new file mode 100644 index 0000000000..4d21a7eec3 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterValue.java @@ -0,0 +1,12 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import java.math.BigDecimal; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.ids.FilterId; +import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryRealFilterValue", description = "A single real-number filter value.") +public record RealFilterValue(@NotNull FilterId filter, @NotNull BigDecimal value) implements FilterValue { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealRangeFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealRangeFilterValue.java new file mode 100644 index 0000000000..c2410c25e8 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealRangeFilterValue.java @@ -0,0 +1,11 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.ids.FilterId; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryRealRangeFilterValue", description = "A real-number range filter value.") +public record RealRangeFilterValue(@NotNull FilterId filter, @Valid @NotNull RealFilterRange value) implements FilterValue { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/SelectFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/SelectFilterValue.java new file mode 100644 index 0000000000..105848aa50 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/SelectFilterValue.java @@ -0,0 +1,10 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.ids.FilterId; +import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QuerySelectFilterValue", description = "A single selected filter value.") +public record SelectFilterValue(@NotNull FilterId filter, @NotNull String value) implements FilterValue { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/StringFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/StringFilterValue.java new file mode 100644 index 0000000000..c2f5c88074 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/StringFilterValue.java @@ -0,0 +1,10 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.definitions; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.ids.FilterId; +import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema(name = "QueryStringFilterValue", description = "A free-text filter value.") +public record StringFilterValue(@NotNull FilterId filter, @NotNull String value) implements FilterValue { +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/AbstractFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/AbstractFilterValueProvider.java new file mode 100644 index 0000000000..d3029a9a40 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/AbstractFilterValueProvider.java @@ -0,0 +1,18 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValueProvider; + +abstract class AbstractFilterValueProvider implements FilterValueProvider { + + private final Class payloadType; + + protected AbstractFilterValueProvider(Class payloadType) { + this.payloadType = payloadType; + } + + @Override + public Class payloadType() { + return payloadType; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/BigMultiSelectFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/BigMultiSelectFilterValueProvider.java new file mode 100644 index 0000000000..ac651c2743 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/BigMultiSelectFilterValueProvider.java @@ -0,0 +1,16 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.BigMultiSelectFilterValue; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class BigMultiSelectFilterValueProvider extends AbstractFilterValueProvider { + public BigMultiSelectFilterValueProvider() { + super(BigMultiSelectFilterValue.class); + } + + @Override + public String type() { + return "BIG_MULTI_SELECT"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerFilterValueProvider.java new file mode 100644 index 0000000000..3acbc817c4 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerFilterValueProvider.java @@ -0,0 +1,16 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerFilterValue; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class IntegerFilterValueProvider extends AbstractFilterValueProvider { + public IntegerFilterValueProvider() { + super(IntegerFilterValue.class); + } + + @Override + public String type() { + return "INTEGER"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerRangeFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerRangeFilterValueProvider.java new file mode 100644 index 0000000000..1b3fd64679 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerRangeFilterValueProvider.java @@ -0,0 +1,16 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class IntegerRangeFilterValueProvider extends AbstractFilterValueProvider { + public IntegerRangeFilterValueProvider() { + super(IntegerRangeFilterValue.class); + } + + @Override + public String type() { + return "INTEGER_RANGE"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MoneyRangeFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MoneyRangeFilterValueProvider.java new file mode 100644 index 0000000000..bd73809b80 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MoneyRangeFilterValueProvider.java @@ -0,0 +1,16 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MoneyRangeFilterValue; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class MoneyRangeFilterValueProvider extends AbstractFilterValueProvider { + public MoneyRangeFilterValueProvider() { + super(MoneyRangeFilterValue.class); + } + + @Override + public String type() { + return "MONEY_RANGE"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MultiSelectFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MultiSelectFilterValueProvider.java new file mode 100644 index 0000000000..e1d824eaf5 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MultiSelectFilterValueProvider.java @@ -0,0 +1,16 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MultiSelectFilterValue; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class MultiSelectFilterValueProvider extends AbstractFilterValueProvider { + public MultiSelectFilterValueProvider() { + super(MultiSelectFilterValue.class); + } + + @Override + public String type() { + return "MULTI_SELECT"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealFilterValueProvider.java new file mode 100644 index 0000000000..2d809b60ec --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealFilterValueProvider.java @@ -0,0 +1,16 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.RealFilterValue; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class RealFilterValueProvider extends AbstractFilterValueProvider { + public RealFilterValueProvider() { + super(RealFilterValue.class); + } + + @Override + public String type() { + return "REAL"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealRangeFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealRangeFilterValueProvider.java new file mode 100644 index 0000000000..3d44335639 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealRangeFilterValueProvider.java @@ -0,0 +1,16 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.RealRangeFilterValue; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class RealRangeFilterValueProvider extends AbstractFilterValueProvider { + public RealRangeFilterValueProvider() { + super(RealRangeFilterValue.class); + } + + @Override + public String type() { + return "REAL_RANGE"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/SelectFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/SelectFilterValueProvider.java new file mode 100644 index 0000000000..d4240e3357 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/SelectFilterValueProvider.java @@ -0,0 +1,17 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.SelectFilterValue; +import jakarta.enterprise.context.ApplicationScoped; + +// TODO maybe remove, it may has a different symantic to the frontend than StringFilterValueProvider +@ApplicationScoped +public class SelectFilterValueProvider extends AbstractFilterValueProvider { + public SelectFilterValueProvider() { + super(SelectFilterValue.class); + } + + @Override + public String type() { + return "SELECT"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/StringFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/StringFilterValueProvider.java new file mode 100644 index 0000000000..747680d114 --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/StringFilterValueProvider.java @@ -0,0 +1,16 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values.providers; + +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.StringFilterValue; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class StringFilterValueProvider extends AbstractFilterValueProvider { + public StringFilterValueProvider() { + super(StringFilterValue.class); + } + + @Override + public String type() { + return "STRING"; + } +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/EntityQueryService.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/EntityQueryService.java index 501bb14c5b..2e0a81bc96 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/EntityQueryService.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/services/EntityQueryService.java @@ -1,63 +1,28 @@ package com.bakdata.conquery.quarkus.services; -import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; import com.bakdata.conquery.quarkus.api.QuerySubmissionPayload; -import com.bakdata.conquery.quarkus.config.FrontendRuntimeConfig; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; @ApplicationScoped public class EntityQueryService { - @Inject - FrontendRuntimeConfig frontendConfig; - + // TODO(quarkus-migration): Implement entity queries against dataset data. The records below only preserve the API models. public EntityHistoryResponse getEntityHistory(EntityHistoryRequest request) { - return new EntityHistoryResponse( - List.of(), - List.of(), - List.of( - new EntityInfoResponse( - "entityId", - request.entityId(), - "STRING", - List.of() - ) - ), - List.of() - ); - } - - public List> resolveEntities(List filters) { - String idKind = resolveOutputIdKind(); - return filters.stream() - .flatMap(filter -> filter.value().stream().map(value -> Map.of(idKind, syntheticEntityId(filter.filter(), value)))) - .distinct() - .collect(Collectors.toCollection(ArrayList::new)); + throw notImplemented(); } - private String resolveOutputIdKind() { - Optional> configured = frontendConfig.queryUpload().ids(); - if (configured.isPresent() && !configured.get().isEmpty()) { - return configured.get().stream() - .filter(FrontendRuntimeConfig.IdColumn::print) - .findFirst() - .map(FrontendRuntimeConfig.IdColumn::name) - .orElse(configured.get().getFirst().name()); - } - return "ID"; + public List> resolveEntities(List filters) { + throw notImplemented(); } - private String syntheticEntityId(String filterId, String filterValue) { - // TODO(quarkus-migration): Replace synthetic IDs with actual entity resolution against dataset data. - // The legacy backend executes a concept query and returns matching entity IDs. - String input = (filterId == null ? "" : filterId) + "|" + (filterValue == null ? "" : filterValue); - return "entity-" + Integer.toUnsignedString(input.hashCode(), 36); + private WebApplicationException notImplemented() { + return new WebApplicationException("Entity queries are not implemented yet.", Response.Status.NOT_IMPLEMENTED); } public record EntityHistoryRequest( @@ -68,13 +33,6 @@ public record EntityHistoryRequest( ) { } - public record FilterValuesRequest( - String filter, - String type, - List value - ) { - } - public record EntityHistoryResponse( List resultUrls, List columnDescriptions, 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 77f07f7316..2ec3c7bbd2 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 @@ -161,4 +161,35 @@ void postQueriesEndpointReturns404ForUnknownDataset() { .then() .statusCode(404); } + + @Test + void resolveEntitiesEndpointIsMarkedAsNotImplemented() { + given() + .contentType("application/json") + .body(""" + [{ + "filter":"imdb.titles.release_age", + "type":"MULTI_SELECT", + "value":["one","two"] + }] + """) + .when().post("/api/datasets/imdb/queries/resolve-entities") + .then().statusCode(501); + } + + @Test + void entityHistoryEndpointIsMarkedAsNotImplemented() { + given() + .contentType("application/json") + .body(""" + { + "idKind":"ID", + "entityId":"one", + "time":{"min":"2020-01-01","max":"2020-12-31"}, + "sources":["imdb.titles"] + } + """) + .when().post("/api/datasets/imdb/queries/entity") + .then().statusCode(501); + } } 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 da508b23bf..a51176e577 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 @@ -35,6 +35,60 @@ void cancelQueryEndpointResponds() { .statusCode(204); } + @Test + void queryEndpointRoundTripsTypedFilterValues() { + String queryId = given() + .contentType("application/json") + .body(""" + { + "type":"CONCEPT_QUERY", + "root":{ + "type":"CONCEPT", + "tables":[{ + "id":"imdb.titles", + "filters":[{ + "filter":"imdb.titles.release_age", + "type":"INTEGER_RANGE", + "value":{"min":1,"max":10} + }] + }] + } + } + """) + .when().post("/api/datasets/imdb/queries") + .then().statusCode(200) + .extract().path("id"); + + given() + .when().get("/api/queries/{queryId}", queryId) + .then().statusCode(200) + .body("query.root.tables[0].filters[0].type", equalTo("INTEGER_RANGE")) + .body("query.root.tables[0].filters[0].filter", equalTo("imdb.titles.release_age")) + .body("query.root.tables[0].filters[0].value.min", equalTo(1)) + .body("query.root.tables[0].filters[0].value.max", equalTo(10)); + } + + @Test + void queryEndpointRejectsUnknownFilterValueType() { + given() + .contentType("application/json") + .body(""" + { + "type":"CONCEPT_QUERY", + "root":{ + "type":"CONCEPT", + "tables":[{"filters":[{ + "filter":"imdb.titles.release_age", + "type":"EXTERNAL_VALUE", + "value":1 + }]}] + } + } + """) + .when().post("/api/datasets/imdb/queries") + .then().statusCode(400); + } + private String createQuery() { return given() .contentType("application/json") diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProviderValueTypesTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProviderValueTypesTest.java new file mode 100644 index 0000000000..732322ab96 --- /dev/null +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProviderValueTypesTest.java @@ -0,0 +1,60 @@ +package com.bakdata.conquery.quarkus.concepts.filters; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import java.util.Set; + +import com.bakdata.conquery.quarkus.concepts.filters.providers.BigMultiSelectFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.CategoryMaxSumFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.CountFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.CountQuartersFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.DateDistanceFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.DurationSumFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.FlagsFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.NumberFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.PrefixTextFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.QuartersInYearFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.SelectFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.SingleSelectFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.providers.SumFilterProvider; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.BigMultiSelectFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.IntegerRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MoneyRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.MultiSelectFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.RealRangeFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.SelectFilterValue; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.StringFilterValue; +import org.junit.jupiter.api.Test; + +class FilterDefinitionProviderValueTypesTest { + + private static final Set> INTEGER_RANGE = Set.of(IntegerRangeFilterValue.class); + private static final Set> NUMERIC_RANGES = Set.of( + IntegerRangeFilterValue.class, + MoneyRangeFilterValue.class, + RealRangeFilterValue.class + ); + + @Test + void declaresAcceptedFilterValueTypesForEveryBuiltInProvider() { + Map, Set>> expected = Map.ofEntries( + Map.entry(new BigMultiSelectFilterProvider(), Set.of(BigMultiSelectFilterValue.class)), + Map.entry(new CategoryMaxSumFilterProvider(), NUMERIC_RANGES), + Map.entry(new CountFilterProvider(), INTEGER_RANGE), + Map.entry(new CountQuartersFilterProvider(), INTEGER_RANGE), + Map.entry(new DateDistanceFilterProvider(), INTEGER_RANGE), + Map.entry(new DurationSumFilterProvider(), INTEGER_RANGE), + Map.entry(new FlagsFilterProvider(), Set.of(MultiSelectFilterValue.class)), + Map.entry(new NumberFilterProvider(), NUMERIC_RANGES), + Map.entry(new PrefixTextFilterProvider(), Set.of(StringFilterValue.class)), + Map.entry(new QuartersInYearFilterProvider(), INTEGER_RANGE), + Map.entry(new SelectFilterProvider(), Set.of(MultiSelectFilterValue.class, BigMultiSelectFilterValue.class)), + Map.entry(new SingleSelectFilterProvider(), Set.of(SelectFilterValue.class)), + Map.entry(new SumFilterProvider(), NUMERIC_RANGES) + ); + + expected.forEach((provider, valueTypes) -> assertEquals(valueTypes, provider.acceptedValueTypes(), provider.type())); + } +} diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueDeserializationTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueDeserializationTest.java new file mode 100644 index 0000000000..cf73640347 --- /dev/null +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueDeserializationTest.java @@ -0,0 +1,87 @@ +package com.bakdata.conquery.quarkus.concepts.filters.values; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import java.math.BigDecimal; +import java.util.Set; + +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.definitions.MultiSelectFilterDefinition; +import com.bakdata.conquery.quarkus.concepts.filters.values.definitions.*; +import com.bakdata.conquery.quarkus.ids.FilterId; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@QuarkusTest +class FilterValueDeserializationTest { + + @Inject + ObjectMapper objectMapper; + + @ParameterizedTest + @ValueSource(strings = {"MULTI_SELECT", "BIG_MULTI_SELECT", "SELECT", "STRING", "INTEGER", "INTEGER_RANGE", "MONEY_RANGE", "REAL", "REAL_RANGE"}) + void deserializesEveryRegisteredType(String type) throws Exception { + String value = switch (type) { + case "MULTI_SELECT", "BIG_MULTI_SELECT" -> "[\"one\"]"; + case "SELECT", "STRING" -> "\"one\""; + case "INTEGER" -> "42"; + case "INTEGER_RANGE", "MONEY_RANGE" -> "{\"min\":1,\"max\":2}"; + case "REAL" -> "1.5"; + case "REAL_RANGE" -> "{\"min\":1.5,\"max\":2.5}"; + default -> throw new IllegalArgumentException(type); + }; + FilterValue filterValue = read(type, value); + + assertEquals(expectedClass(type), filterValue.getClass()); + assertEquals(FilterId.parse("demo.concept.connector.filter"), filterValue.filter()); + assertEquals(type, objectMapper.valueToTree(filterValue).path("type").asText()); + } + + @Test + void keepsFilterDefinitionAndFilterValueSelectDiscriminatorsIndependent() throws Exception { + FilterDefinition definition = objectMapper.readValue(""" + {"type":"SELECT","column":"code"} + """, FilterDefinition.class); + FilterValue value = read("SELECT", "\"one\""); + + assertInstanceOf(MultiSelectFilterDefinition.class, definition); + SelectFilterValue selectValue = assertInstanceOf(SelectFilterValue.class, value); + assertEquals("one", selectValue.value()); + } + + @Test + void usesConcreteCollectionNumberAndRangeTypes() throws Exception { + MultiSelectFilterValue multi = assertInstanceOf(MultiSelectFilterValue.class, read("MULTI_SELECT", "[\"one\",\"two\"]")); + RealFilterValue real = assertInstanceOf(RealFilterValue.class, read("REAL", "1.25")); + IntegerRangeFilterValue range = assertInstanceOf(IntegerRangeFilterValue.class, read("INTEGER_RANGE", "{\"min\":1,\"max\":4}")); + + assertEquals(Set.of("one", "two"), multi.value()); + assertEquals(new BigDecimal("1.25"), real.value()); + assertEquals(1L, range.value().min()); + assertEquals(4L, range.value().max()); + } + + private FilterValue read(String type, String value) throws Exception { + return objectMapper.readValue("{\"type\":\"" + type + "\",\"filter\":\"demo.concept.connector.filter\",\"value\":" + value + "}", FilterValue.class); + } + + private Class expectedClass(String type) { + return switch (type) { + case "MULTI_SELECT" -> MultiSelectFilterValue.class; + case "BIG_MULTI_SELECT" -> BigMultiSelectFilterValue.class; + case "SELECT" -> SelectFilterValue.class; + case "STRING" -> StringFilterValue.class; + case "INTEGER" -> IntegerFilterValue.class; + case "INTEGER_RANGE" -> IntegerRangeFilterValue.class; + case "MONEY_RANGE" -> MoneyRangeFilterValue.class; + case "REAL" -> RealFilterValue.class; + case "REAL_RANGE" -> RealRangeFilterValue.class; + default -> throw new IllegalArgumentException(type); + }; + } +} diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java index 1c67945136..399e3d12af 100644 --- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java @@ -66,6 +66,17 @@ void exposesRegisteredFilterModelsAsDiscriminatedSchema() throws Exception { JsonNode andConditionSchema = schemas.path("MetadataAndConceptCondition"); assertEquals("AND", findProperty(andConditionSchema, "type").path("const").asText()); assertNotNull(findProperty(andConditionSchema, "conditions")); + + JsonNode filterValueSchema = schemas.path("QueryFilterValue"); + assertEquals("type", filterValueSchema.path("discriminator").path("propertyName").asText()); + assertEquals("#/components/schemas/QuerySelectFilterValue", filterValueSchema.path("discriminator").path("mapping").path("SELECT").asText()); + assertEquals(9, filterValueSchema.path("oneOf").size()); + assertEquals("#/components/schemas/MetadataMultiSelectFilter", filterSchema.path("discriminator").path("mapping").path("SELECT").asText()); + + JsonNode integerRangeFilterValueSchema = schemas.path("QueryIntegerRangeFilterValue"); + assertEquals("INTEGER_RANGE", findProperty(integerRangeFilterValueSchema, "type").path("const").asText()); + assertNotNull(findProperty(integerRangeFilterValueSchema, "filter")); + assertNotNull(findProperty(integerRangeFilterValueSchema, "value")); } private JsonNode findProperty(JsonNode schema, String name) { From 01115bc383cf1cbc91763d10453bea256ba8a0ab Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:00:29 +0200 Subject: [PATCH 43/45] fix openapi spec for classes annotated with JsonCreator --- .../PolymorphicModelsOpenApiFilter.java | 161 ++++++++++++++++++ .../models/PolymorphicModelsOpenApiTest.java | 6 +- 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiFilter.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiFilter.java index d11b24125d..e9eb86e428 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiFilter.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiFilter.java @@ -1,10 +1,24 @@ package com.bakdata.conquery.quarkus.models; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.RecordComponent; +import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; import io.quarkus.arc.Arc; import io.quarkus.smallrye.openapi.OpenApiFilter; import org.eclipse.microprofile.openapi.OASFactory; @@ -14,6 +28,22 @@ import org.eclipse.microprofile.openapi.models.media.Discriminator; import org.eclipse.microprofile.openapi.models.media.Schema; +/** + * Aligns the generated OpenAPI document with the runtime Jackson model. + *

+ * Vanilla SmallRye OpenAPI generation cannot fully describe the model for two reasons: + *

    + *
  • Polymorphic implementations are registered dynamically through CDI providers. The static OpenAPI scan does + * not know which implementations belong to a base type, so it cannot build the corresponding {@code oneOf} and + * discriminator mappings.
  • + *
  • Some value objects, notably IDs, are structured records in Java but use a delegating {@code @JsonCreator} + * and {@code @JsonValue} to deserialize and serialize as scalars. SmallRye otherwise exposes their internal record + * components instead of their JSON representation.
  • + *
+ * This filter projects the runtime polymorphic registry into OpenAPI and replaces affected component schemas with the + * scalar type used by Jackson. As a result, generated clients see the same shapes that the API actually accepts and + * returns. + */ @OpenApiFilter(OpenApiFilter.RunStage.RUN) public class PolymorphicModelsOpenApiFilter implements OASFilter { @@ -31,6 +61,137 @@ public void filterOpenAPI(OpenAPI openAPI) { for (Map.Entry, List>> family : byBase.entrySet()) { addFamilySchema(components, family.getKey(), family.getValue()); } + replaceJacksonScalarSchemas(components, registry.allTypes()); + } + + private void replaceJacksonScalarSchemas(Components components, Collection> types) { + Set> visited = new HashSet<>(); + for (PolymorphicModelType type : types) { + visitReferencedTypes(components, type.modelType(), visited); + } + } + + private void visitReferencedTypes(Components components, Type type, Set> visited) { + if (type instanceof ParameterizedType parameterizedType) { + visitReferencedTypes(components, parameterizedType.getRawType(), visited); + for (Type argument : parameterizedType.getActualTypeArguments()) { + visitReferencedTypes(components, argument, visited); + } + return; + } + if (!(type instanceof Class modelType) || modelType.isPrimitive() || modelType.isArray() || !visited.add(modelType)) { + return; + } + + Optional> scalarType = jacksonScalarType(modelType); + if (scalarType.isPresent()) { + replaceComponentSchema(components, modelType, scalarType.get()); + return; + } + if (!modelType.getPackageName().startsWith("com.bakdata.conquery")) { + return; + } + + for (RecordComponent component : modelType.getRecordComponents() == null ? new RecordComponent[0] : modelType.getRecordComponents()) { + visitReferencedTypes(components, component.getGenericType(), visited); + } + for (Field field : modelType.getDeclaredFields()) { + if (!field.isSynthetic() && !Modifier.isStatic(field.getModifiers())) { + visitReferencedTypes(components, field.getGenericType(), visited); + } + } + Class superType = modelType.getSuperclass(); + if (superType != null) { + visitReferencedTypes(components, superType, visited); + } + } + + private Optional> jacksonScalarType(Class modelType) { + Optional> creatorType = delegatingCreatorType(modelType); + Optional> jsonValueType = jsonValueType(modelType); + if (creatorType.isEmpty() || jsonValueType.isEmpty()) { + return Optional.empty(); + } + if (!creatorType.get().equals(jsonValueType.get())) { + throw new IllegalStateException("Jackson scalar model " + modelType.getName() + + " uses incompatible @JsonCreator and @JsonValue types: " + + creatorType.get().getName() + " and " + jsonValueType.get().getName()); + } + return creatorType; + } + + private Optional> delegatingCreatorType(Class modelType) { + for (Method method : modelType.getDeclaredMethods()) { + JsonCreator creator = method.getAnnotation(JsonCreator.class); + if (isDelegatingCreator(creator, method.getParameterCount())) { + return Optional.of(method.getParameterTypes()[0]); + } + } + for (Constructor constructor : modelType.getDeclaredConstructors()) { + JsonCreator creator = constructor.getAnnotation(JsonCreator.class); + if (isDelegatingCreator(creator, constructor.getParameterCount())) { + return Optional.of(constructor.getParameterTypes()[0]); + } + } + return Optional.empty(); + } + + private boolean isDelegatingCreator(JsonCreator creator, int parameterCount) { + return creator != null && creator.mode() == JsonCreator.Mode.DELEGATING && parameterCount == 1; + } + + private Optional> jsonValueType(Class modelType) { + for (Method method : modelType.getDeclaredMethods()) { + if (isJsonValue(method)) { + return Optional.of(method.getReturnType()); + } + } + for (Field field : modelType.getDeclaredFields()) { + if (isJsonValue(field)) { + return Optional.of(field.getType()); + } + } + return Optional.empty(); + } + + private boolean isJsonValue(AnnotatedElement element) { + JsonValue annotation = element.getAnnotation(JsonValue.class); + return annotation != null && annotation.value(); + } + + private void replaceComponentSchema(Components components, Class modelType, Class scalarType) { + if (components.getSchemas() == null) { + return; + } + String name = schemaName(modelType); + Schema existing = components.getSchemas().get(name); + if (existing == null) { + return; + } + Schema replacement = OASFactory.createSchema().addType(openApiScalarType(modelType, scalarType)); + if (existing.getDescription() != null) { + replacement.description(existing.getDescription()); + } + components.addSchema(name, replacement); + } + + private Schema.SchemaType openApiScalarType(Class modelType, Class scalarType) { + if (scalarType == String.class || scalarType == char.class || scalarType == Character.class) { + return Schema.SchemaType.STRING; + } + if (scalarType == boolean.class || scalarType == Boolean.class) { + return Schema.SchemaType.BOOLEAN; + } + if (scalarType == byte.class || scalarType == Byte.class + || scalarType == short.class || scalarType == Short.class + || scalarType == int.class || scalarType == Integer.class + || scalarType == long.class || scalarType == Long.class) { + return Schema.SchemaType.INTEGER; + } + if (Number.class.isAssignableFrom(scalarType) || scalarType == float.class || scalarType == double.class) { + return Schema.SchemaType.NUMBER; + } + throw new IllegalStateException("Unsupported @JsonCreator scalar type " + scalarType.getName() + " on " + modelType.getName()); } private void addFamilySchema(Components components, Class baseType, List> types) { diff --git a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java index 399e3d12af..3e812b82f4 100644 --- a/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java +++ b/backend-quarkus/src/test/java/com/bakdata/conquery/quarkus/models/PolymorphicModelsOpenApiTest.java @@ -75,8 +75,12 @@ void exposesRegisteredFilterModelsAsDiscriminatedSchema() throws Exception { JsonNode integerRangeFilterValueSchema = schemas.path("QueryIntegerRangeFilterValue"); assertEquals("INTEGER_RANGE", findProperty(integerRangeFilterValueSchema, "type").path("const").asText()); - assertNotNull(findProperty(integerRangeFilterValueSchema, "filter")); + assertEquals("#/components/schemas/FilterId", findProperty(integerRangeFilterValueSchema, "filter").path("$ref").asText()); assertNotNull(findProperty(integerRangeFilterValueSchema, "value")); + + JsonNode filterIdSchema = schemas.path("FilterId"); + assertEquals("string", filterIdSchema.path("type").asText()); + assertTrue(filterIdSchema.path("properties").isMissingNode()); } private JsonNode findProperty(JsonNode schema, String name) { From ee8d10ceeaa0167cc95e25ad331848eb9a0c8235 Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:57:30 +0200 Subject: [PATCH 44/45] Move polymorphic type definition to definition class Co-authored-by: Codex --- backend-quarkus/README.md | 6 +- .../conditions/ConceptConditionProvider.java | 19 +----- .../definitions/AndConceptCondition.java | 2 + .../ColumnEqualConceptCondition.java | 3 + .../definitions/EqualConceptCondition.java | 3 + .../definitions/NotConceptCondition.java | 2 + .../definitions/OrConceptCondition.java | 2 + .../PrefixListConceptCondition.java | 3 + .../PrefixRangeConceptCondition.java | 3 + .../definitions/PresentConceptCondition.java | 3 + .../AbstractConceptConditionProvider.java | 10 ++-- .../AndConceptConditionProvider.java | 4 -- .../ColumnEqualConceptConditionProvider.java | 4 -- .../EqualConceptConditionProvider.java | 4 -- .../NotConceptConditionProvider.java | 4 -- .../providers/OrConceptConditionProvider.java | 4 -- .../PrefixListConceptConditionProvider.java | 4 -- .../PrefixRangeConceptConditionProvider.java | 4 -- .../PresentConceptConditionProvider.java | 4 -- .../filters/FilterDefinitionAssembler.java | 2 +- .../filters/FilterDefinitionProvider.java | 58 +++++++++++++------ .../filters/FilterDefinitionRegistry.java | 4 +- .../BigMultiSelectFilterDefinition.java | 3 + .../CategoryMaxSumFilterDefinition.java | 3 + .../definitions/CountFilterDefinition.java | 3 + .../CountQuartersFilterDefinition.java | 3 + .../DateDistanceFilterDefinition.java | 3 + .../DurationSumFilterDefinition.java | 3 + .../definitions/FlagsFilterDefinition.java | 3 + .../MultiSelectFilterDefinition.java | 3 + .../definitions/NumberFilterDefinition.java | 3 + .../PrefixTextFilterDefinition.java | 3 + .../QuartersInYearFilterDefinition.java | 3 + .../SingleSelectFilterDefinition.java | 3 + .../definitions/SumFilterDefinition.java | 3 + .../providers/AbstractFilterProvider.java | 10 ++-- .../BigMultiSelectFilterProvider.java | 4 -- .../CategoryMaxSumFilterProvider.java | 12 +--- .../providers/CountFilterProvider.java | 4 -- .../CountQuartersFilterProvider.java | 4 -- .../providers/DateDistanceFilterProvider.java | 4 -- .../providers/DurationSumFilterProvider.java | 4 -- .../providers/FlagsFilterProvider.java | 4 -- .../providers/NumberFilterProvider.java | 4 -- .../providers/PrefixTextFilterProvider.java | 4 -- .../QuartersInYearFilterProvider.java | 4 -- .../providers/SelectFilterProvider.java | 4 -- .../providers/SingleSelectFilterProvider.java | 4 -- .../filters/providers/SumFilterProvider.java | 4 -- .../concepts/filters/values/FilterValue.java | 1 + .../filters/values/FilterValueProvider.java | 19 +----- .../filters/values/FilterValueRegistry.java | 4 +- .../BigMultiSelectFilterValue.java | 2 + .../definitions/IntegerFilterValue.java | 2 + .../definitions/IntegerRangeFilterValue.java | 2 + .../definitions/MoneyRangeFilterValue.java | 2 + .../definitions/MultiSelectFilterValue.java | 2 + .../values/definitions/RealFilterValue.java | 2 + .../definitions/RealRangeFilterValue.java | 2 + .../values/definitions/SelectFilterValue.java | 2 + .../values/definitions/StringFilterValue.java | 2 + .../AbstractFilterValueProvider.java | 10 ++-- .../BigMultiSelectFilterValueProvider.java | 4 -- .../providers/IntegerFilterValueProvider.java | 4 -- .../IntegerRangeFilterValueProvider.java | 4 -- .../MoneyRangeFilterValueProvider.java | 4 -- .../MultiSelectFilterValueProvider.java | 4 -- .../providers/RealFilterValueProvider.java | 4 -- .../RealRangeFilterValueProvider.java | 4 -- .../providers/SelectFilterValueProvider.java | 4 -- .../providers/StringFilterValueProvider.java | 4 -- .../selects/SelectDefinitionAssembler.java | 2 +- .../selects/SelectDefinitionProvider.java | 19 +----- .../selects/SelectDefinitionRegistry.java | 4 +- .../ConceptSelectDefinitionAssembler.java | 2 +- .../ConceptSelectDefinitionProvider.java | 19 +----- .../ConceptSelectDefinitionRegistry.java | 4 +- .../ConceptValuesConceptSelectDefinition.java | 3 + ...EventDateUnionConceptSelectDefinition.java | 3 + ...entDurationSumConceptSelectDefinition.java | 3 + .../ExistsConceptSelectDefinition.java | 3 + .../QuarterConceptSelectDefinition.java | 3 + .../AbstractConceptSelectProvider.java | 10 ++-- .../ConceptValuesConceptSelectProvider.java | 4 -- .../EventDateUnionConceptSelectProvider.java | 4 -- ...EventDurationSumConceptSelectProvider.java | 4 -- .../ExistsConceptSelectProvider.java | 4 -- .../QuarterConceptSelectProvider.java | 4 -- .../CountQuartersSelectDefinition.java | 3 + .../definitions/CountSelectDefinition.java | 3 + .../DateDistanceSelectDefinition.java | 3 + .../DateUnionSelectDefinition.java | 3 + .../definitions/DistinctSelectDefinition.java | 3 + .../DurationSumSelectDefinition.java | 3 + .../definitions/FirstSelectDefinition.java | 3 + .../definitions/FlagsSelectDefinition.java | 3 + .../definitions/LastSelectDefinition.java | 3 + .../definitions/PrefixSelectDefinition.java | 3 + .../QuartersInYearSelectDefinition.java | 3 + .../definitions/RandomSelectDefinition.java | 3 + .../definitions/SumSelectDefinition.java | 3 + .../providers/AbstractSelectProvider.java | 10 ++-- .../CountQuartersSelectProvider.java | 4 -- .../providers/CountSelectProvider.java | 4 -- .../providers/DateDistanceSelectProvider.java | 4 -- .../providers/DateUnionSelectProvider.java | 4 -- .../providers/DistinctSelectProvider.java | 4 -- .../providers/DurationSumSelectProvider.java | 4 -- .../providers/FirstSelectProvider.java | 4 -- .../providers/FlagsSelectProvider.java | 4 -- .../selects/providers/LastSelectProvider.java | 4 -- .../providers/PrefixSelectProvider.java | 4 -- .../QuartersInYearSelectProvider.java | 4 -- .../providers/RandomSelectProvider.java | 4 -- .../selects/providers/SumSelectProvider.java | 4 -- .../models/PolymorphicModelSubtype.java | 20 +++++++ .../models/PolymorphicModelTypeProvider.java | 20 ++++++- 117 files changed, 259 insertions(+), 326 deletions(-) create mode 100644 backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelSubtype.java diff --git a/backend-quarkus/README.md b/backend-quarkus/README.md index d5e89b5577..1ff7342201 100644 --- a/backend-quarkus/README.md +++ b/backend-quarkus/README.md @@ -61,8 +61,10 @@ families are exposed in the OpenAPI document at `GET /q/openapi` as `oneOf` sche Filter, select, and concept-condition implementations are extensible from another CDI-enabled JAR. A filter extension provides: -1. A concrete class implementing `FilterDefinition`, annotated with OpenAPI `@Schema`. -2. A CDI bean implementing `FilterDefinitionProvider` for its type id, model class, and business-model conversion. +1. A concrete class implementing `FilterDefinition`, annotated with OpenAPI `@Schema` and + `@PolymorphicModelSubtype(base = FilterDefinition.class, id = "...")`. +2. A CDI bean implementing `FilterDefinitionProvider` for its model class and business-model conversion and integrity validation. The + provider remains the discovery hook for implementations from external JARs. 3. The filter value model types accepted by that provider. Each model type needs a registered `FilterValueProvider`. Connector selects use the equivalent `SelectDefinition` and `SelectDefinitionProvider` extension points. Concept-level diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionProvider.java index db9dc21768..29343669d6 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/ConceptConditionProvider.java @@ -4,22 +4,7 @@ public interface ConceptConditionProvider extends PolymorphicModelTypeProvider { - String type(); - - Class conditionType(); - - @Override - default Class baseType() { - return ConceptCondition.class; - } - - @Override - default String typeId() { - return type(); - } - - @Override - default Class modelType() { - return conditionType(); + default String type() { + return typeId(); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AndConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AndConceptCondition.java index cc33b00dc2..599744315e 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AndConceptCondition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/AndConceptCondition.java @@ -3,6 +3,7 @@ import java.util.List; import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; @@ -12,6 +13,7 @@ @Getter @Setter @Schema(name = "MetadataAndConceptCondition", description = "Matches when all nested conditions match.") +@PolymorphicModelSubtype(base = ConceptCondition.class, id = "AND") public final class AndConceptCondition extends AbstractConceptCondition { @Valid diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/ColumnEqualConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/ColumnEqualConceptCondition.java index 024356eb0b..3c95aab596 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/ColumnEqualConceptCondition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/ColumnEqualConceptCondition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; @@ -11,6 +13,7 @@ @Getter @Setter @Schema(name = "MetadataColumnEqualConceptCondition", description = "Matches when another column of a record/event has one of the configured values.") +@PolymorphicModelSubtype(base = ConceptCondition.class, id = "COLUMN_EQUAL") public final class ColumnEqualConceptCondition extends AbstractConceptCondition { @NotEmpty diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/EqualConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/EqualConceptCondition.java index ca5dd2c6f4..8bc6702a83 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/EqualConceptCondition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/EqualConceptCondition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; import lombok.Setter; @@ -10,6 +12,7 @@ @Getter @Setter @Schema(name = "MetadataEqualConceptCondition", description = "Matches when a value is contained exactly in this list.") +@PolymorphicModelSubtype(base = ConceptCondition.class, id = "EQUAL") public final class EqualConceptCondition extends AbstractConceptCondition { @NotEmpty diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/NotConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/NotConceptCondition.java index cf96cc397b..d096e3aa35 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/NotConceptCondition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/NotConceptCondition.java @@ -1,6 +1,7 @@ package com.bakdata.conquery.quarkus.concepts.conditions.definitions; import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.Getter; @@ -10,6 +11,7 @@ @Getter @Setter @Schema(name = "MetadataNotConceptCondition", description = "Matches when its nested condition does not match.") +@PolymorphicModelSubtype(base = ConceptCondition.class, id = "NOT") public final class NotConceptCondition extends AbstractConceptCondition { @Valid diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/OrConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/OrConceptCondition.java index 205375476e..975003c66c 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/OrConceptCondition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/OrConceptCondition.java @@ -3,6 +3,7 @@ import java.util.List; import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; @@ -12,6 +13,7 @@ @Getter @Setter @Schema(name = "MetadataOrConceptCondition", description = "Matches when at least one nested condition matches.") +@PolymorphicModelSubtype(base = ConceptCondition.class, id = "OR") public final class OrConceptCondition extends AbstractConceptCondition { @Valid diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixListConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixListConceptCondition.java index 31b6079c38..9a100c7080 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixListConceptCondition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixListConceptCondition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; import lombok.Setter; @@ -10,6 +12,7 @@ @Getter @Setter @Schema(name = "MetadataPrefixListConceptCondition", description = "Matches values beginning with one of the configured prefixes.") +@PolymorphicModelSubtype(base = ConceptCondition.class, id = "PREFIX_LIST") public final class PrefixListConceptCondition extends AbstractConceptCondition { @NotEmpty diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixRangeConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixRangeConceptCondition.java index 117fe8a467..147303a2b9 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixRangeConceptCondition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PrefixRangeConceptCondition.java @@ -1,5 +1,7 @@ package com.bakdata.conquery.quarkus.concepts.conditions.definitions; +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotBlank; import lombok.Getter; @@ -9,6 +11,7 @@ @Getter @Setter @Schema(name = "MetadataPrefixRangeConceptCondition", description = "Matches fixed-length prefixes inside an inclusive lexical range.") +@PolymorphicModelSubtype(base = ConceptCondition.class, id = "PREFIX_RANGE") public final class PrefixRangeConceptCondition extends AbstractConceptCondition { @NotBlank diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PresentConceptCondition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PresentConceptCondition.java index f60f1f4fb5..99fdeb06d2 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PresentConceptCondition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/definitions/PresentConceptCondition.java @@ -1,5 +1,7 @@ package com.bakdata.conquery.quarkus.concepts.conditions.definitions; +import com.bakdata.conquery.quarkus.concepts.conditions.ConceptCondition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @@ -8,6 +10,7 @@ @Getter @Setter @Schema(name = "MetadataPresentConceptCondition", description = "Matches when the configured column in the same record/event has a value present.") +@PolymorphicModelSubtype(base = ConceptCondition.class, id = "PRESENT") public final class PresentConceptCondition extends AbstractConceptCondition { @NotBlank diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AbstractConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AbstractConceptConditionProvider.java index 76ebe73f40..a5bb96d458 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AbstractConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AbstractConceptConditionProvider.java @@ -5,14 +5,14 @@ abstract class AbstractConceptConditionProvider implements ConceptConditionProvider { - private final Class conditionType; + private final Class modelType; - protected AbstractConceptConditionProvider(Class conditionType) { - this.conditionType = conditionType; + protected AbstractConceptConditionProvider(Class modelType) { + this.modelType = modelType; } @Override - public Class conditionType() { - return conditionType; + public Class modelType() { + return modelType; } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AndConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AndConceptConditionProvider.java index 678d0371c5..c9c05ecdfd 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AndConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/AndConceptConditionProvider.java @@ -10,8 +10,4 @@ public AndConceptConditionProvider() { super(AndConceptCondition.class); } - @Override - public String type() { - return "AND"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/ColumnEqualConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/ColumnEqualConceptConditionProvider.java index b6ae5f3d35..307d424e5e 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/ColumnEqualConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/ColumnEqualConceptConditionProvider.java @@ -10,8 +10,4 @@ public ColumnEqualConceptConditionProvider() { super(ColumnEqualConceptCondition.class); } - @Override - public String type() { - return "COLUMN_EQUAL"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/EqualConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/EqualConceptConditionProvider.java index 00e6d6575b..dde4038ca4 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/EqualConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/EqualConceptConditionProvider.java @@ -10,8 +10,4 @@ public EqualConceptConditionProvider() { super(EqualConceptCondition.class); } - @Override - public String type() { - return "EQUAL"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/NotConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/NotConceptConditionProvider.java index c384c52ff7..5504e93dac 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/NotConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/NotConceptConditionProvider.java @@ -10,8 +10,4 @@ public NotConceptConditionProvider() { super(NotConceptCondition.class); } - @Override - public String type() { - return "NOT"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/OrConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/OrConceptConditionProvider.java index 15b8ba6415..2e02620e3a 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/OrConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/OrConceptConditionProvider.java @@ -10,8 +10,4 @@ public OrConceptConditionProvider() { super(OrConceptCondition.class); } - @Override - public String type() { - return "OR"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixListConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixListConceptConditionProvider.java index b772367d72..3d5835abbd 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixListConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixListConceptConditionProvider.java @@ -10,8 +10,4 @@ public PrefixListConceptConditionProvider() { super(PrefixListConceptCondition.class); } - @Override - public String type() { - return "PREFIX_LIST"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixRangeConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixRangeConceptConditionProvider.java index b6591d7674..c62075e151 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixRangeConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PrefixRangeConceptConditionProvider.java @@ -10,8 +10,4 @@ public PrefixRangeConceptConditionProvider() { super(PrefixRangeConceptCondition.class); } - @Override - public String type() { - return "PREFIX_RANGE"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PresentConceptConditionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PresentConceptConditionProvider.java index 672a693cd0..032a271e08 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PresentConceptConditionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/conditions/providers/PresentConceptConditionProvider.java @@ -10,8 +10,4 @@ public PresentConceptConditionProvider() { super(PresentConceptCondition.class); } - @Override - public String type() { - return "PRESENT"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionAssembler.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionAssembler.java index c383fd0448..ab3bd2cc1d 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionAssembler.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionAssembler.java @@ -66,7 +66,7 @@ private Optional assemble(FilterConversionConte } private DatasetCatalogRepository.Filter convert(FilterConversionContext context, FilterDefinition definition, FilterDefinitionProvider provider) { - T payload = provider.payloadType().cast(definition); + T payload = provider.modelType().cast(definition); DatasetCatalogRepository.Filter filter = provider.convert(context, payload); Set acceptedTypes = provider.acceptedValueTypes().stream() .map(filterValueRegistry::require) diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProvider.java index 881a1da51e..e0f4f279bc 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionProvider.java @@ -3,31 +3,53 @@ import java.util.Set; import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; -import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; import com.bakdata.conquery.quarkus.models.PolymorphicModelTypeProvider; +import com.bakdata.conquery.quarkus.storage.DatasetCatalogRepository; +/** + * Registers and assembles one polymorphic metadata filter definition. + *

+ * Implementations are CDI beans. The polymorphic model registry uses {@link #modelType()} and its + * {@code @PolymorphicModelSubtype} annotation to + * configure Jackson and OpenAPI, while the metadata loader uses {@link #convert(FilterConversionContext, + * FilterDefinition)} to turn the file-facing definition into the catalog model exposed to the frontend. + * + * @param concrete filter definition handled by this provider + */ public interface FilterDefinitionProvider extends PolymorphicModelTypeProvider { - String type(); - - Class payloadType(); + /** + * @return discriminator used in metadata JSON to select this provider + */ + default String type() { + return typeId(); + } + /** + * Declares every query value model that filters produced by this provider may accept. All returned classes must + * have a registered filter value provider. The assembler verifies that the serialized type of the converted filter + * belongs to this set. + * + * @return non-empty set of accepted filter value model classes + */ Set> acceptedValueTypes(); + /** + * Converts a deserialized metadata definition into the catalog filter consumed by the API and frontend. + *

+ * The payload has already been selected by its discriminator and cast to {@link #modelType()}. Metadata bean + * validation is performed before assembly. Implementations remain responsible for semantic conversion, including + * resolving local column names and constructing hierarchical IDs through the supplied context. + *

+ * The returned filter must be complete and non-null. Its serialized filter value type must correspond to a + * registered filter value provider for one of {@link #acceptedValueTypes()}; violating this invariant aborts metadata + * loading. Invalid references or unsupported metadata should likewise be reported by throwing an exception with + * enough context to identify the definition. + * + * @param context connector, table, column-resolution, and ID-fallback context for this definition + * @param payload concrete file-facing definition to convert + * @return fully assembled catalog filter + */ DatasetCatalogRepository.Filter convert(FilterConversionContext context, T payload); - @Override - default Class baseType() { - return FilterDefinition.class; - } - - @Override - default String typeId() { - return type(); - } - - @Override - default Class modelType() { - return payloadType(); - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionRegistry.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionRegistry.java index d5bc43f590..eb0d3987e8 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionRegistry.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/FilterDefinitionRegistry.java @@ -29,9 +29,9 @@ void init() { throw new IllegalStateException("Filter provider " + provider.getClass().getName() + " does not declare any accepted filter value types"); } provider.acceptedValueTypes().forEach(filterValueRegistry::require); - FilterDefinitionProvider previous = index.put(provider.payloadType(), provider); + FilterDefinitionProvider previous = index.put(provider.modelType(), provider); if (previous != null) { - throw new IllegalStateException("Duplicate filter provider for model type " + provider.payloadType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); + throw new IllegalStateException("Duplicate filter provider for model type " + provider.modelType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); } } providersByModelType = Map.copyOf(index); diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/BigMultiSelectFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/BigMultiSelectFilterDefinition.java index 8f85e01d60..2bc4c3d8d3 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/BigMultiSelectFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/BigMultiSelectFilterDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.filters.definitions; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataBigMultiSelectFilter", description = "Selection filter whose values are fetched by the frontend.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "BIG_MULTI_SELECT") public final class BigMultiSelectFilterDefinition extends SelectFilterDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CategoryMaxSumFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CategoryMaxSumFilterDefinition.java index a2b6ce5217..00c1c7cdf6 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CategoryMaxSumFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CategoryMaxSumFilterDefinition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import com.fasterxml.jackson.annotation.JsonAlias; import lombok.Getter; import lombok.Setter; @@ -11,6 +13,7 @@ @Setter @Schema(name = "MetadataCategoryMaxSumFilter", description = "Numeric range filter over category-wise maximum sums.") // TODO May rename this to DistincMaxSumFilter +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "CATEGORY_MAX_SUM") public final class CategoryMaxSumFilterDefinition extends AbstractFilterDefinition { @Schema(description = "Category column.") @JsonAlias("category") diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CountFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CountFilterDefinition.java index e194a6d29a..8ec2c5dd9b 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CountFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CountFilterDefinition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import lombok.Getter; import lombok.Setter; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -9,6 +11,7 @@ @Getter @Setter @Schema(name = "MetadataCountFilter", description = "Range filter over an event or distinct-value count.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "COUNT") public final class CountFilterDefinition extends SingleColumnFilterDefinition { @Schema(description = "Optional local columns whose value combinations are counted distinctly.") private List distinctByColumn; diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CountQuartersFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CountQuartersFilterDefinition.java index 19e8cc26cc..572ebcfd2b 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CountQuartersFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/CountQuartersFilterDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.filters.definitions; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataCountQuartersFilter", description = "Range filter over the number of quarters covered by a date range.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "COUNT_QUARTERS") public final class CountQuartersFilterDefinition extends SingleColumnFilterDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/DateDistanceFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/DateDistanceFilterDefinition.java index a11799fbb2..df64e5bcfb 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/DateDistanceFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/DateDistanceFilterDefinition.java @@ -1,5 +1,7 @@ package com.bakdata.conquery.quarkus.concepts.filters.definitions; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import lombok.Getter; import lombok.Setter; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -7,6 +9,7 @@ @Getter @Setter @Schema(name = "MetadataDateDistanceFilter", description = "Range filter over the distance between a date and the reference date.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "DATE_DISTANCE") public final class DateDistanceFilterDefinition extends SingleColumnFilterDefinition { @Schema(description = "Unit used to measure the date distance, for example YEARS or DAYS.", defaultValue = "YEARS") private String timeUnit; diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/DurationSumFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/DurationSumFilterDefinition.java index ac668703da..1c43535674 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/DurationSumFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/DurationSumFilterDefinition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import lombok.Getter; import lombok.Setter; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -9,6 +11,7 @@ @Getter @Setter @Schema(name = "MetadataDurationSumFilter", description = "Range filter over summed date-range durations.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "DURATION_SUM") public final class DurationSumFilterDefinition extends SingleColumnFilterDefinition { @Schema(description = "Local columns used to remove duplicate date ranges before summing durations.") private List distinctBy; diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/FlagsFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/FlagsFilterDefinition.java index 013a5f0b6a..365867f726 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/FlagsFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/FlagsFilterDefinition.java @@ -2,6 +2,8 @@ import java.util.Map; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; import lombok.Setter; @@ -10,6 +12,7 @@ @Getter @Setter @Schema(name = "MetadataFlagsFilter", description = "Multi-selection filter backed by one boolean column per option.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "FLAGS") public final class FlagsFilterDefinition extends AbstractFilterDefinition { @NotEmpty @Schema(description = "Mapping from selectable labels to local boolean columns.", required = true) diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/MultiSelectFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/MultiSelectFilterDefinition.java index 60d560c7db..1e15382bb1 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/MultiSelectFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/MultiSelectFilterDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.filters.definitions; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataMultiSelectFilter", description = "Selection filter whose values may be embedded in the concept metadata.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "SELECT") public final class MultiSelectFilterDefinition extends SelectFilterDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/NumberFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/NumberFilterDefinition.java index 4afdc96607..6d41eea6dd 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/NumberFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/NumberFilterDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.filters.definitions; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataNumberFilter", description = "Numeric range filter over one column.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "NUMBER") public final class NumberFilterDefinition extends SingleColumnFilterDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/PrefixTextFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/PrefixTextFilterDefinition.java index 6febaeb1c4..730716dae0 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/PrefixTextFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/PrefixTextFilterDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.filters.definitions; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataPrefixTextFilter", description = "Text-prefix filter over one string column.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "PREFIX_TEXT") public final class PrefixTextFilterDefinition extends SingleColumnFilterDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/QuartersInYearFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/QuartersInYearFilterDefinition.java index c6b0fc133a..98479e1cc4 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/QuartersInYearFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/QuartersInYearFilterDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.filters.definitions; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataQuartersInYearFilter", description = "Range filter over the quarter of a date.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "QUARTERS_IN_YEAR") public final class QuartersInYearFilterDefinition extends SingleColumnFilterDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/SingleSelectFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/SingleSelectFilterDefinition.java index ba85ff1ee0..1c5acb0be5 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/SingleSelectFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/SingleSelectFilterDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.filters.definitions; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataSingleSelectFilter", description = "Selection filter accepting one value.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "SINGLE_SELECT") public final class SingleSelectFilterDefinition extends SelectFilterDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/SumFilterDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/SumFilterDefinition.java index 15941d5039..ba2a7e1293 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/SumFilterDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/definitions/SumFilterDefinition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.filters.FilterDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import lombok.Getter; import lombok.Setter; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -9,6 +11,7 @@ @Getter @Setter @Schema(name = "MetadataSumFilter", description = "Numeric range filter over a sum or difference of sums.") +@PolymorphicModelSubtype(base = FilterDefinition.class, id = "SUM") public final class SumFilterDefinition extends SingleColumnFilterDefinition { @Schema(description = "Optional local column subtracted from the primary sum column.") private String subtractColumn; diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java index 644905b2ff..2c25b0e140 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/AbstractFilterProvider.java @@ -23,21 +23,21 @@ abstract class AbstractFilterProvider implements FilterDefinitionProvider { - private final Class payloadType; + private final Class modelType; private final Set> acceptedValueTypes; @Inject FilterValueRegistry filterValueRegistry; @SafeVarargs - protected AbstractFilterProvider(Class payloadType, Class... acceptedValueTypes) { - this.payloadType = payloadType; + protected AbstractFilterProvider(Class modelType, Class... acceptedValueTypes) { + this.modelType = modelType; this.acceptedValueTypes = Set.of(acceptedValueTypes); } @Override - public Class payloadType() { - return payloadType; + public Class modelType() { + return modelType; } @Override diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java index a1a89f65dd..4db59cb62e 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/BigMultiSelectFilterProvider.java @@ -13,10 +13,6 @@ public class BigMultiSelectFilterProvider extends AbstractFilterProvider columns, String value) { - if (value != null && !value.isBlank()) { - columns.add(context.columnId(value)); - } + return filter(context, payload, numericRangeValueType(context, valueColumnId), null, null, false, required); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java index 7a90be0206..efb1ba6935 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/providers/CountFilterProvider.java @@ -12,10 +12,6 @@ public class CountFilterProvider extends AbstractFilterProvider extends PolymorphicModelTypeProvider { - String type(); - - Class payloadType(); - - @Override - default Class baseType() { - return FilterValue.class; - } - - @Override - default String typeId() { - return type(); - } - - @Override - default Class modelType() { - return payloadType(); + default String type() { + return typeId(); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueRegistry.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueRegistry.java index ef47065c86..b0ed5aba1d 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueRegistry.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/FilterValueRegistry.java @@ -20,9 +20,9 @@ public class FilterValueRegistry { void init() { Map, FilterValueProvider> index = new LinkedHashMap<>(); for (FilterValueProvider provider : providers) { - FilterValueProvider previous = index.put(provider.payloadType(), provider); + FilterValueProvider previous = index.put(provider.modelType(), provider); if (previous != null) { - throw new IllegalStateException("Duplicate filter value provider for model type " + provider.payloadType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); + throw new IllegalStateException("Duplicate filter value provider for model type " + provider.modelType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); } } providersByModelType = Map.copyOf(index); diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/BigMultiSelectFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/BigMultiSelectFilterValue.java index c5fa6cc06f..fc8ba94c45 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/BigMultiSelectFilterValue.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/BigMultiSelectFilterValue.java @@ -4,10 +4,12 @@ import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "QueryBigMultiSelectFilterValue", description = "A potentially large set of selected filter values.") +@PolymorphicModelSubtype(base = FilterValue.class, id = "BIG_MULTI_SELECT") public record BigMultiSelectFilterValue(@NotNull FilterId filter, @NotNull @NotEmpty Set<@NotNull String> value) implements FilterValue { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterValue.java index a632b60be5..08c30d679c 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterValue.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerFilterValue.java @@ -2,9 +2,11 @@ import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "QueryIntegerFilterValue", description = "A single integer filter value.") +@PolymorphicModelSubtype(base = FilterValue.class, id = "INTEGER") public record IntegerFilterValue(@NotNull FilterId filter, @NotNull Long value) implements FilterValue { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerRangeFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerRangeFilterValue.java index b7b7a94054..6028640cc2 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerRangeFilterValue.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/IntegerRangeFilterValue.java @@ -2,10 +2,12 @@ import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "QueryIntegerRangeFilterValue", description = "An integer range filter value.") +@PolymorphicModelSubtype(base = FilterValue.class, id = "INTEGER_RANGE") public record IntegerRangeFilterValue(@NotNull FilterId filter, @Valid @NotNull IntegerFilterRange value) implements FilterValue { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MoneyRangeFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MoneyRangeFilterValue.java index b64097041b..e6266ebeec 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MoneyRangeFilterValue.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MoneyRangeFilterValue.java @@ -2,10 +2,12 @@ import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "QueryMoneyRangeFilterValue", description = "A monetary range represented in the configured minor currency unit.") +@PolymorphicModelSubtype(base = FilterValue.class, id = "MONEY_RANGE") public record MoneyRangeFilterValue(@NotNull FilterId filter, @Valid @NotNull IntegerFilterRange value) implements FilterValue { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MultiSelectFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MultiSelectFilterValue.java index e91977833c..83f48b9559 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MultiSelectFilterValue.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/MultiSelectFilterValue.java @@ -4,10 +4,12 @@ import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "QueryMultiSelectFilterValue", description = "A set of selected filter values.") +@PolymorphicModelSubtype(base = FilterValue.class, id = "MULTI_SELECT") public record MultiSelectFilterValue(@NotNull FilterId filter, @NotNull @NotEmpty Set<@NotNull String> value) implements FilterValue { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterValue.java index 4d21a7eec3..c12f553e0c 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterValue.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealFilterValue.java @@ -4,9 +4,11 @@ import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "QueryRealFilterValue", description = "A single real-number filter value.") +@PolymorphicModelSubtype(base = FilterValue.class, id = "REAL") public record RealFilterValue(@NotNull FilterId filter, @NotNull BigDecimal value) implements FilterValue { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealRangeFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealRangeFilterValue.java index c2410c25e8..62134d2a3c 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealRangeFilterValue.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/RealRangeFilterValue.java @@ -2,10 +2,12 @@ import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "QueryRealRangeFilterValue", description = "A real-number range filter value.") +@PolymorphicModelSubtype(base = FilterValue.class, id = "REAL_RANGE") public record RealRangeFilterValue(@NotNull FilterId filter, @Valid @NotNull RealFilterRange value) implements FilterValue { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/SelectFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/SelectFilterValue.java index 105848aa50..1a8db9daf0 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/SelectFilterValue.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/SelectFilterValue.java @@ -2,9 +2,11 @@ import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "QuerySelectFilterValue", description = "A single selected filter value.") +@PolymorphicModelSubtype(base = FilterValue.class, id = "SELECT") public record SelectFilterValue(@NotNull FilterId filter, @NotNull String value) implements FilterValue { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/StringFilterValue.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/StringFilterValue.java index c2f5c88074..86d185437b 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/StringFilterValue.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/definitions/StringFilterValue.java @@ -2,9 +2,11 @@ import com.bakdata.conquery.quarkus.concepts.filters.values.FilterValue; import com.bakdata.conquery.quarkus.ids.FilterId; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "QueryStringFilterValue", description = "A free-text filter value.") +@PolymorphicModelSubtype(base = FilterValue.class, id = "STRING") public record StringFilterValue(@NotNull FilterId filter, @NotNull String value) implements FilterValue { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/AbstractFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/AbstractFilterValueProvider.java index d3029a9a40..8d7c191c69 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/AbstractFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/AbstractFilterValueProvider.java @@ -5,14 +5,14 @@ abstract class AbstractFilterValueProvider implements FilterValueProvider { - private final Class payloadType; + private final Class modelType; - protected AbstractFilterValueProvider(Class payloadType) { - this.payloadType = payloadType; + protected AbstractFilterValueProvider(Class modelType) { + this.modelType = modelType; } @Override - public Class payloadType() { - return payloadType; + public Class modelType() { + return modelType; } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/BigMultiSelectFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/BigMultiSelectFilterValueProvider.java index ac651c2743..37f0196cd7 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/BigMultiSelectFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/BigMultiSelectFilterValueProvider.java @@ -9,8 +9,4 @@ public BigMultiSelectFilterValueProvider() { super(BigMultiSelectFilterValue.class); } - @Override - public String type() { - return "BIG_MULTI_SELECT"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerFilterValueProvider.java index 3acbc817c4..7e6b6dca00 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerFilterValueProvider.java @@ -9,8 +9,4 @@ public IntegerFilterValueProvider() { super(IntegerFilterValue.class); } - @Override - public String type() { - return "INTEGER"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerRangeFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerRangeFilterValueProvider.java index 1b3fd64679..b580b0079b 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerRangeFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/IntegerRangeFilterValueProvider.java @@ -9,8 +9,4 @@ public IntegerRangeFilterValueProvider() { super(IntegerRangeFilterValue.class); } - @Override - public String type() { - return "INTEGER_RANGE"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MoneyRangeFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MoneyRangeFilterValueProvider.java index bd73809b80..49a08be1a1 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MoneyRangeFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MoneyRangeFilterValueProvider.java @@ -9,8 +9,4 @@ public MoneyRangeFilterValueProvider() { super(MoneyRangeFilterValue.class); } - @Override - public String type() { - return "MONEY_RANGE"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MultiSelectFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MultiSelectFilterValueProvider.java index e1d824eaf5..0e62fc26a9 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MultiSelectFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/MultiSelectFilterValueProvider.java @@ -9,8 +9,4 @@ public MultiSelectFilterValueProvider() { super(MultiSelectFilterValue.class); } - @Override - public String type() { - return "MULTI_SELECT"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealFilterValueProvider.java index 2d809b60ec..3841338f1b 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealFilterValueProvider.java @@ -9,8 +9,4 @@ public RealFilterValueProvider() { super(RealFilterValue.class); } - @Override - public String type() { - return "REAL"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealRangeFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealRangeFilterValueProvider.java index 3d44335639..0df564b13a 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealRangeFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/RealRangeFilterValueProvider.java @@ -9,8 +9,4 @@ public RealRangeFilterValueProvider() { super(RealRangeFilterValue.class); } - @Override - public String type() { - return "REAL_RANGE"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/SelectFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/SelectFilterValueProvider.java index d4240e3357..ce191b2019 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/SelectFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/SelectFilterValueProvider.java @@ -10,8 +10,4 @@ public SelectFilterValueProvider() { super(SelectFilterValue.class); } - @Override - public String type() { - return "SELECT"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/StringFilterValueProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/StringFilterValueProvider.java index 747680d114..3b08560280 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/StringFilterValueProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/filters/values/providers/StringFilterValueProvider.java @@ -9,8 +9,4 @@ public StringFilterValueProvider() { super(StringFilterValue.class); } - @Override - public String type() { - return "STRING"; - } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionAssembler.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionAssembler.java index c6d99c84e2..8ab4220488 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionAssembler.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionAssembler.java @@ -57,6 +57,6 @@ private Optional assemble(SelectConversionConte } private DatasetCatalogRepository.Select convert(SelectConversionContext context, SelectDefinition definition, SelectDefinitionProvider provider) { - return provider.convert(context, provider.payloadType().cast(definition)); + return provider.convert(context, provider.modelType().cast(definition)); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionProvider.java index 4b0da58048..fb6d29379d 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionProvider.java @@ -5,24 +5,9 @@ public interface SelectDefinitionProvider extends PolymorphicModelTypeProvider { - String type(); - - Class payloadType(); - DatasetCatalogRepository.Select convert(SelectConversionContext context, T payload); - @Override - default Class baseType() { - return SelectDefinition.class; - } - - @Override - default String typeId() { - return type(); - } - - @Override - default Class modelType() { - return payloadType(); + default String type() { + return typeId(); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionRegistry.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionRegistry.java index 5081be2d99..e38e4039fc 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionRegistry.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/SelectDefinitionRegistry.java @@ -21,9 +21,9 @@ public class SelectDefinitionRegistry { void init() { Map, SelectDefinitionProvider> index = new LinkedHashMap<>(); for (SelectDefinitionProvider provider : providers) { - SelectDefinitionProvider previous = index.put(provider.payloadType(), provider); + SelectDefinitionProvider previous = index.put(provider.modelType(), provider); if (previous != null) { - throw new IllegalStateException("Duplicate select provider for model type " + provider.payloadType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); + throw new IllegalStateException("Duplicate select provider for model type " + provider.modelType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); } } providersByModelType = Map.copyOf(index); diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionAssembler.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionAssembler.java index 52c17ccd5e..d915a3371a 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionAssembler.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionAssembler.java @@ -54,6 +54,6 @@ private Optional assemble(ConceptSelectC } private DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, ConceptSelectDefinition definition, ConceptSelectDefinitionProvider provider) { - return provider.convert(context, provider.payloadType().cast(definition)); + return provider.convert(context, provider.modelType().cast(definition)); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionProvider.java index 05bf3ed7bd..9ff6e53781 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionProvider.java @@ -5,24 +5,9 @@ public interface ConceptSelectDefinitionProvider extends PolymorphicModelTypeProvider { - String type(); - - Class payloadType(); - DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, T payload); - @Override - default Class baseType() { - return ConceptSelectDefinition.class; - } - - @Override - default String typeId() { - return type(); - } - - @Override - default Class modelType() { - return payloadType(); + default String type() { + return typeId(); } } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionRegistry.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionRegistry.java index dad1d3233d..1c1d7a8f32 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionRegistry.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/ConceptSelectDefinitionRegistry.java @@ -21,9 +21,9 @@ public class ConceptSelectDefinitionRegistry { void init() { Map, ConceptSelectDefinitionProvider> index = new LinkedHashMap<>(); for (ConceptSelectDefinitionProvider provider : providers) { - ConceptSelectDefinitionProvider previous = index.put(provider.payloadType(), provider); + ConceptSelectDefinitionProvider previous = index.put(provider.modelType(), provider); if (previous != null) { - throw new IllegalStateException("Duplicate concept select provider for model type " + provider.payloadType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); + throw new IllegalStateException("Duplicate concept select provider for model type " + provider.modelType().getName() + ": " + previous.getClass().getName() + " and " + provider.getClass().getName()); } } providersByModelType = Map.copyOf(index); diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ConceptValuesConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ConceptValuesConceptSelectDefinition.java index 03da2f540c..4f0ec2299d 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ConceptValuesConceptSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ConceptValuesConceptSelectDefinition.java @@ -1,5 +1,7 @@ package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import lombok.Getter; import lombok.Setter; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -7,6 +9,7 @@ @Getter @Setter @Schema(name = "MetadataConceptValuesConceptSelect", description = "Returns values of matching concept nodes.") +@PolymorphicModelSubtype(base = ConceptSelectDefinition.class, id = "CONCEPT_VALUES") public final class ConceptValuesConceptSelectDefinition extends AbstractConceptSelectDefinition { @Schema(description = "Whether concept ids are returned instead of labels.") diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDateUnionConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDateUnionConceptSelectDefinition.java index d8dd842c89..878bbc6873 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDateUnionConceptSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDateUnionConceptSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataEventDateUnionConceptSelect", description = "Returns the union of matching event dates.") +@PolymorphicModelSubtype(base = ConceptSelectDefinition.class, id = "EVENT_DATE_UNION") public final class EventDateUnionConceptSelectDefinition extends AbstractConceptSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDurationSumConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDurationSumConceptSelectDefinition.java index 4cedbe32b5..bb00a397bf 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDurationSumConceptSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/EventDurationSumConceptSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataEventDurationSumConceptSelect", description = "Returns the total duration of matching events.") +@PolymorphicModelSubtype(base = ConceptSelectDefinition.class, id = "EVENT_DURATION_SUM") public final class EventDurationSumConceptSelectDefinition extends AbstractConceptSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ExistsConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ExistsConceptSelectDefinition.java index 99e48d4cac..2bac963a1e 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ExistsConceptSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/ExistsConceptSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataExistsConceptSelect", description = "Returns whether the concept has any matching event.") +@PolymorphicModelSubtype(base = ConceptSelectDefinition.class, id = "EXISTS") public final class ExistsConceptSelectDefinition extends AbstractConceptSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/QuarterConceptSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/QuarterConceptSelectDefinition.java index f394b4e38a..faf4208b81 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/QuarterConceptSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/definitions/QuarterConceptSelectDefinition.java @@ -1,5 +1,7 @@ package com.bakdata.conquery.quarkus.concepts.selects.concept.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.concept.ConceptSelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; @@ -8,6 +10,7 @@ @Getter @Setter @Schema(name = "MetadataQuarterConceptSelect", description = "Returns the quarter containing a sampled event date.") +@PolymorphicModelSubtype(base = ConceptSelectDefinition.class, id = "QUARTER") public final class QuarterConceptSelectDefinition extends AbstractConceptSelectDefinition { @NotNull diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/AbstractConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/AbstractConceptSelectProvider.java index 302a0709c3..3181da091a 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/AbstractConceptSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/AbstractConceptSelectProvider.java @@ -10,15 +10,15 @@ abstract class AbstractConceptSelectProvider implements ConceptSelectDefinitionProvider { - private final Class payloadType; + private final Class modelType; - protected AbstractConceptSelectProvider(Class payloadType) { - this.payloadType = payloadType; + protected AbstractConceptSelectProvider(Class modelType) { + this.modelType = modelType; } @Override - public Class payloadType() { - return payloadType; + public Class modelType() { + return modelType; } protected DatasetCatalogRepository.ConceptSelect select(ConceptSelectConversionContext context, T payload, DatasetCatalogRepository.SelectResultType resultType) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ConceptValuesConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ConceptValuesConceptSelectProvider.java index 81a3eea746..dc2060d137 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ConceptValuesConceptSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ConceptValuesConceptSelectProvider.java @@ -12,10 +12,6 @@ public ConceptValuesConceptSelectProvider() { super(ConceptValuesConceptSelectDefinition.class); } - @Override - public String type() { - return "CONCEPT_VALUES"; - } @Override public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, ConceptValuesConceptSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDateUnionConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDateUnionConceptSelectProvider.java index 07e9fe2b1b..57d573f8e4 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDateUnionConceptSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDateUnionConceptSelectProvider.java @@ -12,10 +12,6 @@ public EventDateUnionConceptSelectProvider() { super(EventDateUnionConceptSelectDefinition.class); } - @Override - public String type() { - return "EVENT_DATE_UNION"; - } @Override public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, EventDateUnionConceptSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDurationSumConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDurationSumConceptSelectProvider.java index 1d8f58c48d..1faaf59291 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDurationSumConceptSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/EventDurationSumConceptSelectProvider.java @@ -12,10 +12,6 @@ public EventDurationSumConceptSelectProvider() { super(EventDurationSumConceptSelectDefinition.class); } - @Override - public String type() { - return "EVENT_DURATION_SUM"; - } @Override public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, EventDurationSumConceptSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ExistsConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ExistsConceptSelectProvider.java index dd3e1b4e52..78ee8b9876 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ExistsConceptSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/ExistsConceptSelectProvider.java @@ -12,10 +12,6 @@ public ExistsConceptSelectProvider() { super(ExistsConceptSelectDefinition.class); } - @Override - public String type() { - return "EXISTS"; - } @Override public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, ExistsConceptSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/QuarterConceptSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/QuarterConceptSelectProvider.java index a76a592de1..74ee592184 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/QuarterConceptSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/concept/providers/QuarterConceptSelectProvider.java @@ -12,10 +12,6 @@ public QuarterConceptSelectProvider() { super(QuarterConceptSelectDefinition.class); } - @Override - public String type() { - return "QUARTER"; - } @Override public DatasetCatalogRepository.ConceptSelect convert(ConceptSelectConversionContext context, QuarterConceptSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/CountQuartersSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/CountQuartersSelectDefinition.java index 060a3c85d5..fe6d0276dd 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/CountQuartersSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/CountQuartersSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataCountQuartersSelect", description = "Counts distinct quarters covered by dates or date ranges.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "COUNT_QUARTERS") public final class CountQuartersSelectDefinition extends DateRangeSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/CountSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/CountSelectDefinition.java index 0d40418122..e46c484872 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/CountSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/CountSelectDefinition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @@ -10,6 +12,7 @@ @Getter @Setter @Schema(name = "MetadataCountSelect", description = "Counts matching values or events.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "COUNT") public final class CountSelectDefinition extends AbstractSelectDefinition { private boolean distinct; private List distinctByColumn; diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DateDistanceSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DateDistanceSelectDefinition.java index 5553d5ef4b..4f11a5fb25 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DateDistanceSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DateDistanceSelectDefinition.java @@ -2,6 +2,8 @@ import java.time.temporal.ChronoUnit; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import lombok.Getter; import lombok.Setter; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -9,6 +11,7 @@ @Getter @Setter @Schema(name = "MetadataDateDistanceSelect", description = "Calculates a date distance in the configured unit.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "DATE_DISTANCE") public final class DateDistanceSelectDefinition extends SingleColumnSelectDefinition { private ChronoUnit timeUnit = ChronoUnit.YEARS; } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DateUnionSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DateUnionSelectDefinition.java index eba17231da..076df3581b 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DateUnionSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DateUnionSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataDateUnionSelect", description = "Returns the union of selected date ranges.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "DATE_UNION") public final class DateUnionSelectDefinition extends DateRangeSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DistinctSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DistinctSelectDefinition.java index 074d1a2dc2..440576856a 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DistinctSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DistinctSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataDistinctSelect", description = "Selects all distinct values of a column.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "DISTINCT") public final class DistinctSelectDefinition extends MappableSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DurationSumSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DurationSumSelectDefinition.java index db4432c446..0380de0b65 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DurationSumSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/DurationSumSelectDefinition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import lombok.Getter; import lombok.Setter; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -9,6 +11,7 @@ @Getter @Setter @Schema(name = "MetadataDurationSumSelect", description = "Sums durations of selected date ranges.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "DURATION_SUM") public final class DurationSumSelectDefinition extends DateRangeSelectDefinition { private List distinctBy; } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/FirstSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/FirstSelectDefinition.java index f5dd79d589..7c663ba76c 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/FirstSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/FirstSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataFirstSelect", description = "Selects the first value of a column.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "FIRST") public final class FirstSelectDefinition extends MappableSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/FlagsSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/FlagsSelectDefinition.java index 48ca75638f..d63d7e109c 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/FlagsSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/FlagsSelectDefinition.java @@ -2,6 +2,8 @@ import java.util.Map; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; import lombok.Setter; @@ -10,6 +12,7 @@ @Getter @Setter @Schema(name = "MetadataFlagsSelect", description = "Returns labels for matching boolean flag columns.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "FLAGS") public final class FlagsSelectDefinition extends AbstractSelectDefinition { @NotEmpty private Map flags; diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/LastSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/LastSelectDefinition.java index 8e712bd8a8..f7eea6c21a 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/LastSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/LastSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataLastSelect", description = "Selects the last value of a column.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "LAST") public final class LastSelectDefinition extends MappableSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/PrefixSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/PrefixSelectDefinition.java index 5b4eaadf44..4500bbbe13 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/PrefixSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/PrefixSelectDefinition.java @@ -1,5 +1,7 @@ package com.bakdata.conquery.quarkus.concepts.selects.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; @@ -8,6 +10,7 @@ @Getter @Setter @Schema(name = "MetadataPrefixSelect", description = "Returns values prefixed with a configured string.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "PREFIX") public final class PrefixSelectDefinition extends SingleColumnSelectDefinition { @NotNull private String prefix; diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/QuartersInYearSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/QuartersInYearSelectDefinition.java index e6ae6cd27e..28eaa75775 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/QuartersInYearSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/QuartersInYearSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataQuartersInYearSelect", description = "Counts quarters with matching events per year.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "QUARTERS_IN_YEAR") public final class QuartersInYearSelectDefinition extends SingleColumnSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/RandomSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/RandomSelectDefinition.java index dc9231767d..8379f5f3a2 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/RandomSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/RandomSelectDefinition.java @@ -1,7 +1,10 @@ package com.bakdata.conquery.quarkus.concepts.selects.definitions; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "MetadataRandomSelect", description = "Selects one value of a column.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "RANDOM") public final class RandomSelectDefinition extends MappableSelectDefinition { } diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/SumSelectDefinition.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/SumSelectDefinition.java index a14929f512..359a518d61 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/SumSelectDefinition.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/definitions/SumSelectDefinition.java @@ -2,6 +2,8 @@ import java.util.List; +import com.bakdata.conquery.quarkus.concepts.selects.SelectDefinition; +import com.bakdata.conquery.quarkus.models.PolymorphicModelSubtype; import lombok.Getter; import lombok.Setter; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -9,6 +11,7 @@ @Getter @Setter @Schema(name = "MetadataSumSelect", description = "Sums a numeric column, optionally subtracting another column.") +@PolymorphicModelSubtype(base = SelectDefinition.class, id = "SUM") public final class SumSelectDefinition extends SingleColumnSelectDefinition { private String subtractColumn; private List distinctByColumn; diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSelectProvider.java index c6654e56f0..2359aaaacd 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/AbstractSelectProvider.java @@ -15,15 +15,15 @@ abstract class AbstractSelectProvider implements SelectDefinitionProvider { - private final Class payloadType; + private final Class modelType; - protected AbstractSelectProvider(Class payloadType) { - this.payloadType = payloadType; + protected AbstractSelectProvider(Class modelType) { + this.modelType = modelType; } @Override - public Class payloadType() { - return payloadType; + public Class modelType() { + return modelType; } protected DatasetCatalogRepository.Select select( diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountQuartersSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountQuartersSelectProvider.java index ddb36d2cd3..fdb89e6061 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountQuartersSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountQuartersSelectProvider.java @@ -15,10 +15,6 @@ public CountQuartersSelectProvider() { super(CountQuartersSelectDefinition.class); } - @Override - public String type() { - return "COUNT_QUARTERS"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, CountQuartersSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountSelectProvider.java index f2ac8802b2..9600ac0a6b 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/CountSelectProvider.java @@ -16,10 +16,6 @@ public CountSelectProvider() { super(CountSelectDefinition.class); } - @Override - public String type() { - return "COUNT"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, CountSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateDistanceSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateDistanceSelectProvider.java index d4659382fc..3a8e27db51 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateDistanceSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateDistanceSelectProvider.java @@ -15,10 +15,6 @@ public DateDistanceSelectProvider() { super(DateDistanceSelectDefinition.class); } - @Override - public String type() { - return "DATE_DISTANCE"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, DateDistanceSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateUnionSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateUnionSelectProvider.java index ac18109fa8..444a877d34 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateUnionSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DateUnionSelectProvider.java @@ -15,10 +15,6 @@ public DateUnionSelectProvider() { super(DateUnionSelectDefinition.class); } - @Override - public String type() { - return "DATE_UNION"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, DateUnionSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DistinctSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DistinctSelectProvider.java index 4af8a1704e..a1df976882 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DistinctSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DistinctSelectProvider.java @@ -15,10 +15,6 @@ public DistinctSelectProvider() { super(DistinctSelectDefinition.class); } - @Override - public String type() { - return "DISTINCT"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, DistinctSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DurationSumSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DurationSumSelectProvider.java index f0e5ef224f..0ac3961060 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DurationSumSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/DurationSumSelectProvider.java @@ -15,10 +15,6 @@ public DurationSumSelectProvider() { super(DurationSumSelectDefinition.class); } - @Override - public String type() { - return "DURATION_SUM"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, DurationSumSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FirstSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FirstSelectProvider.java index fd02809741..21369a7fd3 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FirstSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FirstSelectProvider.java @@ -12,10 +12,6 @@ public FirstSelectProvider() { super(FirstSelectDefinition.class); } - @Override - public String type() { - return "FIRST"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, FirstSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FlagsSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FlagsSelectProvider.java index e1c0a9e449..0fbb61c156 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FlagsSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/FlagsSelectProvider.java @@ -15,10 +15,6 @@ public FlagsSelectProvider() { super(FlagsSelectDefinition.class); } - @Override - public String type() { - return "FLAGS"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, FlagsSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/LastSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/LastSelectProvider.java index 7eea1514ce..6959ef10d9 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/LastSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/LastSelectProvider.java @@ -12,10 +12,6 @@ public LastSelectProvider() { super(LastSelectDefinition.class); } - @Override - public String type() { - return "LAST"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, LastSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/PrefixSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/PrefixSelectProvider.java index fe87c0e820..8e1ef038bc 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/PrefixSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/PrefixSelectProvider.java @@ -15,10 +15,6 @@ public PrefixSelectProvider() { super(PrefixSelectDefinition.class); } - @Override - public String type() { - return "PREFIX"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, PrefixSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/QuartersInYearSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/QuartersInYearSelectProvider.java index a8c54715fb..dd5a5709cd 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/QuartersInYearSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/QuartersInYearSelectProvider.java @@ -15,10 +15,6 @@ public QuartersInYearSelectProvider() { super(QuartersInYearSelectDefinition.class); } - @Override - public String type() { - return "QUARTERS_IN_YEAR"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, QuartersInYearSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/RandomSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/RandomSelectProvider.java index f8e023fddb..5d07d148dc 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/RandomSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/RandomSelectProvider.java @@ -12,10 +12,6 @@ public RandomSelectProvider() { super(RandomSelectDefinition.class); } - @Override - public String type() { - return "RANDOM"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, RandomSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/SumSelectProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/SumSelectProvider.java index 970dbf91af..0cc45936e0 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/SumSelectProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/concepts/selects/providers/SumSelectProvider.java @@ -16,10 +16,6 @@ public SumSelectProvider() { super(SumSelectDefinition.class); } - @Override - public String type() { - return "SUM"; - } @Override public DatasetCatalogRepository.Select convert(SelectConversionContext context, SumSelectDefinition payload) { diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelSubtype.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelSubtype.java new file mode 100644 index 0000000000..9bdd891a5d --- /dev/null +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelSubtype.java @@ -0,0 +1,20 @@ +package com.bakdata.conquery.quarkus.models; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares the serialized identity of a concrete polymorphic model. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface PolymorphicModelSubtype { + + Class base(); + + String id(); +} diff --git a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelTypeProvider.java b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelTypeProvider.java index 8b8ca91e6f..44cc26b77f 100644 --- a/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelTypeProvider.java +++ b/backend-quarkus/src/main/java/com/bakdata/conquery/quarkus/models/PolymorphicModelTypeProvider.java @@ -2,9 +2,23 @@ public interface PolymorphicModelTypeProvider { - Class baseType(); + Class modelType(); - String typeId(); + @SuppressWarnings("unchecked") + default Class baseType() { + return (Class) subtype().base(); + } - Class modelType(); + default String typeId() { + return subtype().id(); + } + + private PolymorphicModelSubtype subtype() { + PolymorphicModelSubtype subtype = modelType().getAnnotation(PolymorphicModelSubtype.class); + if (subtype == null) { + throw new IllegalStateException("Polymorphic model " + modelType().getName() + + " must be annotated with @PolymorphicModelSubtype"); + } + return subtype; + } } From 999fec5b4578611ad62224ab7d5eeb5d94e863ae Mon Sep 17 00:00:00 2001 From: MT <12283268+thoniTUB@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:25:40 +0200 Subject: [PATCH 45/45] bring back frontend's .env.example --- frontend/.env.example | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 frontend/.env.example diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000000..f3763b5599 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,25 @@ +######################################## +# 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