Skip to content

Commit b711919

Browse files
committed
Add support for subqueries in filters (fix #602)
1 parent 405e1b0 commit b711919

File tree

10 files changed

+265
-80
lines changed

10 files changed

+265
-80
lines changed

gemma-core/src/main/java/ubic/gemma/persistence/service/AbstractFilteringVoEnabledDao.java

+62-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ubic.gemma.model.common.Identifiable;
1313
import ubic.gemma.persistence.util.Filter;
1414
import ubic.gemma.persistence.util.Sort;
15+
import ubic.gemma.persistence.util.Subquery;
1516

1617
import javax.annotation.Nullable;
1718
import javax.annotation.OverridingMethodsMustInvokeSuper;
@@ -53,6 +54,11 @@ private static class Key {
5354
*/
5455
private final Set<String> filterableProperties = new HashSet<>();
5556

57+
/**
58+
* Subset of {@link #filterableProperties} that should use a subquery for filtering.
59+
*/
60+
private final Set<String> filterablePropertiesViaSubquery = new HashSet<>();
61+
5662
/**
5763
* Aliases for filterable properties.
5864
*/
@@ -89,15 +95,22 @@ protected class FilterablePropertiesConfigurer {
8995

9096
private final Map<String, Class<?>> entityByPrefix = new HashMap<>();
9197

98+
public void registerProperty( String propertyName ) {
99+
registerProperty( propertyName, false );
100+
}
101+
92102
/**
93103
* Register a given property.
94104
* @throws IllegalArgumentException if the property is already registered
95105
*/
96-
public void registerProperty( String propertyName ) {
106+
public void registerProperty( String propertyName, boolean useSubquery ) {
97107
if ( getFilterablePropertyMeta( propertyName ) == null ) {
98108
throw new IllegalArgumentException( "Property %s does not have any associated meta information." );
99109
}
100110
if ( filterableProperties.add( propertyName ) ) {
111+
if ( useSubquery ) {
112+
filterablePropertiesViaSubquery.add( propertyName );
113+
}
101114
log.debug( String.format( "Registered property %s.", propertyName ) );
102115
} else {
103116
throw new IllegalArgumentException( String.format( "Filterable property %s is already registered.",
@@ -150,6 +163,10 @@ public void unregisterProperties( Predicate<? super String> predicate ) throws I
150163
}
151164
}
152165

166+
public void registerEntity( String prefix, Class<?> entityClass, int maxDepth ) throws IllegalArgumentException {
167+
registerEntity( prefix, entityClass, maxDepth, false );
168+
}
169+
153170
/**
154171
* Register an entity available at a given prefix.
155172
* <p>
@@ -159,10 +176,11 @@ public void unregisterProperties( Predicate<? super String> predicate ) throws I
159176
* @param maxDepth maximum depth for visiting properties. For example, zero would expose no property but the
160177
* entity itself, 1 would expose the properties of the alias, 2 would expose the properties
161178
* of any entity directly related to the given entity, etc.
179+
* @param useSubquery whether to use a subquery when filtering by this entity (and its descendant)
162180
* @throws IllegalArgumentException if no entity of the given type is registered under the given prefix or if
163181
* the prefix is invalid
164182
*/
165-
public void registerEntity( String prefix, Class<?> entityClass, int maxDepth ) throws IllegalArgumentException {
183+
public void registerEntity( String prefix, Class<?> entityClass, int maxDepth, boolean useSubquery ) throws IllegalArgumentException {
166184
if ( !prefix.isEmpty() && !prefix.endsWith( "." ) ) {
167185
throw new IllegalArgumentException( "A non-empty prefix must end with a '.' character." );
168186
}
@@ -190,7 +208,7 @@ public void registerEntity( String prefix, Class<?> entityClass, int maxDepth )
190208
Type propertyType = propertyTypes[i];
191209
if ( propertyType.isEntityType() ) {
192210
if ( maxDepth > 1 ) {
193-
registerEntity( prefix + propertyName + ".", propertyType.getReturnedClass(), maxDepth - 1 );
211+
registerEntity( prefix + propertyName + ".", propertyType.getReturnedClass(), maxDepth - 1, useSubquery );
194212
} else {
195213
log.debug( String.format( "Max depth reached, will not recurse into %s", propertyName ) );
196214
}
@@ -201,7 +219,7 @@ public void registerEntity( String prefix, Class<?> entityClass, int maxDepth )
201219
log.debug( String.format( "Property %s%s of type %s was excluded in %s: BLOBs and CLOBs are not exposed by default.",
202220
prefix, propertyName, propertyType.getName(), entityClass.getName() ) );
203221
} else if ( Filter.getConversionService().canConvert( String.class, propertyType.getReturnedClass() ) ) {
204-
registerProperty( prefix + propertyName );
222+
registerProperty( prefix + propertyName, useSubquery );
205223
} else {
206224
log.warn( String.format( "Property %s%s of type %s in %s is not supported and will be skipped.",
207225
prefix, propertyName, propertyType.getReturnedClass().getName(), entityClass.getName() ) );
@@ -239,16 +257,23 @@ public void unregisterEntity( String prefix, Class<?> entityClass ) {
239257
}
240258
}
241259

260+
/**
261+
* @see #registerAlias(String, String, Class, String, int, boolean)
262+
*/
263+
public void registerAlias( String prefix, @Nullable String objectAlias, Class<?> propertyType, @Nullable String aliasFor, int maxDepth ) {
264+
registerAlias( prefix, objectAlias, propertyType, aliasFor, maxDepth, false );
265+
}
266+
242267
/**
243268
* Register an alias for a property.
244269
* <p>
245270
* This also registers a property under the given prefix as per {@link #registerEntity(String, Class, int)}.
246271
* @param objectAlias internal alias used to refer to the entity as per {@link Filter#getObjectAlias()}.
247272
* @see #registerEntity(String, Class, int)
248273
*/
249-
public void registerAlias( String prefix, @Nullable String objectAlias, Class<?> propertyType, @Nullable String aliasFor, int maxDepth ) {
274+
public void registerAlias( String prefix, @Nullable String objectAlias, Class<?> propertyType, @Nullable String aliasFor, int maxDepth, boolean useSubquery ) {
250275
filterablePropertyAliases.add( new FilterablePropertyAlias( prefix, objectAlias, propertyType, aliasFor ) );
251-
registerEntity( prefix, propertyType, maxDepth );
276+
registerEntity( prefix, propertyType, maxDepth, useSubquery );
252277
log.debug( String.format( "Registered alias for %s (%s) %s.", objectAlias, propertyType.getName(), summarizePrefix( prefix ) ) );
253278
}
254279

@@ -285,13 +310,42 @@ public List<Object> getFilterablePropertyAllowedValues( String propertyName ) th
285310
@Override
286311
public final Filter getFilter( String property, Filter.Operator operator, String value ) {
287312
FilterablePropertyMeta propertyMeta = getFilterablePropertyMeta( property );
288-
return Filter.parse( propertyMeta.objectAlias, propertyMeta.propertyName, propertyMeta.propertyType, operator, value, property );
313+
return nestIfSubquery( Filter.parse( propertyMeta.objectAlias, propertyMeta.propertyName, propertyMeta.propertyType, operator, value, property ),
314+
( property ) );
289315
}
290316

291317
@Override
292318
public final Filter getFilter( String property, Filter.Operator operator, Collection<String> values ) {
293319
FilterablePropertyMeta propertyMeta = getFilterablePropertyMeta( property );
294-
return Filter.parse( propertyMeta.objectAlias, propertyMeta.propertyName, propertyMeta.propertyType, operator, values, property );
320+
return nestIfSubquery( Filter.parse( propertyMeta.objectAlias, propertyMeta.propertyName, propertyMeta.propertyType, operator, values, property ),
321+
property );
322+
}
323+
324+
private Filter nestIfSubquery( Filter f, String propertyName ) {
325+
if ( filterablePropertiesViaSubquery.contains( propertyName ) ) {
326+
String entityName = getSessionFactory().getClassMetadata( elementClass ).getEntityName();
327+
List<Subquery.Alias> aliases;
328+
if ( f.getObjectAlias() != null ) {
329+
aliases = null;
330+
for ( FilterablePropertyAlias fpa : filterablePropertyAliases ) {
331+
if ( f.getObjectAlias().equals( fpa.getObjectAlias() ) ) {
332+
// FIXME: the prefix is not always a valid path
333+
aliases = Collections.singletonList( new Subquery.Alias( fpa.prefix.substring( 0, fpa.prefix.length() - 1 ), fpa.getObjectAlias() ) );
334+
break;
335+
}
336+
}
337+
if ( aliases == null ) {
338+
throw new IllegalArgumentException();
339+
}
340+
} else {
341+
// the property refers to the root entity, no need for aliases
342+
aliases = Collections.emptyList();
343+
}
344+
return Filter.by( objectAlias, getIdentifierPropertyName(), Long.class, Filter.Operator.inSubquery,
345+
new Subquery( entityName, getIdentifierPropertyName(), aliases, f ) );
346+
} else {
347+
return f;
348+
}
295349
}
296350

297351
@Override

gemma-core/src/main/java/ubic/gemma/persistence/service/expression/arrayDesign/ArrayDesignDaoImpl.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1084,7 +1084,7 @@ protected void configureFilterableProperties( FilterablePropertiesConfigurer con
10841084
// see https://github.com/PavlidisLab/Gemma/issues/546
10851085
String recursiveProperty = String.join( "|", new String[] { "subsumingArrayDesign", "mergedInto", "alternativeTo" } );
10861086
configurer.unregisterProperties( Pattern.compile( "^(" + recursiveProperty + ")\\.(" + recursiveProperty + ")\\..+$" ).asPredicate() );
1087-
configurer.registerAlias( "externalReferences.", EXTERNAL_REFERENCE_ALIAS, DatabaseEntry.class, null, 2 );
1087+
configurer.registerAlias( "externalReferences.", EXTERNAL_REFERENCE_ALIAS, DatabaseEntry.class, null, 2, true );
10881088
configurer.registerAlias( "taxon.", PRIMARY_TAXON_ALIAS, Taxon.class, "primaryTaxon", 2 );
10891089
}
10901090

gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDaoImpl.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -1815,16 +1815,16 @@ protected void configureFilterableProperties( FilterablePropertiesConfigurer con
18151815
configurer.unregisterProperty( "primaryPublication.pubAccession.Uri" );
18161816

18171817
// attached terms
1818-
configurer.registerAlias( "characteristics.", CHARACTERISTIC_ALIAS, Characteristic.class, null, 1 );
1818+
configurer.registerAlias( "characteristics.", CHARACTERISTIC_ALIAS, Characteristic.class, null, 1, true );
18191819
configurer.unregisterProperty( "characteristics.originalValue" );
1820-
configurer.registerAlias( "experimentalDesign.experimentalFactors.factorValues.characteristics.", FACTOR_VALUE_CHARACTERISTIC_ALIAS, Characteristic.class, null, 1 );
1820+
configurer.registerAlias( "experimentalDesign.experimentalFactors.factorValues.characteristics.", FACTOR_VALUE_CHARACTERISTIC_ALIAS, Characteristic.class, null, 1, true );
18211821
configurer.unregisterProperty( "experimentalDesign.experimentalFactors.factorValues.characteristics.originalValue" );
1822-
configurer.registerAlias( "bioAssays.sampleUsed.characteristics.", BIO_MATERIAL_CHARACTERISTIC_ALIAS, Characteristic.class, null, 1 );
1822+
configurer.registerAlias( "bioAssays.sampleUsed.characteristics.", BIO_MATERIAL_CHARACTERISTIC_ALIAS, Characteristic.class, null, 1, true );
18231823
configurer.unregisterProperty( "bioAssays.sampleUsed.characteristics.originalValue" );
1824-
configurer.registerAlias( "allCharacteristics.", ALL_CHARACTERISTIC_ALIAS, Characteristic.class, null, 1 );
1824+
configurer.registerAlias( "allCharacteristics.", ALL_CHARACTERISTIC_ALIAS, Characteristic.class, null, 1, true );
18251825
configurer.unregisterProperty( "allCharacteristics.originalValue" );
18261826

1827-
configurer.registerAlias( "bioAssays.", BIO_ASSAY_ALIAS, BioAssay.class, null, 2 );
1827+
configurer.registerAlias( "bioAssays.", BIO_ASSAY_ALIAS, BioAssay.class, null, 2, true );
18281828
configurer.unregisterProperty( "bioAssays.accession.Uri" );
18291829

18301830
// this is not useful, unless we add an alias to the alternate names

gemma-core/src/main/java/ubic/gemma/persistence/util/Filter.java

+13-2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ public static <T> Filter by( @Nullable String objectAlias, String propertyName,
133133
return new Filter( objectAlias, propertyName, propertyType, operator, requiredValues, originalProperty );
134134
}
135135

136+
public static <T> Filter by( @Nullable String objectAlias, String propertyName, Class<T> propertyType, Operator operator, Subquery requiredValues, String originalProperty ) {
137+
return new Filter( objectAlias, propertyName, propertyType, operator, requiredValues, originalProperty );
138+
}
139+
136140
/**
137141
* Create a new filter without an original property.
138142
* @see #by(String, String, Class, Operator, Object, String)
@@ -149,6 +153,10 @@ public static <T> Filter by( @Nullable String objectAlias, String propertyName,
149153
return new Filter( objectAlias, propertyName, propertyType, operator, requiredValues, null );
150154
}
151155

156+
public static <T> Filter by( @Nullable String objectAlias, String propertyName, Class<T> propertyType, Operator operator, Subquery requiredValues ) {
157+
return new Filter( objectAlias, propertyName, propertyType, operator, requiredValues, null );
158+
}
159+
152160
/**
153161
* Parse filter where the right-hand side is a scalar.
154162
*
@@ -194,7 +202,8 @@ public enum Operator {
194202
greaterThan( ">", true, null ),
195203
lessOrEq( "<=", true, null ),
196204
greaterOrEq( ">=", true, null ),
197-
in( "in", true, Collection.class );
205+
in( "in", true, Collection.class ),
206+
inSubquery( "in", true, Subquery.class );
198207

199208
/**
200209
* Token used when parsing filter input.
@@ -254,7 +263,9 @@ public String toOriginalString() {
254263

255264
private String toString( boolean withOriginalProperties ) {
256265
String requiredValueString;
257-
if ( requiredValue instanceof Collection ) {
266+
if ( requiredValue instanceof Subquery ) {
267+
return ( ( Subquery ) requiredValue ).getFilter().toString( withOriginalProperties );
268+
} else if ( requiredValue instanceof Collection ) {
258269
requiredValueString = "(" + ( ( Collection<?> ) requiredValue ).stream()
259270
.map( e -> conversionService.convert( e, String.class ) )
260271
.map( Filter::quoteIfNecessary )

0 commit comments

Comments
 (0)