Skip to content

Commit

Permalink
Array filtering support (Part #3): Postgres relational filter refacto…
Browse files Browse the repository at this point in the history
…ring (#189)
  • Loading branch information
suresh-prakash authored Jan 16, 2024
1 parent 614e082 commit 88a0440
Show file tree
Hide file tree
Showing 16 changed files with 381 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.builder;

import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
import org.hypertrace.core.documentstore.postgres.query.v1.vistors.PostgresSelectTypeExpressionVisitor;

public interface PostgresSelectExpressionParserBuilder {
PostgresSelectTypeExpressionVisitor build(final RelationalExpression expression);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.builder;

import static org.hypertrace.core.documentstore.postgres.utils.PostgresUtils.getType;

import lombok.AllArgsConstructor;
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser;
import org.hypertrace.core.documentstore.postgres.query.v1.vistors.PostgresConstantExpressionVisitor;
import org.hypertrace.core.documentstore.postgres.query.v1.vistors.PostgresDataAccessorIdentifierExpressionVisitor;
import org.hypertrace.core.documentstore.postgres.query.v1.vistors.PostgresFieldIdentifierExpressionVisitor;
import org.hypertrace.core.documentstore.postgres.query.v1.vistors.PostgresFunctionExpressionVisitor;
import org.hypertrace.core.documentstore.postgres.query.v1.vistors.PostgresSelectTypeExpressionVisitor;

@AllArgsConstructor
public class PostgresSelectExpressionParserBuilderImpl
implements PostgresSelectExpressionParserBuilder {

private final PostgresQueryParser postgresQueryParser;

@Override
public PostgresSelectTypeExpressionVisitor build(final RelationalExpression expression) {
switch (expression.getOperator()) {
case CONTAINS:
case NOT_CONTAINS:
case EXISTS:
case NOT_EXISTS:
case IN:
case NOT_IN:
return new PostgresFunctionExpressionVisitor(
new PostgresFieldIdentifierExpressionVisitor(this.postgresQueryParser));

default:
return new PostgresFunctionExpressionVisitor(
new PostgresDataAccessorIdentifierExpressionVisitor(
this.postgresQueryParser,
getType(expression.getRhs().accept(new PostgresConstantExpressionVisitor()))));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hypertrace.core.documentstore.Document;
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;

class PostgresContainsRelationalFilterParser implements PostgresRelationalFilterParser {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

@Override
public String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
final Object parsedRhs = expression.getRhs().accept(context.rhsParser());

final Object convertedRhs = prepareJsonValueForContainsOp(parsedRhs);
context.getParamsBuilder().addObjectParam(convertedRhs);

return String.format("%s @> ?::jsonb", parsedLhs);
}

static String prepareJsonValueForContainsOp(final Object value) {
if (value instanceof Document) {
return prepareDocumentValueForContainsOp((Document) value);
} else {
return prepareValueForContainsOp(value);
}
}

private static String prepareDocumentValueForContainsOp(final Document document) {
try {
final JsonNode node = OBJECT_MAPPER.readTree(document.toJson());
if (node.isArray()) {
return document.toJson();
} else {
return "[" + document.toJson() + "]";
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

private static String prepareValueForContainsOp(final Object value) {
try {
if (value instanceof Iterable<?>) {
return OBJECT_MAPPER.writeValueAsString(value);
} else {
return "[" + OBJECT_MAPPER.writeValueAsString(value) + "]";
}
} catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import org.hypertrace.core.documentstore.expression.impl.ConstantExpression;
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;

class PostgresExistsRelationalFilterParser implements PostgresRelationalFilterParser {
@Override
public String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
final boolean parsedRhs = !ConstantExpression.of(false).equals(expression.getRhs());
return parsedRhs
? String.format("%s IS NOT NULL", parsedLhs)
: String.format("%s IS NULL", parsedLhs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
import org.hypertrace.core.documentstore.postgres.Params;

class PostgresInRelationalFilterParser implements PostgresRelationalFilterParser {

@Override
public String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
final Iterable<Object> parsedRhs = expression.getRhs().accept(context.rhsParser());

return prepareFilterStringForInOperator(parsedLhs, parsedRhs, context.getParamsBuilder());
}

private String prepareFilterStringForInOperator(
final String parsedLhs,
final Iterable<Object> parsedRhs,
final Params.Builder paramsBuilder) {
// In order to make the behaviour same as for Mongo, the "NOT_IN" operator would match if the
// LHS and RHS have any intersection (i.e. non-empty intersection)
return StreamSupport.stream(parsedRhs.spliterator(), false)
.map(
value -> {
paramsBuilder.addObjectParam(value).addObjectParam(value);
return String.format(
"((jsonb_typeof(to_jsonb(%s)) = 'array' AND to_jsonb(%s) @> jsonb_build_array(?)) OR (jsonb_build_array(%s) @> jsonb_build_array(?)))",
parsedLhs, parsedLhs, parsedLhs);
})
.collect(Collectors.joining(" OR ", "(", ")"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;

class PostgresLikeRelationalFilterParser implements PostgresRelationalFilterParser {

@Override
public String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
final Object parsedLhs = expression.getLhs().accept(context.lhsParser());
final Object parsedRhs = expression.getRhs().accept(context.rhsParser());

// Append ".*" at beginning and end of value to do a regex
// Like operator is only applicable for string RHS. Hence, convert the RHS to string
final String rhsValue = ".*" + parsedRhs + ".*";
context.getParamsBuilder().addObjectParam(rhsValue);

// Case-insensitive regex search
return String.format("%s ~* ?", parsedLhs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import static org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.PostgresContainsRelationalFilterParser.prepareJsonValueForContainsOp;

import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;

class PostgresNotContainsRelationalFilterParser implements PostgresRelationalFilterParser {
@Override
public String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
final Object parsedRhs = expression.getRhs().accept(context.rhsParser());

final Object convertedRhs = prepareJsonValueForContainsOp(parsedRhs);
context.getParamsBuilder().addObjectParam(convertedRhs);

return String.format("%s IS NULL OR NOT %s @> ?::jsonb", parsedLhs, parsedLhs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import org.hypertrace.core.documentstore.expression.impl.ConstantExpression;
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;

class PostgresNotExistsRelationalFilterParser implements PostgresRelationalFilterParser {
@Override
public String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
final boolean parsedRhs = ConstantExpression.of(false).equals(expression.getRhs());
return parsedRhs
? String.format("%s IS NOT NULL", parsedLhs)
: String.format("%s IS NULL", parsedLhs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;

class PostgresNotInRelationalFilterParser implements PostgresRelationalFilterParser {
private static final PostgresInRelationalFilterParser inRelationalFilterParser =
new PostgresInRelationalFilterParser();

@Override
public String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
final String parsedInExpression = inRelationalFilterParser.parse(expression, context);
return String.format("%s IS NULL OR NOT (%s)", parsedLhs, parsedInExpression);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import lombok.Builder;
import lombok.Builder.Default;
import lombok.Value;
import lombok.experimental.Accessors;
import lombok.experimental.Delegate;
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser;
import org.hypertrace.core.documentstore.postgres.query.v1.vistors.PostgresConstantExpressionVisitor;
import org.hypertrace.core.documentstore.postgres.query.v1.vistors.PostgresSelectTypeExpressionVisitor;

public interface PostgresRelationalFilterParser {
String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context);

@Value
@Builder
@Accessors(fluent = true)
class PostgresRelationalFilterContext {
PostgresSelectTypeExpressionVisitor lhsParser;

@Default
PostgresSelectTypeExpressionVisitor rhsParser = new PostgresConstantExpressionVisitor();

@Delegate PostgresQueryParser postgresQueryParser;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;

public interface PostgresRelationalFilterParserFactory {
PostgresRelationalFilterParser parser(final RelationalExpression expression);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import static java.util.Map.entry;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.CONTAINS;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.EXISTS;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.IN;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.LIKE;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NOT_CONTAINS;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NOT_EXISTS;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NOT_IN;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.STARTS_WITH;

import com.google.common.collect.Maps;
import java.util.Map;
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
import org.hypertrace.core.documentstore.expression.operators.RelationalOperator;

public class PostgresRelationalFilterParserFactoryImpl
implements PostgresRelationalFilterParserFactory {
private static final Map<RelationalOperator, PostgresRelationalFilterParser> parserMap =
Maps.immutableEnumMap(
Map.ofEntries(
entry(CONTAINS, new PostgresContainsRelationalFilterParser()),
entry(NOT_CONTAINS, new PostgresNotContainsRelationalFilterParser()),
entry(EXISTS, new PostgresExistsRelationalFilterParser()),
entry(NOT_EXISTS, new PostgresNotExistsRelationalFilterParser()),
entry(IN, new PostgresInRelationalFilterParser()),
entry(NOT_IN, new PostgresNotInRelationalFilterParser()),
entry(LIKE, new PostgresLikeRelationalFilterParser()),
entry(STARTS_WITH, new PostgresStartsWithRelationalFilterParser())));
private static final PostgresStandardRelationalFilterParser
postgresStandardRelationalFilterParser = new PostgresStandardRelationalFilterParser();

@Override
public PostgresRelationalFilterParser parser(final RelationalExpression expression) {
return parserMap.getOrDefault(expression.getOperator(), postgresStandardRelationalFilterParser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;

class PostgresStandardRelationalFilterParser implements PostgresRelationalFilterParser {
private static final PostgresStandardRelationalOperatorMapper mapper =
new PostgresStandardRelationalOperatorMapper();

@Override
public String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
final Object parsedLhs = expression.getLhs().accept(context.lhsParser());
final String operator = mapper.getMapping(expression.getOperator());
final Object parsedRhs = expression.getRhs().accept(context.rhsParser());

context.getParamsBuilder().addObjectParam(parsedRhs);
return String.format("%s %s ?", parsedLhs, operator);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import static java.util.Map.entry;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.EQ;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.GT;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.GTE;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.LT;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.LTE;
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NEQ;

import com.google.common.collect.Maps;
import java.util.Map;
import java.util.Optional;
import org.hypertrace.core.documentstore.expression.operators.RelationalOperator;

class PostgresStandardRelationalOperatorMapper {
private static final Map<RelationalOperator, String> mapping =
Maps.immutableEnumMap(
Map.ofEntries(
entry(EQ, "="),
entry(NEQ, "!="),
entry(GT, ">"),
entry(LT, "<"),
entry(GTE, ">="),
entry(LTE, "<=")));

String getMapping(final RelationalOperator operator) {
return Optional.ofNullable(mapping.get(operator))
.orElseThrow(() -> new UnsupportedOperationException("Unsupported operator: " + operator));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;

import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;

class PostgresStartsWithRelationalFilterParser implements PostgresRelationalFilterParser {
@Override
public String parse(
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
final Object parsedRhs = expression.getRhs().accept(context.rhsParser());

context.getParamsBuilder().addObjectParam(parsedRhs);
return String.format("%s::text ^@ ?", parsedLhs);
}
}
Loading

0 comments on commit 88a0440

Please sign in to comment.