diff --git a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreTest.java b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreTest.java index 035b0051..872aa439 100644 --- a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreTest.java +++ b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -2099,6 +2100,25 @@ public void testSearch(String dataStoreName) throws IOException { assertTrue(exception.getMessage().contains(expected)); } } + + // test for selections + { + Query query = new Query(); + query.addSelection("entityId"); + query.addSelection("entityType"); + query.setFilter(new Filter(Filter.Op.EQ, "_id", key1.toString())); + Iterator results = collection.search(query); + List documents = new ArrayList<>(); + while (results.hasNext()) { + documents.add(results.next()); + } + Assertions.assertEquals(1, documents.size()); + Map result = + OBJECT_MAPPER.readValue(documents.get(0).toJson(), new TypeReference<>() {}); + Assertions.assertEquals(2, result.size()); + Assertions.assertEquals("SERVICE", result.get("entityType")); + Assertions.assertEquals("e3ffc6f0-fc92-3a9c-9fa0-26269184d1aa", result.get("entityId")); + } } @ParameterizedTest diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/PostgresCollection.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/PostgresCollection.java index 10b45926..be86284c 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/PostgresCollection.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/PostgresCollection.java @@ -1,6 +1,7 @@ package org.hypertrace.core.documentstore.postgres; import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -324,8 +325,11 @@ public BulkUpdateResult bulkOperationOnArrayValue(BulkArrayValueUpdateRequest re @Override public CloseableIterator search(Query query) { + String selection = PostgresQueryParser.parseSelections(query.getSelections()); + StringBuilder sqlBuilder = + new StringBuilder(String.format("SELECT %s FROM ", selection)).append(collectionName); + String filters = null; - StringBuilder sqlBuilder = new StringBuilder("SELECT * FROM ").append(collectionName); Params.Builder paramsBuilder = Params.newBuilder(); // If there is a filter in the query, parse it fully. @@ -333,8 +337,6 @@ public CloseableIterator search(Query query) { filters = PostgresQueryParser.parseFilter(query.getFilter(), paramsBuilder); } - LOGGER.debug("Sending query to PostgresSQL: {} : {}", collectionName, filters); - if (filters != null) { sqlBuilder.append(" WHERE ").append(filters); } @@ -354,13 +356,20 @@ public CloseableIterator search(Query query) { sqlBuilder.append(" OFFSET ").append(offset); } + String pgSqlQuery = sqlBuilder.toString(); try { PreparedStatement preparedStatement = - buildPreparedStatement(sqlBuilder.toString(), paramsBuilder.build()); + buildPreparedStatement(pgSqlQuery, paramsBuilder.build()); + LOGGER.debug("Executing search query to PostgresSQL:{}", preparedStatement.toString()); ResultSet resultSet = preparedStatement.executeQuery(); - return new PostgresResultIterator(resultSet); + CloseableIterator closeableIterator = + query.getSelections().size() > 0 + ? new PostgresResultIteratorWithMetaData(resultSet) + : new PostgresResultIterator(resultSet); + return closeableIterator; } catch (SQLException e) { - LOGGER.error("SQLException querying documents. query: {}", query, e); + LOGGER.error( + "SQLException in querying documents - query: {}, sqlQuery:{}", query, pgSqlQuery, e); } return EMPTY_ITERATOR; @@ -739,8 +748,7 @@ private Optional getCreatedTime(Key key) throws IOException { private CloseableIterator searchDocsForKeys(Set keys) { List keysAsStr = keys.stream().map(Key::toString).collect(Collectors.toList()); - Query query = - new Query().withSelection("*").withFilter(new Filter(Filter.Op.IN, ID, keysAsStr)); + Query query = new Query().withFilter(new Filter(Filter.Op.IN, ID, keysAsStr)); return search(query); } @@ -778,6 +786,7 @@ private CloseableIterator executeQueryV1( try { PreparedStatement preparedStatement = buildPreparedStatement(sqlQuery, queryParser.getParamsBuilder().build()); + LOGGER.debug("Executing executeQueryV1 sqlQuery:{}", preparedStatement.toString()); ResultSet resultSet = preparedStatement.executeQuery(); CloseableIterator closeableIterator = query.getSelections().size() > 0 @@ -1075,11 +1084,7 @@ protected Document prepareDocument() throws SQLException, IOException { Map jsonNode = new HashMap(); for (int i = 1; i <= columnCount; i++) { String columnName = resultSetMetaData.getColumnName(i); - int columnType = resultSetMetaData.getColumnType(i); - String columnValue = - columnType == Types.ARRAY - ? MAPPER.writeValueAsString(resultSet.getArray(i).getArray()) - : resultSet.getString(i); + String columnValue = getColumnValue(resultSetMetaData, columnName, i); if (StringUtils.isNotEmpty(columnValue)) { JsonNode leafNodeValue = MAPPER.readTree(columnValue); if (PostgresUtils.isEncodedNestedField(columnName)) { @@ -1093,6 +1098,24 @@ protected Document prepareDocument() throws SQLException, IOException { return new JSONDocument(MAPPER.writeValueAsString(jsonNode)); } + private String getColumnValue( + ResultSetMetaData resultSetMetaData, String columnName, int columnIndex) + throws SQLException, JsonProcessingException { + int columnType = resultSetMetaData.getColumnType(columnIndex); + // check for array + if (columnType == Types.ARRAY) { + return MAPPER.writeValueAsString(resultSet.getArray(columnIndex).getArray()); + } + + // check for ID column + if (columnName.equals(ID)) { + return MAPPER.writeValueAsString(resultSet.getString(columnIndex)); + } + + // rest of the columns + return resultSet.getString(columnIndex); + } + private void handleNestedField( String columnName, Map rootNode, JsonNode leafNodeValue) { List keys = PostgresUtils.splitNestedField(columnName); diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/PostgresQueryParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/PostgresQueryParser.java index 1e002fd9..2f318240 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/PostgresQueryParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/PostgresQueryParser.java @@ -1,149 +1,43 @@ package org.hypertrace.core.documentstore.postgres; -import static org.hypertrace.core.documentstore.Collection.UNSUPPORTED_QUERY_OPERATION; -import static org.hypertrace.core.documentstore.postgres.PostgresCollection.CREATED_AT; -import static org.hypertrace.core.documentstore.postgres.PostgresCollection.DOCUMENT; -import static org.hypertrace.core.documentstore.postgres.PostgresCollection.DOC_PATH_SEPARATOR; -import static org.hypertrace.core.documentstore.postgres.PostgresCollection.ID; -import static org.hypertrace.core.documentstore.postgres.PostgresCollection.UPDATED_AT; - import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Set; +import java.util.Optional; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.hypertrace.core.documentstore.Filter; import org.hypertrace.core.documentstore.OrderBy; import org.hypertrace.core.documentstore.postgres.Params.Builder; +import org.hypertrace.core.documentstore.postgres.utils.PostgresUtils; class PostgresQueryParser { - private static final String QUESTION_MARK = "?"; - // postgres jsonb uses `->` instead of `.` for json field access - private static final String JSON_FIELD_ACCESSOR = "->"; - // postgres operator to fetch the value of json object as text. - private static final String JSON_DATA_ACCESSOR = "->>"; - private static final Set OUTER_COLUMNS = - new HashSet<>() { - { - add(CREATED_AT); - add(ID); - add(UPDATED_AT); - } - }; + static String parseSelections(List selections) { + return Optional.of( + selections.stream() + .map( + selection -> + String.format( + "%s AS \"%s\"", + PostgresUtils.prepareFieldAccessorExpr( + selection, PostgresUtils.DOCUMENT_COLUMN), + selection)) + .collect(Collectors.joining(","))) + .filter(StringUtils::isNotBlank) + .orElse("*"); + } static String parseFilter(Filter filter, Builder paramsBuilder) { if (filter.isComposite()) { return parseCompositeFilter(filter, paramsBuilder); } else { - return parseNonCompositeFilter(filter, paramsBuilder); - } - } - - static String parseNonCompositeFilter(Filter filter, Builder paramsBuilder) { - Filter.Op op = filter.getOp(); - Object value = filter.getValue(); - String fieldName = filter.getFieldName(); - String fullFieldName = prepareCast(prepareFieldDataAccessorExpr(fieldName), value); - StringBuilder filterString = new StringBuilder(fullFieldName); - String sqlOperator; - Boolean isMultiValued = false; - switch (op) { - case EQ: - sqlOperator = " = "; - break; - case GT: - sqlOperator = " > "; - break; - case LT: - sqlOperator = " < "; - break; - case GTE: - sqlOperator = " >= "; - break; - case LTE: - sqlOperator = " <= "; - break; - case LIKE: - // Case insensitive regex search, Append % at beginning and end of value to do a regex - // search - sqlOperator = " ILIKE "; - value = "%" + value + "%"; - break; - case NOT_IN: - // NOTE: Below two points - // 1. both NOT_IN and IN filter currently limited to non-array field - // - https://github.com/hypertrace/document-store/issues/32#issuecomment-781411676 - // 2. To make semantically opposite filter of IN, we need to check for if key is not present - // Ref in context of NEQ - - // https://github.com/hypertrace/document-store/pull/20#discussion_r547101520Other - // so, we need - "document->key IS NULL OR document->key->> NOT IN (v1, v2)" - StringBuilder notInFilterString = prepareFieldAccessorExpr(fieldName); - if (notInFilterString != null) { - filterString = notInFilterString.append(" IS NULL OR ").append(fullFieldName); - } - sqlOperator = " NOT IN "; - isMultiValued = true; - value = prepareParameterizedStringForList((List) value, paramsBuilder); - break; - case IN: - // NOTE: both NOT_IN and IN filter currently limited to non-array field - // - https://github.com/hypertrace/document-store/issues/32#issuecomment-781411676 - sqlOperator = " IN "; - isMultiValued = true; - value = prepareParameterizedStringForList((List) value, paramsBuilder); - break; - case NOT_EXISTS: - sqlOperator = " IS NULL "; - value = null; - // For fields inside jsonb - StringBuilder notExists = prepareFieldAccessorExpr(fieldName); - if (notExists != null) { - filterString = notExists; - } - break; - case EXISTS: - sqlOperator = " IS NOT NULL "; - value = null; - // For fields inside jsonb - StringBuilder exists = prepareFieldAccessorExpr(fieldName); - if (exists != null) { - filterString = exists; - } - break; - case NEQ: - sqlOperator = " != "; - // https://github.com/hypertrace/document-store/pull/20#discussion_r547101520 - // The expected behaviour is to get all documents which either satisfy non equality - // condition - // or the key doesn't exist in them - // Semantics for handling if key not exists and if it exists, its value - // doesn't equal to the filter for Jsonb document will be done as: - // "document->key IS NULL OR document->key->> != value" - StringBuilder notEquals = prepareFieldAccessorExpr(fieldName); - // For fields inside jsonb - if (notEquals != null) { - filterString = notEquals.append(" IS NULL OR ").append(fullFieldName); - } - break; - case CONTAINS: - // TODO: Matches condition inside an array of documents - default: - throw new UnsupportedOperationException(UNSUPPORTED_QUERY_OPERATION); - } - - filterString.append(sqlOperator); - if (value != null) { - if (isMultiValued) { - filterString.append(value); - } else { - filterString.append(QUESTION_MARK); - paramsBuilder.addObjectParam(value); - } + return PostgresUtils.parseNonCompositeFilter( + filter.getFieldName(), + PostgresUtils.DOCUMENT_COLUMN, + filter.getOp().toString(), + filter.getValue(), + paramsBuilder); } - String filters = filterString.toString(); - return filters; } static String parseCompositeFilter(Filter filter, Builder paramsBuilder) { @@ -179,82 +73,11 @@ static String parseOrderBys(List orderBys) { return orderBys.stream() .map( orderBy -> - prepareFieldDataAccessorExpr(orderBy.getField()) + PostgresUtils.prepareFieldDataAccessorExpr( + orderBy.getField(), PostgresUtils.DOCUMENT_COLUMN) + " " + (orderBy.isAsc() ? "ASC" : "DESC")) .filter(str -> !StringUtils.isEmpty(str)) .collect(Collectors.joining(" , ")); } - - private static String prepareParameterizedStringForList( - List values, Params.Builder paramsBuilder) { - String collect = - values.stream() - .map( - val -> { - paramsBuilder.addObjectParam(val); - return QUESTION_MARK; - }) - .collect(Collectors.joining(", ")); - return "(" + collect + ")"; - } - - private static StringBuilder prepareFieldAccessorExpr(String fieldName) { - // Generate json field accessor statement - if (!OUTER_COLUMNS.contains(fieldName)) { - StringBuilder filterString = new StringBuilder(DOCUMENT); - String[] nestedFields = fieldName.split(DOC_PATH_SEPARATOR); - for (String nestedField : nestedFields) { - filterString.append(JSON_FIELD_ACCESSOR).append("'").append(nestedField).append("'"); - } - return filterString; - } - // Field accessor is only applicable to jsonb fields, return null otherwise - return null; - } - - /** - * Add field prefix for searching into json document based on postgres syntax, handles nested - * keys. Note: It doesn't handle array elements in json document. e.g SELECT * FROM TABLE where - * document ->> 'first' = 'name' and document -> 'address' ->> 'pin' = "00000" - */ - private static String prepareFieldDataAccessorExpr(String fieldName) { - StringBuilder fieldPrefix = new StringBuilder(fieldName); - if (!OUTER_COLUMNS.contains(fieldName)) { - fieldPrefix = new StringBuilder(DOCUMENT); - String[] nestedFields = fieldName.split(DOC_PATH_SEPARATOR); - for (int i = 0; i < nestedFields.length - 1; i++) { - fieldPrefix.append(JSON_FIELD_ACCESSOR).append("'").append(nestedFields[i]).append("'"); - } - fieldPrefix - .append(JSON_DATA_ACCESSOR) - .append("'") - .append(nestedFields[nestedFields.length - 1]) - .append("'"); - } - return fieldPrefix.toString(); - } - - private static String prepareCast(String field, Object value) { - String fmt = "CAST (%s AS %s)"; - - // handle the case if the value type is collection for filter operator - `IN` - // Currently, for `IN` operator, we are considering List collection, and it is fair - // assumption that all its value of the same types. Based on that and for consistency - // we will use CAST ( as ) for all non string operator. - // Ref : https://github.com/hypertrace/document-store/pull/30#discussion_r571782575 - - if (value instanceof List && ((List) value).size() > 0) { - List listValue = (List) value; - value = listValue.get(0); - } - - if (value instanceof Number) { - return String.format(fmt, field, "NUMERIC"); - } else if (value instanceof Boolean) { - return String.format(fmt, field, "BOOLEAN"); - } else /* default is string */ { - return field; - } - } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/utils/PostgresUtils.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/utils/PostgresUtils.java index 9448f322..27e60123 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/utils/PostgresUtils.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/utils/PostgresUtils.java @@ -15,11 +15,8 @@ import org.apache.commons.lang3.StringUtils; import org.hypertrace.core.documentstore.postgres.Params; import org.hypertrace.core.documentstore.postgres.Params.Builder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class PostgresUtils { - private static final Logger LOGGER = LoggerFactory.getLogger(PostgresUtils.class); private static final String QUESTION_MARK = "?"; private static final String JSON_FIELD_ACCESSOR = "->"; private static final String JSON_DATA_ACCESSOR = "->>"; @@ -47,10 +44,8 @@ public static StringBuilder prepareFieldAccessorExpr(String fieldName, String co } return filterString; } - // Field accessor is only applicable to jsonb fields, return null otherwise - LOGGER.warn( - "Returning null string for field name {} and column name {}", fieldName, columnName); - return null; + // There is no need of field accessor in case of outer column. + return new StringBuilder(fieldName); } /** @@ -187,7 +182,7 @@ public static String parseNonCompositeFilter( // https://github.com/hypertrace/document-store/pull/20#discussion_r547101520Other // so, we need - "document->key IS NULL OR document->key->> NOT IN (v1, v2)" StringBuilder notInFilterString = prepareFieldAccessorExpr(fieldName, columnName); - if (notInFilterString != null) { + if (notInFilterString != null && !OUTER_COLUMNS.contains(fieldName)) { filterString = notInFilterString.append(" IS NULL OR ").append(fullFieldName); } sqlOperator = " NOT IN "; @@ -206,7 +201,7 @@ public static String parseNonCompositeFilter( value = null; // For fields inside jsonb StringBuilder notExists = prepareFieldAccessorExpr(fieldName, columnName); - if (notExists != null) { + if (notExists != null && !OUTER_COLUMNS.contains(fieldName)) { filterString = notExists; } break; @@ -215,7 +210,7 @@ public static String parseNonCompositeFilter( value = null; // For fields inside jsonb StringBuilder exists = prepareFieldAccessorExpr(fieldName, columnName); - if (exists != null) { + if (exists != null && !OUTER_COLUMNS.contains(fieldName)) { filterString = exists; } break; @@ -230,7 +225,7 @@ public static String parseNonCompositeFilter( // "document->key IS NULL OR document->key->> != value" StringBuilder notEquals = prepareFieldAccessorExpr(fieldName, columnName); // For fields inside jsonb - if (notEquals != null) { + if (notEquals != null && !OUTER_COLUMNS.contains(fieldName)) { filterString = notEquals.append(" IS NULL OR ").append(fullFieldName); } break; diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/PostgresQueryParserTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/PostgresQueryParserTest.java index a83773f3..d2490fc3 100644 --- a/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/PostgresQueryParserTest.java +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/PostgresQueryParserTest.java @@ -10,6 +10,7 @@ import org.hypertrace.core.documentstore.Filter; import org.hypertrace.core.documentstore.Filter.Op; import org.hypertrace.core.documentstore.OrderBy; +import org.hypertrace.core.documentstore.postgres.utils.PostgresUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -19,49 +20,97 @@ class PostgresQueryParserTest { void testParseNonCompositeFilter() { { Filter filter = new Filter(Filter.Op.EQ, ID, "val1"); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = + PostgresUtils.parseNonCompositeFilter( + filter.getFieldName(), + PostgresUtils.DOCUMENT_COLUMN, + filter.getOp().toString(), + filter.getValue(), + initParams()); Assertions.assertEquals(ID + " = ?", query); } { Filter filter = new Filter(Filter.Op.NEQ, ID, "val1"); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = + PostgresUtils.parseNonCompositeFilter( + filter.getFieldName(), + PostgresUtils.DOCUMENT_COLUMN, + filter.getOp().toString(), + filter.getValue(), + initParams()); Assertions.assertEquals(ID + " != ?", query); } { Filter filter = new Filter(Filter.Op.GT, ID, 5); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = + PostgresUtils.parseNonCompositeFilter( + filter.getFieldName(), + PostgresUtils.DOCUMENT_COLUMN, + filter.getOp().toString(), + filter.getValue(), + initParams()); Assertions.assertEquals("CAST (" + ID + " AS NUMERIC) > ?", query); } { Filter filter = new Filter(Filter.Op.GTE, ID, 5); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = + PostgresUtils.parseNonCompositeFilter( + filter.getFieldName(), + PostgresUtils.DOCUMENT_COLUMN, + filter.getOp().toString(), + filter.getValue(), + initParams()); Assertions.assertEquals("CAST (" + ID + " AS NUMERIC) >= ?", query); } { Filter filter = new Filter(Filter.Op.LT, ID, 5); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = + PostgresUtils.parseNonCompositeFilter( + filter.getFieldName(), + PostgresUtils.DOCUMENT_COLUMN, + filter.getOp().toString(), + filter.getValue(), + initParams()); Assertions.assertEquals("CAST (" + ID + " AS NUMERIC) < ?", query); } { Filter filter = new Filter(Filter.Op.LTE, ID, 5); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = + PostgresUtils.parseNonCompositeFilter( + filter.getFieldName(), + PostgresUtils.DOCUMENT_COLUMN, + filter.getOp().toString(), + filter.getValue(), + initParams()); Assertions.assertEquals("CAST (" + ID + " AS NUMERIC) <= ?", query); } { Filter filter = new Filter(Filter.Op.LIKE, ID, "abc"); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = + PostgresUtils.parseNonCompositeFilter( + filter.getFieldName(), + PostgresUtils.DOCUMENT_COLUMN, + filter.getOp().toString(), + filter.getValue(), + initParams()); Assertions.assertEquals(ID + " ILIKE ?", query); } { Filter filter = new Filter(Filter.Op.IN, ID, List.of("abc", "xyz")); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = + PostgresUtils.parseNonCompositeFilter( + filter.getFieldName(), + PostgresUtils.DOCUMENT_COLUMN, + filter.getOp().toString(), + filter.getValue(), + initParams()); Assertions.assertEquals(ID + " IN (?, ?)", query); } } @@ -70,74 +119,74 @@ void testParseNonCompositeFilter() { void testParseNonCompositeFilterForJsonField() { { Filter filter = new Filter(Filter.Op.EQ, "key1", "val1"); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("document->>'key1' = ?", query); } { Filter filter = new Filter(Filter.Op.NEQ, "key1", "val1"); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("document->'key1' IS NULL OR document->>'key1' != ?", query); } { Filter filter = new Filter(Filter.Op.GT, "key1", 5); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("CAST (document->>'key1' AS NUMERIC) > ?", query); } { Filter filter = new Filter(Filter.Op.GTE, "key1", 5); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("CAST (document->>'key1' AS NUMERIC) >= ?", query); } { Filter filter = new Filter(Filter.Op.LT, "key1", 5); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("CAST (document->>'key1' AS NUMERIC) < ?", query); } { Filter filter = new Filter(Filter.Op.LTE, "key1", 5); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("CAST (document->>'key1' AS NUMERIC) <= ?", query); } { Filter filter = new Filter(Filter.Op.LIKE, "key1", "abc"); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("document->>'key1' ILIKE ?", query); } { Filter filter = new Filter(Filter.Op.IN, "key1", List.of("abc", "xyz")); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("document->>'key1' IN (?, ?)", query); } { Filter filter = new Filter(Op.NOT_IN, "key1", List.of("abc", "xyz")); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("document->'key1' IS NULL OR document->>'key1' NOT IN (?, ?)", query); } { Filter filter = new Filter(Filter.Op.EQ, DOCUMENT_ID, "k1:k2"); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("document->>'_id' = ?", query); } { Filter filter = new Filter(Filter.Op.EXISTS, "key1.key2", null); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); System.err.println(query); Assertions.assertEquals("document->'key1'->'key2' IS NOT NULL ", query); } { Filter filter = new Filter(Filter.Op.NOT_EXISTS, "key1", null); - String query = PostgresQueryParser.parseNonCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("document->'key1' IS NULL ", query); } } @@ -151,7 +200,7 @@ void testNonCompositeFilterUnsupportedException() { Exception exception = assertThrows( UnsupportedOperationException.class, - () -> PostgresQueryParser.parseNonCompositeFilter(filter, initParams())); + () -> PostgresQueryParser.parseFilter(filter, initParams())); String actualMessage = exception.getMessage(); Assertions.assertTrue(actualMessage.contains(expected)); } @@ -174,7 +223,7 @@ void testParseQueryForCompositeFilter() { { Filter filter = new Filter(Filter.Op.EQ, ID, "val1").and(new Filter(Filter.Op.EQ, CREATED_AT, "val2")); - String query = PostgresQueryParser.parseCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals(String.format("(%s = ?) AND (%s = ?)", ID, CREATED_AT), query); } @@ -182,7 +231,7 @@ void testParseQueryForCompositeFilter() { Filter filter = new Filter(Filter.Op.EQ, ID, "val1").or(new Filter(Filter.Op.EQ, CREATED_AT, "val2")); - String query = PostgresQueryParser.parseCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals(String.format("(%s = ?) OR (%s = ?)", ID, CREATED_AT), query); } } @@ -192,7 +241,7 @@ void testParseQueryForCompositeFilterForJsonField() { { Filter filter = new Filter(Filter.Op.EQ, "key1", "val1").and(new Filter(Filter.Op.EQ, "key2", "val2")); - String query = PostgresQueryParser.parseCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("(document->>'key1' = ?) AND (document->>'key2' = ?)", query); } @@ -200,7 +249,7 @@ void testParseQueryForCompositeFilterForJsonField() { Filter filter = new Filter(Filter.Op.EQ, "key1", "val1").or(new Filter(Filter.Op.EQ, "key2", "val2")); - String query = PostgresQueryParser.parseCompositeFilter(filter, initParams()); + String query = PostgresQueryParser.parseFilter(filter, initParams()); Assertions.assertEquals("(document->>'key1' = ?) OR (document->>'key2' = ?)", query); } } @@ -247,6 +296,15 @@ void testParseOrderBys() { PostgresQueryParser.parseOrderBys(orderBys)); } + @Test + void testSelectionClause() { + List selections = + List.of("id", "identifyingAttributes", "tenantId", "attributes", "type"); + Assertions.assertEquals( + "id AS \"id\",document->'identifyingAttributes' AS \"identifyingAttributes\",document->'tenantId' AS \"tenantId\",document->'attributes' AS \"attributes\",document->'type' AS \"type\"", + PostgresQueryParser.parseSelections(selections)); + } + private Params.Builder initParams() { return Params.newBuilder(); }