From a480dd3d0fa4d72c4c0702fe8215a3525b6b3efe Mon Sep 17 00:00:00 2001 From: Pavel Horal Date: Fri, 22 May 2026 22:10:47 +0200 Subject: [PATCH 1/5] Add base infrastructure for JSON value filter support --- .../jdbc/impl/handler/MappedTableHandler.java | 22 +++++++---- .../query/MappedSQLQueryFilterVisitor.java | 37 ++++++++++++++----- .../vendor/PostgreSQLMappedTableHandler.java | 2 +- .../handler/AbstractTableHandlerTest.java | 17 ++++++++- .../vendor/DB2TestConnectionProvider.java | 1 + .../vendor/MSSQLTestConnectionProvider.java | 1 + 6 files changed, 62 insertions(+), 18 deletions(-) diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/MappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/MappedTableHandler.java index 8ba29fdfb..d4f64092d 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/MappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/MappedTableHandler.java @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2011-2016 ForgeRock AS. - * Portions Copyright 2018-2025 Wren Security. + * Portions Copyright 2018-2026 Wren Security. */ package org.forgerock.openidm.repo.jdbc.impl.handler; @@ -45,6 +45,7 @@ import org.forgerock.openidm.repo.jdbc.Constants; import org.forgerock.openidm.repo.jdbc.SQLExceptionHandler; import org.forgerock.openidm.repo.jdbc.impl.SQLBuilder; +import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig.ValueType; import org.forgerock.openidm.repo.jdbc.impl.mapper.MappedResultMapper; import org.forgerock.openidm.repo.jdbc.impl.mapper.ResultMapper; import org.forgerock.openidm.repo.jdbc.impl.mapper.ResultMappers; @@ -471,14 +472,21 @@ protected SQLBuilder resolveQueryFilter(QueryFilter queryFilter, Li * @return new configuration resolver instance */ protected MappedConfigResolver createConfigResolver() { - Map columnConfig = columnMapping.values().stream() + Map columnConfigs = columnMapping.values().stream() .collect(Collectors.toMap(value -> value.propertyName, value -> value)); return field -> { - var config = columnConfig.get(field); - if (config == null) { - throw new IllegalArgumentException("Unknown object field: " + field.toString()); - } - return config; + var columnPath = field; + do { + var config = columnConfigs.get(columnPath); + if (config == null) { + continue; + } + if (columnPath != field && config.valueType != ValueType.JSON_MAP) { + break; // only JSON_MAP can be mapped to parent path + } + return config; + } while ((columnPath = columnPath.parent()) != null); + throw new IllegalArgumentException("Unknown object field: " + field.toString()); }; } diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/MappedSQLQueryFilterVisitor.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/MappedSQLQueryFilterVisitor.java index d5759c82f..c81bf069b 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/MappedSQLQueryFilterVisitor.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/MappedSQLQueryFilterVisitor.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.query; @@ -53,16 +53,15 @@ public StringSQLRenderer visitValueAssertion(NamedParameterCollector collector, return visitBooleanAssertion(collector, config, operand, field, valueAssertion); } - String paramValue; - try { - paramValue = valueAssertion instanceof String - ? (String) valueAssertion - : objectMapper.writeValueAsString(valueAssertion); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Unexpected JSON conversion error", e); + if (config.valueType == ValueType.JSON_LIST) { + return visitJsonListAssertion(collector, config, operand, field, valueAssertion); } - String paramName = collector.register("v", paramValue); + if (config.valueType == ValueType.JSON_MAP) { + return visitJsonMapAssertion(collector, config, operand, field, valueAssertion); + } + + String paramName = collector.register("v", toJson(valueAssertion)); return new StringSQLRenderer( config.columnName + " " + operand + " " @@ -89,6 +88,16 @@ protected StringSQLRenderer visitBooleanAssertion(NamedParameterCollector collec + "${" + paramName + "}"); } + protected StringSQLRenderer visitJsonListAssertion(NamedParameterCollector collector, MappedColumnConfig config, + String operand, JsonPointer field, Object valueAssertion) { + throw new UnsupportedOperationException("JSON_LIST value assertions are not supported"); + } + + protected StringSQLRenderer visitJsonMapAssertion(NamedParameterCollector collector, MappedColumnConfig config, + String operand, JsonPointer field, Object valueAssertion) { + throw new UnsupportedOperationException("JSON_LIST value assertions are not supported"); + } + @Override public StringSQLRenderer visitPresentFilter(NamedParameterCollector collector, JsonPointer field) { MappedColumnConfig config = configResolver.resolve(field); @@ -102,4 +111,14 @@ protected boolean isNumeric(final Object valueAssertion) { || valueAssertion instanceof Double; } + protected String toJson(Object valueAssertion) { + try { + return valueAssertion instanceof String + ? (String) valueAssertion + : objectMapper.writeValueAsString(valueAssertion); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Unexpected JSON conversion error", e); + } + } + } diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/PostgreSQLMappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/PostgreSQLMappedTableHandler.java index 9a40f1fd8..d0b808414 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/PostgreSQLMappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/PostgreSQLMappedTableHandler.java @@ -55,7 +55,7 @@ protected Map initializeImplicitSql() { + columnMapping.values().stream() .map(config -> config.isJson() ? "?::json" : "?") .collect(Collectors.joining(", ")) - +")"); + + ")"); result.put(ImplicitSqlType.UPDATE, "UPDATE ${_dbSchema}.${_table} " + "SET " diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java index fe1ab12cf..43ed19cd1 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.handler; @@ -300,6 +300,21 @@ public void testQueryFilterEmpty() throws Exception { assertTrue(result.isEmpty()); } + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testQueryFilterJsonList() throws Exception { + queryResource("tags eq 'foo'"); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testQueryFilterJsonMap() throws Exception { + queryResource("meta/owner eq 'john'"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testQueryFilterInvalidField() throws Exception { + queryResource("tags/owner eq 'john'"); + } + @Test public void testQueryPaging() throws Exception { for (int i = 0; i < 10; i++) { diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2TestConnectionProvider.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2TestConnectionProvider.java index 121638b9b..45a0afbee 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2TestConnectionProvider.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2TestConnectionProvider.java @@ -24,6 +24,7 @@ public class DB2TestConnectionProvider extends AbstractTestConnectionProvider { + @SuppressWarnings("resource") private static Db2Container container = new Db2Container("icr.io/db2_community/db2:11.5.9.0") .acceptLicense() .withDatabaseName("wrenidm") diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLTestConnectionProvider.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLTestConnectionProvider.java index 77bf8c5af..54c49d7c0 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLTestConnectionProvider.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLTestConnectionProvider.java @@ -28,6 +28,7 @@ public class MSSQLTestConnectionProvider extends AbstractTestConnectionProvider private static final String IMAGE_NAME = "mcr.microsoft.com/mssql/server:2019-CU14-ubuntu-20.04"; + @SuppressWarnings("resource") private static JdbcDatabaseContainer container = new MSSQLServerContainer(IMAGE_NAME) .acceptLicense() .withInitScript("vendor/mssql.sql"); From 63517b4371df7e14449c8c61441e245c57c1550d Mon Sep 17 00:00:00 2001 From: Pavel Horal Date: Sat, 23 May 2026 11:39:15 +0200 Subject: [PATCH 2/5] Upgrade Testcontainers dependency --- openidm-repo-jdbc/pom.xml | 25 ++++++++++--------- .../vendor/DB2TestConnectionProvider.java | 2 +- .../vendor/MSSQLTestConnectionProvider.java | 2 +- pom.xml | 6 ++--- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/openidm-repo-jdbc/pom.xml b/openidm-repo-jdbc/pom.xml index 49181657c..82e734cd8 100644 --- a/openidm-repo-jdbc/pom.xml +++ b/openidm-repo-jdbc/pom.xml @@ -13,7 +13,7 @@ information: "Portions copyright [year] [name of copyright owner]". Copyright (c) 2011-2013 ForgeRock AS. All Rights Reserved - Portions Copyright 2017-2024 Wren Security. + Portions Copyright 2017-2026 Wren Security. --> 4.0.0 @@ -35,7 +35,8 @@ !org.testng.annotations,!sun.misc,!org.w3c.dom,jakarta.inject;resolution:=optional - 1.19.3 + 2.0.5 + 2.0.1 @@ -118,8 +119,8 @@ org.testcontainers - db2 - ${testcontainers.version} + testcontainers-db2 + ${testcontainers-module.version} test @@ -132,8 +133,8 @@ org.testcontainers - mssqlserver - ${testcontainers.version} + testcontainers-mssqlserver + ${testcontainers-module.version} test @@ -146,8 +147,8 @@ org.testcontainers - oracle-xe - ${testcontainers.version} + testcontainers-oracle-xe + ${testcontainers-module.version} test @@ -160,8 +161,8 @@ org.testcontainers - mysql - ${testcontainers.version} + testcontainers-mysql + ${testcontainers-module.version} test @@ -174,8 +175,8 @@ org.testcontainers - postgresql - ${testcontainers.version} + testcontainers-postgresql + ${testcontainers-module.version} test diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2TestConnectionProvider.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2TestConnectionProvider.java index 45a0afbee..7a57a6012 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2TestConnectionProvider.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2TestConnectionProvider.java @@ -20,7 +20,7 @@ import java.sql.Connection; import java.sql.DriverManager; import org.forgerock.openidm.repo.jdbc.impl.handler.AbstractTestConnectionProvider; -import org.testcontainers.containers.Db2Container; +import org.testcontainers.db2.Db2Container; public class DB2TestConnectionProvider extends AbstractTestConnectionProvider { diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLTestConnectionProvider.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLTestConnectionProvider.java index 54c49d7c0..021d51647 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLTestConnectionProvider.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLTestConnectionProvider.java @@ -21,7 +21,7 @@ import java.sql.DriverManager; import org.forgerock.openidm.repo.jdbc.impl.handler.AbstractTestConnectionProvider; import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.mssqlserver.MSSQLServerContainer; @SuppressWarnings("rawtypes") public class MSSQLTestConnectionProvider extends AbstractTestConnectionProvider { diff --git a/pom.xml b/pom.xml index fcc978b32..9e0b395aa 100644 --- a/pom.xml +++ b/pom.xml @@ -122,7 +122,7 @@ - 1.9.15 + 1.9.18 17 @@ -191,8 +191,8 @@ - 1.10 - 2.15.1 + 1.22.0 + 2.22.0 From dd15ea20146ea16019db4921b77055391cb6ac26 Mon Sep 17 00:00:00 2001 From: Pavel Horal Date: Fri, 22 May 2026 22:10:47 +0200 Subject: [PATCH 3/5] Add base infrastructure for JSON value filter support --- .../impl/handler/GenericTableHandler.java | 4 +-- .../AbstractGenericTableHandlerTest.java | 9 ++++++- .../AbstractMappedTableHandlerTest.java | 24 ++++++++++++++++- .../handler/AbstractTableHandlerTest.java | 26 ++++++++++++++----- .../impl/handler/GenericTableConfigTest.java | 14 +++++----- .../vendor/OracleTestConnectionProvider.java | 1 + 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java index 76557f565..aa299120b 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java @@ -63,7 +63,7 @@ import org.slf4j.LoggerFactory; /** - * Generic table handler that supports objects stored as JSON string with a separate properies table used for + * Generic table handler that supports objects stored as JSON string with a separate properties table used for * indexing and querying objects by property value filters. * *

@@ -419,8 +419,8 @@ private void extractValueProperties(JsonValue json, BiConsumer resource.get(OBJECT_ID)) + .collect(Collectors.toSet()); + assertEquals(resultIds, Set.of(RESOURCE_ID)); } - @Test(expectedExceptions = UnsupportedOperationException.class) + @Test public void testQueryFilterJsonMap() throws Exception { - queryResource("meta/owner eq 'john'"); + createResource(RESOURCE_ID, Map.of("meta", Map.of("owner", "john"))); + createResource("ignored", Map.of("meta", Map.of("owner", "lucy"))); + + var resultIds = queryResource("meta/owner eq 'john'").stream() + .map(resource -> resource.get(OBJECT_ID)) + .collect(Collectors.toSet()); + assertEquals(resultIds, Set.of(RESOURCE_ID)); } - @Test(expectedExceptions = IllegalArgumentException.class) + @Test public void testQueryFilterInvalidField() throws Exception { - queryResource("tags/owner eq 'john'"); + var result = queryResource("tags/owner eq 'john'"); + assertTrue(result.isEmpty()); } @Test diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableConfigTest.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableConfigTest.java index 7004af0a8..962e8ff89 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableConfigTest.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableConfigTest.java @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2014 ForgeRock AS. - * Portions Copyright 2024 Wren Security + * Portions Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.handler; @@ -20,9 +20,9 @@ import static org.forgerock.json.JsonValue.json; import static org.forgerock.json.JsonValue.object; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import org.forgerock.json.JsonPointer; -import org.testng.Assert; import org.testng.annotations.Test; /** @@ -48,16 +48,16 @@ public void testSearchable() throws Exception { // simple property assertFalse(tableConfig.isSearchable(new JsonPointer("/arbitrary"))); // map/object property - Assert.assertFalse(tableConfig.isSearchable(new JsonPointer("/arbitrary2/map/x"))); + assertFalse(tableConfig.isSearchable(new JsonPointer("/arbitrary2/map/x"))); // list/array property - Assert.assertFalse(tableConfig.isSearchable(new JsonPointer("/arbitrary3/list/0"))); + assertFalse(tableConfig.isSearchable(new JsonPointer("/arbitrary3/list/0"))); // simple property - Assert.assertTrue(tableConfig.isSearchable(new JsonPointer("/userName"))); + assertTrue(tableConfig.isSearchable(new JsonPointer("/userName"))); // map/object property - Assert.assertTrue(tableConfig.isSearchable(new JsonPointer("/addresses/home/street"))); + assertTrue(tableConfig.isSearchable(new JsonPointer("/addresses/home/street"))); // list/array property - Assert.assertTrue(tableConfig.isSearchable(new JsonPointer("/roles/3"))); + assertTrue(tableConfig.isSearchable(new JsonPointer("/roles/3"))); } } diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleTestConnectionProvider.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleTestConnectionProvider.java index 96a06e2f3..8659186ba 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleTestConnectionProvider.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleTestConnectionProvider.java @@ -28,6 +28,7 @@ public class OracleTestConnectionProvider extends AbstractTestConnectionProvider private static final String IMAGE_NAME = "gvenzl/oracle-xe:21-slim-faststart"; + @SuppressWarnings("resource") private static JdbcDatabaseContainer container = new OracleContainer(IMAGE_NAME) .withDatabaseName("wrenidm") .withUsername("wrenidm") From 3e6fb062061c863664f859b985953ad4c7791ce8 Mon Sep 17 00:00:00 2001 From: Pavel Horal Date: Sun, 24 May 2026 12:19:52 +0200 Subject: [PATCH 4/5] Add support for list based query filters in generic tables (#167) --- .../impl/handler/GenericTableHandler.java | 25 ++++++++++------- .../query/GenericSQLQueryFilterVisitor.java | 28 ++++++++----------- .../handler/AbstractTableHandlerTest.java | 7 +++-- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java index aa299120b..680ffdb69 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java @@ -13,7 +13,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2011-2016 ForgeRock AS. - * Portions Copyright 2024 Wren Security + * Portions Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.handler; @@ -26,13 +26,14 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.EnumMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.function.BiConsumer; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -347,13 +348,13 @@ protected void writeValueProperties(String fullId, long databaseId, JsonValue va return; // no searchable properties, no need to index } - Map pairs = new LinkedHashMap(); - extractValueProperties(value, pairs::put); + List> pairs = new ArrayList<>(); + extractValueProperties(value, (p, v) -> pairs.add(new SimpleEntry(p, v)), false); try (var createStatement = resolveImplicitStatement(ImplicitSqlType.PROPCREATE, false, connection)) { int batchingCount = 0; - for (var pair : pairs.entrySet()) { + for (var pair : pairs) { // prepare index properties var object = pair.getValue(); var idxkey = pair.getKey().toString(); @@ -401,18 +402,22 @@ protected void writeValueProperties(String fullId, long databaseId, JsonValue va * * @param value JSON value (array or object) * @param collector callback for collecting extracted property values + * @param unwrap whether to collect primitive values under the parent pointer */ - private void extractValueProperties(JsonValue json, BiConsumer collector) { + private void extractValueProperties(JsonValue json, BiConsumer collector, boolean unwrap) { for (JsonValue entry : json) { JsonPointer pointer = entry.getPointer(); if (!tableConfig.isSearchable(pointer)) { continue; } if (entry.isMap() || entry.isList()) { - extractValueProperties(entry, collector); - continue; + extractValueProperties(entry, collector, entry.isList()); + } else { + collector.accept(pointer, entry.getObject()); + } + if (unwrap) { + collector.accept(pointer.parent(), entry.getObject()); } - collector.accept(pointer, entry.getObject()); } } @@ -640,7 +645,7 @@ protected int getSearchableLength() { * @return new GenericSQLQueryFilterVisitor instance */ protected GenericSQLQueryFilterVisitor createFilterVisitor(SQLBuilder builder) { - return new GenericSQLQueryFilterVisitor(getSearchableLength(), builder); + return new GenericSQLQueryFilterVisitor(getSearchableLength()); } @Override diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/GenericSQLQueryFilterVisitor.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/GenericSQLQueryFilterVisitor.java index 30f9a8e02..e8d57ebc9 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/GenericSQLQueryFilterVisitor.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/GenericSQLQueryFilterVisitor.java @@ -2,7 +2,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2015 ForgeRock AS. All rights reserved. - * Portions Copyright 2024 Wren Security. + * Portions Copyright 2024-2026 Wren Security. * * The contents of this file are subject to the terms * of the Common Development and Distribution License @@ -33,7 +33,6 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.forgerock.json.JsonPointer; -import org.forgerock.openidm.repo.jdbc.impl.SQLBuilder; import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; import org.forgerock.openidm.repo.util.AbstractSQLQueryFilterVisitor; import org.forgerock.openidm.repo.util.Clause; @@ -48,22 +47,17 @@ * Filter visitor does not support contains filters for collection members. Only simple string * based contains is supported. */ -// TODO support collection based assertions public class GenericSQLQueryFilterVisitor extends AbstractSQLQueryFilterVisitor { private final int searchableLength; - private final SQLBuilder builder; - /** * Construct a QueryFilterVisitor to produce SQL for managed objects using the generic table structure. * * @param searchableLength the searchable length; properties longer than this will be trimmed to this length - * @param builder the {@link SQLBuilder} to use to keep track of the select columns, table joins, and order by lists */ - public GenericSQLQueryFilterVisitor(final int searchableLength, SQLBuilder builder) { + public GenericSQLQueryFilterVisitor(final int searchableLength) { this.searchableLength = searchableLength; - this.builder = builder; } private boolean isNumeric(final Object valueAssertion) { @@ -136,8 +130,8 @@ public Clause visitValueAssertion(NamedParameterCollector collector, String oper return where("obj.objectid " + operand + " ${" + valueParam + "}"); } - String propParam = collector.register("k", field.toString()); String joinAlias = collector.generate("p"); + String propParam = collector.register("k", field.toString()); final Clause valueClause; if (isNumeric(valueAssertion)) { valueClause = buildNumericValueClause(joinAlias, operand, valueParam); @@ -146,10 +140,11 @@ public Clause visitValueAssertion(NamedParameterCollector collector, String oper } else { valueClause = buildStringValueClause(joinAlias, operand, valueParam); } - builder.leftJoin("${_dbSchema}.${_propTable}", joinAlias) - .on(where(joinAlias + ".${_mainTable}_id = obj.id") - .and(where(joinAlias + ".propkey = ${" + propParam + "}"))); - return valueClause; + + return where("EXISTS (SELECT 1 FROM ${_dbSchema}.${_propTable} " + joinAlias + " WHERE " + + joinAlias + ".${_mainTable}_id = obj.id" + + " AND " + joinAlias + ".propkey = ${" + propParam + "}" + + " AND " + valueClause.toSQL() + ")"); } /** @@ -182,10 +177,9 @@ public Clause visitPresentFilter(NamedParameterCollector collector, JsonPointer } else { var propParam = collector.register("k", field.toString()); var joinAlias = collector.generate("p"); - builder.leftJoin("${_dbSchema}.${_propTable}", joinAlias) - .on(where(joinAlias + ".${_mainTable}_id = obj.id") - .and(joinAlias + ".propkey = ${" + propParam + "}")); - return where(joinAlias + ".propvalue IS NOT NULL"); + return where("EXISTS (SELECT 1 FROM ${_dbSchema}.${_propTable} " + joinAlias + " WHERE " + + joinAlias + ".${_mainTable}_id = obj.id" + + " AND " + joinAlias + ".propkey = ${" + propParam + "})"); } } diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java index d4f630f3c..2babae5ff 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java @@ -301,12 +301,15 @@ public void testQueryFilterEmpty() throws Exception { assertTrue(result.isEmpty()); } - @Test(enabled = false) + @Test public void testQueryFilterJsonList() throws Exception { createResource(RESOURCE_ID, Map.of("tags", List.of("foo", "bar"))); createResource("ignored", Map.of("tags", List.of("bar", "baz"))); - var resultIds = queryResource("tags eq 'foo'").stream() + var result = queryResource("tags eq 'foo'"); + assertEquals(result.size(), 1); + + var resultIds = result.stream() .map(resource -> resource.get(OBJECT_ID)) .collect(Collectors.toSet()); assertEquals(resultIds, Set.of(RESOURCE_ID)); From c37d643148fff3e8b6a24ab6dd830972b395d4e3 Mon Sep 17 00:00:00 2001 From: Pavel Horal Date: Sun, 24 May 2026 18:59:53 +0200 Subject: [PATCH 5/5] Add support for list based query filters in mapped tables (#167) --- openidm-repo-jdbc/pom.xml | 6 + .../jdbc/impl/JDBCDataSourceService.java | 2 +- .../impl/handler/GenericTableHandler.java | 2 +- .../jdbc/impl/handler/MappedTableHandler.java | 28 +++- .../query/FieldFilterVisitorResolver.java | 34 ++++ .../impl/query/JsonFieldFilterVisitor.java | 131 +++++++++++++++ .../query/MappedSQLQueryFilterVisitor.java | 124 -------------- .../repo/jdbc/impl/query/SQLJSONUtils.java | 61 +++++++ .../query/SQLRendererFieldFilterVisitor.java | 55 ++++++ .../query/SQLRendererQueryFilterVisitor.java | 124 ++++++++++++++ .../impl/query/SimpleFieldFilterVisitor.java | 158 ++++++++++++++++++ .../impl/vendor/DB2MappedTableHandler.java | 17 +- .../impl/vendor/H2MappedTableHandler.java | 17 +- .../impl/vendor/MySQLMappedTableHandler.java | 17 +- .../impl/vendor/OracleMappedTableHandler.java | 17 +- .../vendor/PostgreSQLMappedTableHandler.java | 39 ++++- .../AbstractMappedTableHandlerTest.java | 14 +- .../handler/AbstractTableHandlerTest.java | 22 ++- .../handler/HSQLDBMappedTableHandlerTest.java | 14 +- .../jdbc/impl/query/SQLJSONUtilsTest.java | 49 ++++++ .../impl/vendor/DB2MappedTableHandlerIT.java | 14 +- .../impl/vendor/H2MappedTableHandlerIT.java | 14 +- .../vendor/MSSQLMappedTableHandlerIT.java | 14 +- .../vendor/MySQLMappedTableHandlerIT.java | 14 +- .../vendor/OracleMappedTableHandlerIT.java | 14 +- openidm-zip/pom.xml | 1 - pom.xml | 2 +- 27 files changed, 805 insertions(+), 199 deletions(-) create mode 100644 openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/FieldFilterVisitorResolver.java create mode 100644 openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/JsonFieldFilterVisitor.java delete mode 100644 openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/MappedSQLQueryFilterVisitor.java create mode 100644 openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLJSONUtils.java create mode 100644 openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLRendererFieldFilterVisitor.java create mode 100644 openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLRendererQueryFilterVisitor.java create mode 100644 openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SimpleFieldFilterVisitor.java create mode 100644 openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLJSONUtilsTest.java diff --git a/openidm-repo-jdbc/pom.xml b/openidm-repo-jdbc/pom.xml index 82e734cd8..f879d9462 100644 --- a/openidm-repo-jdbc/pom.xml +++ b/openidm-repo-jdbc/pom.xml @@ -40,6 +40,12 @@ + + ch.qos.logback + logback-classic + test + + org.wrensecurity.wrenidm openidm-enhanced-config diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/datasource/jdbc/impl/JDBCDataSourceService.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/datasource/jdbc/impl/JDBCDataSourceService.java index a0fba1fe9..535236783 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/datasource/jdbc/impl/JDBCDataSourceService.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/datasource/jdbc/impl/JDBCDataSourceService.java @@ -111,7 +111,7 @@ public DataSourceConfig deserialize(JsonParser jsonParser, DeserializationContex final Iterable> fields = new Iterable>() { @Override public Iterator> iterator() { - return node.fields(); + return node.properties().iterator(); } }; for (Map.Entry element : fields) { diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java index 680ffdb69..6990d85a6 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/GenericTableHandler.java @@ -349,7 +349,7 @@ protected void writeValueProperties(String fullId, long databaseId, JsonValue va } List> pairs = new ArrayList<>(); - extractValueProperties(value, (p, v) -> pairs.add(new SimpleEntry(p, v)), false); + extractValueProperties(value, (p, v) -> pairs.add(new SimpleEntry<>(p, v)), false); try (var createStatement = resolveImplicitStatement(ImplicitSqlType.PROPCREATE, false, connection)) { int batchingCount = 0; diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/MappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/MappedTableHandler.java index d4f64092d..5741ee753 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/MappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/handler/MappedTableHandler.java @@ -49,7 +49,9 @@ import org.forgerock.openidm.repo.jdbc.impl.mapper.MappedResultMapper; import org.forgerock.openidm.repo.jdbc.impl.mapper.ResultMapper; import org.forgerock.openidm.repo.jdbc.impl.mapper.ResultMappers; -import org.forgerock.openidm.repo.jdbc.impl.query.MappedSQLQueryFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.SQLRendererFieldFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.SQLRendererQueryFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.SimpleFieldFilterVisitor; import org.forgerock.openidm.repo.jdbc.impl.query.TableQueryHandler; import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterSql; @@ -511,13 +513,29 @@ public String toSQL() { } /** - * Create new {@link MappedSQLQueryFilterVisitor} to render query filter queries. + * Create new {@link SQLRendererQueryFilterVisitor} to render query filter queries. * * @param configResolver column configuration resolver - * @return new MappedSQLQueryFilterVisitor instance + * @return new query filter visitor instance */ - protected MappedSQLQueryFilterVisitor createFilterVisitor(MappedConfigResolver configResolver) { - return new MappedSQLQueryFilterVisitor(configResolver, objectMapper); + protected SQLRendererQueryFilterVisitor createFilterVisitor(MappedConfigResolver configResolver) { + return new SQLRendererQueryFilterVisitor(field -> { + var columnConfig = configResolver.resolve(field); + switch (columnConfig.valueType) { + case JSON_LIST, JSON_MAP: + return createJsonFieldVisitor(columnConfig); + default: + return createSimpleFieldVisitor(columnConfig); + } + }); + } + + protected SQLRendererFieldFilterVisitor createSimpleFieldVisitor(MappedColumnConfig columnConfig) { + return new SimpleFieldFilterVisitor(columnConfig, objectMapper); + } + + protected SQLRendererFieldFilterVisitor createJsonFieldVisitor(MappedColumnConfig columnConfig) { + throw new UnsupportedOperationException("JSON based field filtering not supported"); } @Override diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/FieldFilterVisitorResolver.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/FieldFilterVisitorResolver.java new file mode 100644 index 000000000..9b417092e --- /dev/null +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/FieldFilterVisitorResolver.java @@ -0,0 +1,34 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.1.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.1.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 Wren Security + */ +package org.forgerock.openidm.repo.jdbc.impl.query; + +import org.forgerock.json.JsonPointer; + +/** + * Resolver for field based filter rendering visitors. + */ +@FunctionalInterface +public interface FieldFilterVisitorResolver { + + /** + * Resolve field filter rendering visitor. + * + * @param field field being visited + * @return visitor for the field + */ + SQLRendererFieldFilterVisitor resolve(JsonPointer field); + +} diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/JsonFieldFilterVisitor.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/JsonFieldFilterVisitor.java new file mode 100644 index 000000000..da60b76aa --- /dev/null +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/JsonFieldFilterVisitor.java @@ -0,0 +1,131 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.1.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.1.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 Wren Security + */ +package org.forgerock.openidm.repo.jdbc.impl.query; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.regex.Pattern; +import org.forgerock.json.JsonPointer; +import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig; +import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; +import org.forgerock.openidm.repo.util.SQLRenderer; +import org.forgerock.openidm.repo.util.StringSQLRenderer; + +/** + * JSON field query filter visitor. + * + *

+ * This field filter visitor works only with compound value (JSON_LIST and JSON_MAP) columns. + * Rendered filter format is based on SQL/JSON paths and functions. + */ +public class JsonFieldFilterVisitor extends SQLRendererFieldFilterVisitor { + + protected final MappedColumnConfig columnConfig; + + protected final ObjectMapper objectMapper; + + public JsonFieldFilterVisitor(MappedColumnConfig columnConfig, ObjectMapper objectMapper) { + this.columnConfig = columnConfig; + this.objectMapper = objectMapper; + } + + private SQLRenderer visitValueAssertion(NamedParameterCollector collector, String operand, + JsonPointer field, Object valueAssertion) { + if (!columnConfig.isJson()) { + throw new UnsupportedOperationException("Primitive value filter not supported"); + } + return visitJsonAssertion(collector, operand, toRelativePointer(field), valueAssertion); + } + + protected SQLRenderer visitJsonAssertion(NamedParameterCollector collector, String operand, + JsonPointer nestedPath, Object valueAssertion) { + + String pathExpression = SQLJSONUtils.toSqlJsonPath(nestedPath) + + " ? (@ " + operand + " " + toJsonValue(valueAssertion) + ")"; + + String paramName = collector.register("v", pathExpression); + return new StringSQLRenderer( + "JSON_EXISTS(" + columnConfig.columnName + ", ${" + paramName + "})"); + } + + protected JsonPointer toRelativePointer(JsonPointer field) { + return field.relativePointer(field.size() - columnConfig.propertyName.size()); + } + + protected String toJsonValue(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Unexpected JSON conversion error", e); + } + } + + @Override + public SQLRenderer visitEqualsFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "==", field, valueAssertion); + } + + @Override + public SQLRenderer visitExtendedMatchFilter(NamedParameterCollector collector, JsonPointer field, + String operator, Object valueAssertion) { + throw new UnsupportedOperationException("Extended match filter not supported on this endpoint"); + } + + @Override + public SQLRenderer visitGreaterThanFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, ">", field, valueAssertion); + } + + @Override + public SQLRenderer visitGreaterThanOrEqualToFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, ">=", field, valueAssertion); + } + + @Override + public SQLRenderer visitLessThanFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "<", field, valueAssertion); + } + + @Override + public SQLRenderer visitLessThanOrEqualToFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "<=", field, valueAssertion); + } + + @Override + public SQLRenderer visitPresentFilter(NamedParameterCollector collector, JsonPointer field) { + return new StringSQLRenderer(columnConfig.columnName + " IS NOT NULL"); + } + + @Override + public SQLRenderer visitStartsWithFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "like_regex", field, + "^" + Pattern.quote(String.valueOf(valueAssertion))); + } + + @Override + public SQLRenderer visitContainsFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "like_regex", field, + Pattern.quote(String.valueOf(valueAssertion))); + } + +} diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/MappedSQLQueryFilterVisitor.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/MappedSQLQueryFilterVisitor.java deleted file mode 100644 index c81bf069b..000000000 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/MappedSQLQueryFilterVisitor.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * The contents of this file are subject to the terms of the Common Development and - * Distribution License (the License). You may not use this file except in compliance with the - * License. - * - * You can obtain a copy of the License at legal/CDDLv1.1.txt. See the License for the - * specific language governing permission and limitations under the License. - * - * When distributing Covered Software, include this CDDL Header Notice in each file and include - * the License file at legal/CDDLv1.1.txt. If applicable, add the following below the CDDL - * Header, with the fields enclosed by brackets [] replaced by your own identifying - * information: "Portions copyright [year] [name of copyright owner]". - * - * Copyright 2024-2026 Wren Security - */ -package org.forgerock.openidm.repo.jdbc.impl.query; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.forgerock.json.JsonPointer; -import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig; -import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig.ValueType; -import org.forgerock.openidm.repo.jdbc.impl.handler.MappedConfigResolver; -import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; -import org.forgerock.openidm.repo.util.StringSQLQueryFilterVisitor; -import org.forgerock.openidm.repo.util.StringSQLRenderer; -import org.forgerock.util.query.QueryFilterVisitor; - -/** - * {@link QueryFilterVisitor} for generating WHERE clause for mapped table schema. - */ -public class MappedSQLQueryFilterVisitor extends StringSQLQueryFilterVisitor { - - protected final MappedConfigResolver configResolver; - - protected final ObjectMapper objectMapper; - - public MappedSQLQueryFilterVisitor(MappedConfigResolver configResolver, ObjectMapper objectMapper) { - this.configResolver = configResolver; - this.objectMapper = objectMapper; - } - - @Override - public StringSQLRenderer visitValueAssertion(NamedParameterCollector collector, String operand, JsonPointer field, - Object valueAssertion) { - MappedColumnConfig config = configResolver.resolve(field); - - if (isNumeric(valueAssertion) && config.valueType == ValueType.NUMBER) { - return visitNumericAssertion(collector, config, operand, field, valueAssertion); - } - - if (valueAssertion instanceof Boolean && config.valueType == ValueType.BOOLEAN) { - return visitBooleanAssertion(collector, config, operand, field, valueAssertion); - } - - if (config.valueType == ValueType.JSON_LIST) { - return visitJsonListAssertion(collector, config, operand, field, valueAssertion); - } - - if (config.valueType == ValueType.JSON_MAP) { - return visitJsonMapAssertion(collector, config, operand, field, valueAssertion); - } - - String paramName = collector.register("v", toJson(valueAssertion)); - return new StringSQLRenderer( - config.columnName - + " " + operand + " " - + "${" + paramName + "}"); - } - - protected StringSQLRenderer visitNumericAssertion(NamedParameterCollector collector, MappedColumnConfig config, - String operand, JsonPointer field, Object valueAssertion) { - // convert column value to DECIMAL to ensure correct operator behavior - String paramName = collector.register("v", valueAssertion); - return new StringSQLRenderer( - "CAST(" + config.columnName + " AS DECIMAL)" - + " " + operand + " " - + "CAST(${" + paramName + "} AS DECIMAL)"); - } - - protected StringSQLRenderer visitBooleanAssertion(NamedParameterCollector collector, MappedColumnConfig config, - String operand, JsonPointer field, Object valueAssertion) { - // convert column value to SMALLINT to ensure database vendor support - String paramName = collector.register("v", ((Boolean) valueAssertion).booleanValue() ? 1 : 0); - return new StringSQLRenderer( - "CAST(" + config.columnName + " AS BIT)" - + " " + operand + " " - + "${" + paramName + "}"); - } - - protected StringSQLRenderer visitJsonListAssertion(NamedParameterCollector collector, MappedColumnConfig config, - String operand, JsonPointer field, Object valueAssertion) { - throw new UnsupportedOperationException("JSON_LIST value assertions are not supported"); - } - - protected StringSQLRenderer visitJsonMapAssertion(NamedParameterCollector collector, MappedColumnConfig config, - String operand, JsonPointer field, Object valueAssertion) { - throw new UnsupportedOperationException("JSON_LIST value assertions are not supported"); - } - - @Override - public StringSQLRenderer visitPresentFilter(NamedParameterCollector collector, JsonPointer field) { - MappedColumnConfig config = configResolver.resolve(field); - return new StringSQLRenderer(config.columnName + " IS NOT NULL"); - } - - protected boolean isNumeric(final Object valueAssertion) { - return valueAssertion instanceof Integer - || valueAssertion instanceof Long - || valueAssertion instanceof Float - || valueAssertion instanceof Double; - } - - protected String toJson(Object valueAssertion) { - try { - return valueAssertion instanceof String - ? (String) valueAssertion - : objectMapper.writeValueAsString(valueAssertion); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Unexpected JSON conversion error", e); - } - } - -} diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLJSONUtils.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLJSONUtils.java new file mode 100644 index 000000000..d48f5119d --- /dev/null +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLJSONUtils.java @@ -0,0 +1,61 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.1.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.1.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 Wren Security + */ +package org.forgerock.openidm.repo.jdbc.impl.query; + +import java.util.regex.Pattern; +import org.forgerock.json.JsonPointer; +import org.forgerock.json.JsonValue; + +/** + * Utility methods for SQL/JSON path based queries (ISO/IEC 9075-2). + * + *

+ * Reference for the implementation: + * + *

    + *
  • https://www.postgresql.org/docs/current/datatype-json.html#DATATYPE-JSONPATH + *
+ */ +public class SQLJSONUtils { + + private static final Pattern ARRAY_INDEX_PATTERN = Pattern.compile("(?:[0-9]|[1-9]\\d+)"); + + private SQLJSONUtils() { + } + + /** + * Convert JSON pointer to SQL/JSON path expression. + * + *

+ * Decimal pointer tokens (e.g. 123 in /foo/123) are always interpreted as array indexes. + * Hence querying objects with numeric property names is not supported. + * + * @param pointer JSON pointer to convert + * @return SQL/JSON path expression + */ + public static String toSqlJsonPath(JsonPointer pointer) { + StringBuilder builder = new StringBuilder("$"); + for (String token : pointer) { + if (ARRAY_INDEX_PATTERN.matcher(token).matches()) { + builder.append("[").append(token).append("]"); + } else { + builder.append(".").append(new JsonValue(token).toString()); + } + } + return builder.toString(); + } + +} diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLRendererFieldFilterVisitor.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLRendererFieldFilterVisitor.java new file mode 100644 index 000000000..649c11a45 --- /dev/null +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLRendererFieldFilterVisitor.java @@ -0,0 +1,55 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.1.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.1.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 Wren Security + */ +package org.forgerock.openidm.repo.jdbc.impl.query; + +import java.util.List; +import org.forgerock.json.JsonPointer; +import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; +import org.forgerock.openidm.repo.util.SQLRenderer; +import org.forgerock.util.query.QueryFilter; +import org.forgerock.util.query.QueryFilterVisitor; + +/** + * Query filter visitor for atomic value assertion filter components. + */ +public abstract class SQLRendererFieldFilterVisitor implements + QueryFilterVisitor, NamedParameterCollector, JsonPointer> { + + @Override + public final SQLRenderer visitAndFilter(NamedParameterCollector collector, + List> subFilters) { + throw new UnsupportedOperationException("unable to handle non-field filter"); + } + + @Override + public final SQLRenderer visitOrFilter(NamedParameterCollector collector, + List> subFilters) { + throw new UnsupportedOperationException("unable to handle non-field filter"); + } + + @Override + public final SQLRenderer visitNotFilter(NamedParameterCollector collector, + QueryFilter subFilter) { + throw new UnsupportedOperationException("unable to handle non-field filter"); + } + + @Override + public final SQLRenderer visitBooleanLiteralFilter(NamedParameterCollector collector, + boolean value) { + throw new UnsupportedOperationException("unable to handle non-field filter"); + } + +} diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLRendererQueryFilterVisitor.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLRendererQueryFilterVisitor.java new file mode 100644 index 000000000..0087567b4 --- /dev/null +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLRendererQueryFilterVisitor.java @@ -0,0 +1,124 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.1.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.1.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 Wren Security + */ +package org.forgerock.openidm.repo.jdbc.impl.query; + +import java.util.List; +import org.forgerock.json.JsonPointer; +import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; +import org.forgerock.openidm.repo.util.Clauses; +import org.forgerock.openidm.repo.util.SQLRenderer; +import org.forgerock.util.query.QueryFilter; +import org.forgerock.util.query.QueryFilterVisitor; + +/** + * {@link SQLRenderer} based query filter visitor with customizable field-based filter rendering. + */ +public class SQLRendererQueryFilterVisitor implements + QueryFilterVisitor, NamedParameterCollector, JsonPointer> { + + private final FieldFilterVisitorResolver fieldVisitorResolver; + + /** + * Create visitor instance. + * + * @param fieldVisitorResolver field filter visitor resolver + */ + public SQLRendererQueryFilterVisitor(FieldFilterVisitorResolver fieldVisitorResolver) { + this.fieldVisitorResolver = fieldVisitorResolver; + } + + @Override + public SQLRenderer visitAndFilter(NamedParameterCollector collector, + List> subFilters) { + return Clauses.and(subFilters.stream() + .map(filter -> Clauses.where(filter.accept(this, collector).toSQL())) + .toList()); + } + + @Override + public SQLRenderer visitOrFilter(NamedParameterCollector collector, + List> subFilters) { + return Clauses.or(subFilters.stream() + .map(filter -> Clauses.where(filter.accept(this, collector).toSQL())) + .toList()); + } + + @Override + public SQLRenderer visitBooleanLiteralFilter(NamedParameterCollector collector, + boolean value) { + return Clauses.where(value ? "1 = 1" : "1 <> 1"); + } + + @Override + public SQLRenderer visitContainsFilter(NamedParameterCollector collector, + JsonPointer field, Object valueAssertion) { + return fieldVisitorResolver.resolve(field).visitContainsFilter(collector, field, valueAssertion); + } + + @Override + public SQLRenderer visitEqualsFilter(NamedParameterCollector collector, + JsonPointer field, Object valueAssertion) { + return fieldVisitorResolver.resolve(field).visitEqualsFilter(collector, field, valueAssertion); + } + + @Override + public SQLRenderer visitExtendedMatchFilter(NamedParameterCollector collector, + JsonPointer field, String operator, Object valueAssertion) { + return fieldVisitorResolver.resolve(field).visitExtendedMatchFilter(collector, field, operator, + valueAssertion); + } + + @Override + public SQLRenderer visitGreaterThanFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return fieldVisitorResolver.resolve(field).visitGreaterThanFilter(collector, field, valueAssertion); + } + + @Override + public SQLRenderer visitGreaterThanOrEqualToFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return fieldVisitorResolver.resolve(field).visitGreaterThanOrEqualToFilter(collector, field, valueAssertion); + } + + @Override + public SQLRenderer visitLessThanFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return fieldVisitorResolver.resolve(field).visitLessThanFilter(collector, field, valueAssertion); + } + + @Override + public SQLRenderer visitLessThanOrEqualToFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return fieldVisitorResolver.resolve(field).visitLessThanOrEqualToFilter(collector, field, valueAssertion); + } + + @Override + public SQLRenderer visitNotFilter(NamedParameterCollector collector, QueryFilter subFilter) { + return Clauses.not(subFilter.accept(this, collector).toSQL()); + } + + @Override + public SQLRenderer visitPresentFilter(NamedParameterCollector collector, JsonPointer field) { + return fieldVisitorResolver.resolve(field).visitPresentFilter(collector, field); + } + + @Override + public SQLRenderer visitStartsWithFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return fieldVisitorResolver.resolve(field).visitStartsWithFilter(collector, field, valueAssertion); + } + +} diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SimpleFieldFilterVisitor.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SimpleFieldFilterVisitor.java new file mode 100644 index 000000000..cd2bc3881 --- /dev/null +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/query/SimpleFieldFilterVisitor.java @@ -0,0 +1,158 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.1.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.1.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 Wren Security + */ +package org.forgerock.openidm.repo.jdbc.impl.query; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.forgerock.json.JsonPointer; +import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig; +import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig.ValueType; +import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; +import org.forgerock.openidm.repo.util.SQLRenderer; +import org.forgerock.openidm.repo.util.StringSQLRenderer; + +/** + * Simple (primitive) field query filter visitor. + * + *

+ * This field filter visitor works only with primitive value columns. Compound values (JSON_LIST and JSON_MAP) + * are not supported. + * + * @see JsonFieldFilterVisitor + */ +public class SimpleFieldFilterVisitor extends SQLRendererFieldFilterVisitor { + + protected final MappedColumnConfig columnConfig; + + protected final ObjectMapper objectMapper; + + public SimpleFieldFilterVisitor(MappedColumnConfig columnConfig, ObjectMapper objectMapper) { + this.columnConfig = columnConfig; + this.objectMapper = objectMapper; + } + + protected SQLRenderer visitValueAssertion(NamedParameterCollector collector, String operand, + Object valueAssertion) { + if (columnConfig.isJson()) { + throw new UnsupportedOperationException("Compound value filter not supported"); + } + + if (isNumeric(valueAssertion) && columnConfig.valueType == ValueType.NUMBER) { + return visitNumericAssertion(collector, operand, valueAssertion); + } + + if (valueAssertion instanceof Boolean && columnConfig.valueType == ValueType.BOOLEAN) { + return visitBooleanAssertion(collector, operand, valueAssertion); + } + + String paramName = collector.register("v", toStringValue(valueAssertion)); + return new StringSQLRenderer( + columnConfig.columnName + + " " + operand + " " + + "${" + paramName + "}"); + } + + protected SQLRenderer visitNumericAssertion(NamedParameterCollector collector, String operand, + Object valueAssertion) { + // convert column value to DECIMAL to ensure correct operator behavior + String paramName = collector.register("v", valueAssertion); + return new StringSQLRenderer( + "CAST(" + columnConfig.columnName + " AS DECIMAL)" + + " " + operand + " " + + "CAST(${" + paramName + "} AS DECIMAL)"); + } + + protected SQLRenderer visitBooleanAssertion(NamedParameterCollector collector, String operand, + Object valueAssertion) { + // convert column value to SMALLINT to ensure database vendor support + String paramName = collector.register("v", ((Boolean) valueAssertion).booleanValue() ? 1 : 0); + return new StringSQLRenderer( + "CAST(" + columnConfig.columnName + " AS BIT)" + + " " + operand + " " + + "${" + paramName + "}"); + } + + protected boolean isNumeric(final Object valueAssertion) { + return valueAssertion instanceof Integer + || valueAssertion instanceof Long + || valueAssertion instanceof Float + || valueAssertion instanceof Double; + } + + protected String toStringValue(Object valueAssertion) { + try { + return valueAssertion instanceof String stringAssertion + ? stringAssertion + : objectMapper.writeValueAsString(valueAssertion); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Unexpected JSON conversion error", e); + } + } + + @Override + public SQLRenderer visitContainsFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "LIKE", "%" + valueAssertion + "%"); + } + + @Override + public SQLRenderer visitEqualsFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "=", valueAssertion); + } + + @Override + public SQLRenderer visitExtendedMatchFilter(NamedParameterCollector collector, JsonPointer field, + String operator, Object valueAssertion) { + throw new UnsupportedOperationException("Extended match filter not supported on this endpoint"); + } + + @Override + public SQLRenderer visitGreaterThanFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, ">", valueAssertion); + } + + @Override + public SQLRenderer visitGreaterThanOrEqualToFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, ">=", valueAssertion); + } + + @Override + public SQLRenderer visitLessThanFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "<", valueAssertion); + } + + @Override + public SQLRenderer visitLessThanOrEqualToFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "<=", valueAssertion); + } + + @Override + public SQLRenderer visitPresentFilter(NamedParameterCollector collector, JsonPointer field) { + return new StringSQLRenderer(columnConfig.columnName + " IS NOT NULL"); + } + + @Override + public SQLRenderer visitStartsWithFilter(NamedParameterCollector collector, JsonPointer field, + Object valueAssertion) { + return visitValueAssertion(collector, "LIKE", valueAssertion + "%"); + } + +} diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2MappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2MappedTableHandler.java index a3735abe3..c31f900a3 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2MappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2MappedTableHandler.java @@ -11,20 +11,19 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.vendor; import java.util.Map; -import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.openidm.repo.jdbc.SQLExceptionHandler; import org.forgerock.openidm.repo.jdbc.impl.SQLBuilder; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig; -import org.forgerock.openidm.repo.jdbc.impl.handler.MappedConfigResolver; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedTableHandler; -import org.forgerock.openidm.repo.jdbc.impl.query.MappedSQLQueryFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.SimpleFieldFilterVisitor; import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; +import org.forgerock.openidm.repo.util.SQLRenderer; import org.forgerock.openidm.repo.util.StringSQLRenderer; /** @@ -65,13 +64,13 @@ public String toSQL() { } @Override - protected MappedSQLQueryFilterVisitor createFilterVisitor(MappedConfigResolver configResolver) { - return new MappedSQLQueryFilterVisitor(configResolver, objectMapper) { + protected SimpleFieldFilterVisitor createSimpleFieldVisitor(MappedColumnConfig columnConfig) { + return new SimpleFieldFilterVisitor(columnConfig, objectMapper) { @Override - protected StringSQLRenderer visitBooleanAssertion(NamedParameterCollector collector, - MappedColumnConfig config, String operand, JsonPointer field, Object valueAssertion) { + protected SQLRenderer visitBooleanAssertion(NamedParameterCollector collector, String operand, + Object valueAssertion) { String paramName = collector.register("v", ((Boolean) valueAssertion).booleanValue() ? 1 : 0); - return new StringSQLRenderer(config.columnName + " " + operand + " " + "${" + paramName + "}"); + return new StringSQLRenderer(columnConfig.columnName + " " + operand + " " + "${" + paramName + "}"); } }; } diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/H2MappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/H2MappedTableHandler.java index 7ce1d44c8..5989d793b 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/H2MappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/H2MappedTableHandler.java @@ -11,20 +11,19 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.vendor; import java.util.Map; import java.util.stream.Collectors; -import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.openidm.repo.jdbc.SQLExceptionHandler; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig; -import org.forgerock.openidm.repo.jdbc.impl.handler.MappedConfigResolver; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedTableHandler; -import org.forgerock.openidm.repo.jdbc.impl.query.MappedSQLQueryFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.SimpleFieldFilterVisitor; import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; +import org.forgerock.openidm.repo.util.SQLRenderer; import org.forgerock.openidm.repo.util.StringSQLRenderer; /** @@ -65,13 +64,13 @@ protected Map initializeImplicitSql() { } @Override - protected MappedSQLQueryFilterVisitor createFilterVisitor(MappedConfigResolver configResolver) { - return new MappedSQLQueryFilterVisitor(configResolver, objectMapper) { + protected SimpleFieldFilterVisitor createSimpleFieldVisitor(MappedColumnConfig columnConfig) { + return new SimpleFieldFilterVisitor(columnConfig, objectMapper) { @Override - protected StringSQLRenderer visitBooleanAssertion(NamedParameterCollector collector, - MappedColumnConfig config, String operand, JsonPointer field, Object valueAssertion) { + protected SQLRenderer visitBooleanAssertion(NamedParameterCollector collector, String operand, + Object valueAssertion) { String paramName = collector.register("v", valueAssertion); - return new StringSQLRenderer(config.columnName + " " + operand + " " + "${" + paramName + "}"); + return new StringSQLRenderer(columnConfig.columnName + " " + operand + " " + "${" + paramName + "}"); } }; } diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MySQLMappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MySQLMappedTableHandler.java index e32f0614d..ae6249b92 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MySQLMappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MySQLMappedTableHandler.java @@ -11,19 +11,18 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.vendor; import java.util.Map; -import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.openidm.repo.jdbc.SQLExceptionHandler; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig; -import org.forgerock.openidm.repo.jdbc.impl.handler.MappedConfigResolver; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedTableHandler; -import org.forgerock.openidm.repo.jdbc.impl.query.MappedSQLQueryFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.SimpleFieldFilterVisitor; import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; +import org.forgerock.openidm.repo.util.SQLRenderer; import org.forgerock.openidm.repo.util.StringSQLRenderer; /** @@ -42,13 +41,13 @@ public MySQLMappedTableHandler( } @Override - protected MappedSQLQueryFilterVisitor createFilterVisitor(MappedConfigResolver configResolver) { - return new MappedSQLQueryFilterVisitor(configResolver, objectMapper) { + protected SimpleFieldFilterVisitor createSimpleFieldVisitor(MappedColumnConfig columnConfig) { + return new SimpleFieldFilterVisitor(columnConfig, objectMapper) { @Override - protected StringSQLRenderer visitBooleanAssertion(NamedParameterCollector collector, - MappedColumnConfig config, String operand, JsonPointer field, Object valueAssertion) { + protected SQLRenderer visitBooleanAssertion(NamedParameterCollector collector, String operand, + Object valueAssertion) { String paramName = collector.register("v", valueAssertion); - return new StringSQLRenderer(config.columnName + " " + operand + " " + "${" + paramName + "}"); + return new StringSQLRenderer(columnConfig.columnName + " " + operand + " " + "${" + paramName + "}"); } }; } diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleMappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleMappedTableHandler.java index a0e07a6ba..877208b40 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleMappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleMappedTableHandler.java @@ -11,20 +11,19 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.vendor; import java.util.Map; -import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.openidm.repo.jdbc.SQLExceptionHandler; import org.forgerock.openidm.repo.jdbc.impl.SQLBuilder; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig; -import org.forgerock.openidm.repo.jdbc.impl.handler.MappedConfigResolver; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedTableHandler; -import org.forgerock.openidm.repo.jdbc.impl.query.MappedSQLQueryFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.SimpleFieldFilterVisitor; import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; +import org.forgerock.openidm.repo.util.SQLRenderer; import org.forgerock.openidm.repo.util.StringSQLRenderer; /** @@ -65,13 +64,13 @@ public String toSQL() { } @Override - protected MappedSQLQueryFilterVisitor createFilterVisitor(MappedConfigResolver configResolver) { - return new MappedSQLQueryFilterVisitor(configResolver, objectMapper) { + protected SimpleFieldFilterVisitor createSimpleFieldVisitor(MappedColumnConfig columnConfig) { + return new SimpleFieldFilterVisitor(columnConfig, objectMapper) { @Override - protected StringSQLRenderer visitBooleanAssertion(NamedParameterCollector collector, - MappedColumnConfig config, String operand, JsonPointer field, Object valueAssertion) { + protected SQLRenderer visitBooleanAssertion(NamedParameterCollector collector, String operand, + Object valueAssertion) { String paramName = collector.register("v", ((Boolean) valueAssertion).booleanValue() ? 1 : 0); - return new StringSQLRenderer(config.columnName + " " + operand + " " + "${" + paramName + "}"); + return new StringSQLRenderer(columnConfig.columnName + " " + operand + " " + "${" + paramName + "}"); } }; } diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/PostgreSQLMappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/PostgreSQLMappedTableHandler.java index d0b808414..ec38ab45c 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/PostgreSQLMappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/vendor/PostgreSQLMappedTableHandler.java @@ -12,7 +12,7 @@  * information: "Portions copyright [year] [name of copyright owner]".  *  * Copyright 2015 ForgeRock AS. - * Portions Copyright 2024 Wren Security. + * Portions Copyright 2024-2026 Wren Security.  */ package org.forgerock.openidm.repo.jdbc.impl.vendor; @@ -22,10 +22,13 @@ import org.forgerock.json.JsonValue; import org.forgerock.openidm.repo.jdbc.SQLExceptionHandler; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedColumnConfig; -import org.forgerock.openidm.repo.jdbc.impl.handler.MappedConfigResolver; import org.forgerock.openidm.repo.jdbc.impl.handler.MappedTableHandler; -import org.forgerock.openidm.repo.jdbc.impl.query.MappedSQLQueryFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.JsonFieldFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.SQLJSONUtils; +import org.forgerock.openidm.repo.jdbc.impl.query.SQLRendererFieldFilterVisitor; +import org.forgerock.openidm.repo.jdbc.impl.query.SimpleFieldFilterVisitor; import org.forgerock.openidm.repo.jdbc.impl.statement.NamedParameterCollector; +import org.forgerock.openidm.repo.util.SQLRenderer; import org.forgerock.openidm.repo.util.StringSQLRenderer; /** @@ -69,15 +72,35 @@ protected Map initializeImplicitSql() { @Override - protected MappedSQLQueryFilterVisitor createFilterVisitor(MappedConfigResolver configResolver) { - return new MappedSQLQueryFilterVisitor(configResolver, objectMapper) { + protected SimpleFieldFilterVisitor createSimpleFieldVisitor(MappedColumnConfig columnConfig) { + return new SimpleFieldFilterVisitor(columnConfig, objectMapper) { @Override - protected StringSQLRenderer visitBooleanAssertion(NamedParameterCollector collector, - MappedColumnConfig config, String operand, JsonPointer field, Object valueAssertion) { + protected SQLRenderer visitBooleanAssertion(NamedParameterCollector collector, String operand, + Object valueAssertion) { String paramName = collector.register("v", valueAssertion); - return new StringSQLRenderer(config.columnName + " " + operand + " " + "${" + paramName + "}"); + return new StringSQLRenderer(columnConfig.columnName + " " + operand + " " + "${" + paramName + "}"); } }; } + @Override + protected SQLRendererFieldFilterVisitor createJsonFieldVisitor(MappedColumnConfig columnConfig) { + return new JsonFieldFilterVisitor(columnConfig, objectMapper) { + @Override + protected SQLRenderer visitJsonAssertion(NamedParameterCollector collector, String operand, + JsonPointer nestedPath, Object valueAssertion) { + String pathExpression = SQLJSONUtils.toSqlJsonPath(nestedPath) + " " + operand + " " + + toJsonValue(valueAssertion); + String paramName = collector.register("v", pathExpression); + return new StringSQLRenderer(columnConfig.columnName + "::JSONB @@ ${" + paramName + "}::JSONPATH"); + } + + @Override + public SQLRenderer visitPresentFilter(NamedParameterCollector collector, JsonPointer field) { + String paramName = collector.register("v", SQLJSONUtils.toSqlJsonPath(toRelativePointer(field))); + return new StringSQLRenderer(columnConfig.columnName + "::JSONB @? ${" + paramName + "}::JSONPATH"); + } + }; + } + } diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractMappedTableHandlerTest.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractMappedTableHandlerTest.java index 3cb40b982..87d3276c2 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractMappedTableHandlerTest.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractMappedTableHandlerTest.java @@ -133,24 +133,12 @@ public void testQueryFilterLegacy() throws Exception { assertEquals(doubleResult.get(0).get(OBJECT_ID), "stringified-double"); } - @Test(expectedExceptions = UnsupportedOperationException.class) - @Override - public void testQueryFilterJsonList() throws Exception { - queryResource("tags eq 'foo'"); - } - - @Test(expectedExceptions = UnsupportedOperationException.class) - @Override - public void testQueryFilterJsonMap() throws Exception { - queryResource("meta/owner eq 'john'"); - } - @Test( expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Unknown object field: /tags/owner" ) @Override - // XXX Maybe this can be implemented by mapping to an unsatisfiable predicate + // XXX Maybe this can be implemented by mapping to an always false condition public void testQueryFilterInvalidField() throws Exception { queryResource("tags/owner eq 'john'"); } diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java index 2babae5ff..70308889b 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/AbstractTableHandlerTest.java @@ -54,6 +54,7 @@ import org.testng.ITestResult; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; /** @@ -253,14 +254,29 @@ public void testQueryFilterSimple() throws Exception { assertResourceValues(result.get(0), template); } - @Test - public void testQueryFilterComplex() throws Exception { + @DataProvider + public Object[] getQueryFilterComplexData() { + // We try to hit as many query filter processing code paths as possible + return new Object[][] { + { "name sw 'HELLO' and score eq 70 and ! visible eq false" }, + { "_id eq 'hello' and true" }, + { "_id eq 'hello' and ! false" }, + { "_id eq 'hello' and ! ( name co 'BYE' )" }, + { "_id eq 'hello' and ! ( name lt 'HELLO TO' or name gt 'HELLO TO' )" }, + { "_id eq 'hello' and ! ( name le 'A' or name ge 'Z' )" }, + { "_id eq 'hello' and ! ( score lt 70 or score gt 70 )" }, + { "_id eq 'hello' and ! ( score le 8 or score ge 80 )" }, + }; + } + + @Test(dataProvider = "getQueryFilterComplexData") + public void testQueryFilterComplex(String filter) throws Exception { createResource(RESOURCE_ID, Map.of("name", "HELLO TO", "score", 70, "visible", true)); createResource("with-different-name", Map.of("name", "GOOD BYE", "score", 70, "visible", true)); createResource("with-lower-score", Map.of("name", "HELLO TO", "score", 8, "visible", true)); createResource("with-not-visible", Map.of("name", "HELLO TO", "score", 80, "visible", false)); - var resultIds = queryResource("name sw 'HELLO' and score eq 70 and visible eq true").stream() + var resultIds = queryResource(filter).stream() .map(resource -> resource.get(OBJECT_ID)) .collect(Collectors.toSet()); assertEquals(resultIds, Set.of(RESOURCE_ID)); diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/HSQLDBMappedTableHandlerTest.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/HSQLDBMappedTableHandlerTest.java index dbd60b03b..fa9c1f015 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/HSQLDBMappedTableHandlerTest.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/handler/HSQLDBMappedTableHandlerTest.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.handler; @@ -39,4 +39,16 @@ protected TableHandler createTableHandler() throws Exception { ); } + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonList() throws Exception { + queryResource("tags eq 'foo'"); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonMap() throws Exception { + queryResource("meta/owner eq 'john'"); + } + } \ No newline at end of file diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLJSONUtilsTest.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLJSONUtilsTest.java new file mode 100644 index 000000000..823d2951a --- /dev/null +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/query/SQLJSONUtilsTest.java @@ -0,0 +1,49 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.1.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.1.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 Wren Security + */ +package org.forgerock.openidm.repo.jdbc.impl.query; + +import static org.forgerock.json.JsonPointer.ptr; +import static org.testng.Assert.assertEquals; + +import org.forgerock.json.JsonPointer; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * {@link SQLJSONUtils} test case. + */ +public class SQLJSONUtilsTest { + + @DataProvider + public Object[][] toSqlJsonPathData(){ + return new Object[][] { + { ptr(""), "$", "empty pointer" }, + { ptr("/"), "$", "root pointer" }, + { ptr("/foo/bar/baz"), "$.\"foo\".\"bar\".\"baz\"", "property chain" }, + { ptr("/foo/0"), "$.\"foo\"[0]", "valid zero index" }, + { ptr("/foo/012"), "$.\"foo\".\"012\"", "invalid zero-leading index" }, + { ptr("/foo/123"), "$.\"foo\"[123]", "multi-digit array index" }, + { ptr("/foo/123/456"), "$.\"foo\"[123][456]", "array index chain" }, + { ptr("/foo/123/bar"), "$.\"foo\"[123].\"bar\"", "property of array item" }, + }; + } + + @Test(dataProvider = "toSqlJsonPathData") + public void testToSqlJsonPath(JsonPointer pointer, String output, String message) { + assertEquals(SQLJSONUtils.toSqlJsonPath(pointer), output, message); + } + +} diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2MappedTableHandlerIT.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2MappedTableHandlerIT.java index c610f79df..70b7bccba 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2MappedTableHandlerIT.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/DB2MappedTableHandlerIT.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.vendor; @@ -40,4 +40,16 @@ protected TableHandler createTableHandler() throws Exception { ); } + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonList() throws Exception { + queryResource("tags eq 'foo'"); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonMap() throws Exception { + queryResource("meta/owner eq 'john'"); + } + } diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/H2MappedTableHandlerIT.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/H2MappedTableHandlerIT.java index e8ee1c355..303dd2a66 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/H2MappedTableHandlerIT.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/H2MappedTableHandlerIT.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.vendor; @@ -40,4 +40,16 @@ protected TableHandler createTableHandler() throws Exception { ); } + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonList() throws Exception { + queryResource("tags eq 'foo'"); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonMap() throws Exception { + queryResource("meta/owner eq 'john'"); + } + } diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLMappedTableHandlerIT.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLMappedTableHandlerIT.java index 6d83553b2..3fd8fc1e3 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLMappedTableHandlerIT.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MSSQLMappedTableHandlerIT.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.vendor; @@ -40,4 +40,16 @@ protected TableHandler createTableHandler() throws Exception { ); } + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonList() throws Exception { + queryResource("tags eq 'foo'"); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonMap() throws Exception { + queryResource("meta/owner eq 'john'"); + } + } diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MySQLMappedTableHandlerIT.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MySQLMappedTableHandlerIT.java index f2e4474c4..6335045b8 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MySQLMappedTableHandlerIT.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/MySQLMappedTableHandlerIT.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.vendor; @@ -40,4 +40,16 @@ protected TableHandler createTableHandler() throws Exception { ); } + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonList() throws Exception { + queryResource("tags eq 'foo'"); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonMap() throws Exception { + queryResource("meta/owner eq 'john'"); + } + } diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleMappedTableHandlerIT.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleMappedTableHandlerIT.java index 6f7361d4d..fb256b559 100644 --- a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleMappedTableHandlerIT.java +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/vendor/OracleMappedTableHandlerIT.java @@ -11,7 +11,7 @@ * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * - * Copyright 2024 Wren Security + * Copyright 2024-2026 Wren Security */ package org.forgerock.openidm.repo.jdbc.impl.vendor; @@ -57,4 +57,16 @@ protected JsonValue getColumnMapping() { return mapping; } + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonList() throws Exception { + queryResource("tags eq 'foo'"); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + @Override + public void testQueryFilterJsonMap() throws Exception { + queryResource("meta/owner eq 'john'"); + } + } diff --git a/openidm-zip/pom.xml b/openidm-zip/pom.xml index 33e89cd96..e4add630b 100644 --- a/openidm-zip/pom.xml +++ b/openidm-zip/pom.xml @@ -868,7 +868,6 @@ ch.qos.logback logback-classic - compile diff --git a/pom.xml b/pom.xml index 9e0b395aa..c13a60895 100644 --- a/pom.xml +++ b/pom.xml @@ -715,7 +715,7 @@ ch.qos.logback logback-classic - 1.2.13 + 1.5.32 test