Skip to content

Commit a4f2355

Browse files
committed
WIP
1 parent 1bc31fb commit a4f2355

2 files changed

Lines changed: 490 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package org.hypertrace.core.documentstore.postgres.query.v1.transformer;
2+
3+
import java.util.Optional;
4+
import org.hypertrace.core.documentstore.OrderBy;
5+
import org.hypertrace.core.documentstore.commons.ColumnMetadata;
6+
import org.hypertrace.core.documentstore.commons.SchemaRegistry;
7+
import org.hypertrace.core.documentstore.expression.impl.DataType;
8+
import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression;
9+
import org.hypertrace.core.documentstore.expression.impl.JsonFieldType;
10+
import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression;
11+
import org.hypertrace.core.documentstore.expression.operators.SortOrder;
12+
import org.hypertrace.core.documentstore.query.Filter;
13+
import org.hypertrace.core.documentstore.query.Pagination;
14+
import org.hypertrace.core.documentstore.query.Query;
15+
16+
/**
17+
* Transforms the legacy {@link org.hypertrace.core.documentstore.Query} to the newer {@link Query}
18+
* format. Since the legacy query does not carry any type information, this class interfaces with
19+
* {@link SchemaRegistry} to find the type info.
20+
*
21+
* <p>This transformer handles:
22+
*
23+
* <ul>
24+
* <li>Filter transformation (delegated to {@link LegacyFilterToQueryFilterTransformer})
25+
* <li>Selection transformation (field names to appropriate identifier expressions)
26+
* <li>OrderBy transformation (field names to sort expressions)
27+
* <li>Pagination (limit/offset)
28+
* </ul>
29+
*/
30+
public class LegacyQueryToV2QueryTransformer {
31+
32+
private final SchemaRegistry<? extends ColumnMetadata> schemaRegistry;
33+
private final String tableName;
34+
private final LegacyFilterToQueryFilterTransformer filterTransformer;
35+
36+
public LegacyQueryToV2QueryTransformer(
37+
SchemaRegistry<? extends ColumnMetadata> schemaRegistry, String tableName) {
38+
this.schemaRegistry = schemaRegistry;
39+
this.tableName = tableName;
40+
this.filterTransformer = new LegacyFilterToQueryFilterTransformer(schemaRegistry, tableName);
41+
}
42+
43+
/**
44+
* Transforms a legacy Query to the new v2 Query.
45+
*
46+
* @param legacyQuery the legacy query to transform
47+
* @return the transformed v2 Query
48+
*/
49+
public Query transform(org.hypertrace.core.documentstore.Query legacyQuery) {
50+
if (legacyQuery == null) {
51+
return Query.builder().build();
52+
}
53+
54+
Query.QueryBuilder builder = Query.builder();
55+
56+
// Transform filter
57+
if (legacyQuery.getFilter() != null) {
58+
Filter v2Filter = filterTransformer.transform(legacyQuery.getFilter());
59+
if (v2Filter != null && v2Filter.getExpression() != null) {
60+
builder.setFilter(v2Filter.getExpression());
61+
}
62+
}
63+
64+
// Transform selections
65+
if (legacyQuery.getSelections() != null && !legacyQuery.getSelections().isEmpty()) {
66+
for (String selection : legacyQuery.getSelections()) {
67+
builder.addSelection(createIdentifierExpression(selection));
68+
}
69+
}
70+
71+
// Transform orderBy
72+
if (legacyQuery.getOrderBys() != null && !legacyQuery.getOrderBys().isEmpty()) {
73+
for (OrderBy orderBy : legacyQuery.getOrderBys()) {
74+
SortOrder sortOrder = orderBy.isAsc() ? SortOrder.ASC : SortOrder.DESC;
75+
builder.addSort(createIdentifierExpression(orderBy.getField()), sortOrder);
76+
}
77+
}
78+
79+
// Set pagination
80+
Integer limit = legacyQuery.getLimit();
81+
Integer offset = legacyQuery.getOffset();
82+
if (limit != null && limit >= 0) {
83+
builder.setPagination(
84+
Pagination.builder()
85+
.offset(offset != null && offset >= 0 ? offset : 0)
86+
.limit(limit)
87+
.build());
88+
}
89+
90+
return builder.build();
91+
}
92+
93+
/**
94+
* Creates the appropriate identifier expression based on the field name and schema.
95+
*
96+
* <p>Uses the schema registry to determine if a field is:
97+
*
98+
* <ul>
99+
* <li>A direct column → IdentifierExpression
100+
* <li>A JSONB nested path → JsonIdentifierExpression with STRING type (default for
101+
* selections/orderBy since we don't have a value to infer type from)
102+
* </ul>
103+
*
104+
* <p>Returns IdentifierExpression (or subclass) which implements both SelectTypeExpression and
105+
* SortTypeExpression, allowing use in both selections and orderBy clauses.
106+
*/
107+
private IdentifierExpression createIdentifierExpression(String fieldName) {
108+
if (fieldName == null || fieldName.isEmpty()) {
109+
throw new IllegalArgumentException("Field name cannot be null or empty");
110+
}
111+
112+
// Check if the full path is a direct column
113+
if (schemaRegistry.getColumnOrRefresh(tableName, fieldName).isPresent()) {
114+
return IdentifierExpression.of(fieldName);
115+
}
116+
117+
// Try to find a JSONB column prefix
118+
Optional<String> jsonbColumn = findJsonbColumnPrefix(fieldName);
119+
if (jsonbColumn.isPresent()) {
120+
String columnName = jsonbColumn.get();
121+
String[] jsonPath = getNestedPath(fieldName, columnName);
122+
// Default to STRING for selections/orderBy since we don't have a value to infer from
123+
return JsonIdentifierExpression.of(columnName, JsonFieldType.STRING, jsonPath);
124+
}
125+
126+
// Fallback: treat as direct column (will fail at query time if column doesn't exist)
127+
return IdentifierExpression.of(fieldName);
128+
}
129+
130+
/**
131+
* Finds the JSONB column prefix for a given path by progressively checking prefixes.
132+
*
133+
* <p>For example, given path "props.inheritedAttributes.color":
134+
*
135+
* <ul>
136+
* <li>If "props" is a JSONB column → returns "props"
137+
* <li>If "props.inheritedAttributes" is a JSONB column → returns "props.inheritedAttributes"
138+
* <li>If neither is JSONB → returns empty
139+
* </ul>
140+
*/
141+
private Optional<String> findJsonbColumnPrefix(String path) {
142+
if (!path.contains(".")) {
143+
return Optional.empty();
144+
}
145+
146+
String[] parts = path.split("\\.");
147+
StringBuilder columnBuilder = new StringBuilder(parts[0]);
148+
149+
for (int i = 0; i < parts.length - 1; i++) {
150+
if (i > 0) {
151+
columnBuilder.append(".").append(parts[i]);
152+
}
153+
String candidateColumn = columnBuilder.toString();
154+
Optional<? extends ColumnMetadata> colMeta =
155+
schemaRegistry.getColumnOrRefresh(tableName, candidateColumn);
156+
157+
if (colMeta.isPresent() && colMeta.get().getCanonicalType() == DataType.JSON) {
158+
return Optional.of(candidateColumn);
159+
}
160+
}
161+
162+
return Optional.empty();
163+
}
164+
165+
/**
166+
* Extracts the JSONB path portion after removing the column name prefix.
167+
*
168+
* <p>For example, if the path is "props.inheritedAttributes.color" and the column name is
169+
* "props", then the returned path is ["inheritedAttributes", "color"].
170+
*/
171+
private String[] getNestedPath(String fullPath, String jsonbColName) {
172+
if (fullPath.equals(jsonbColName)) {
173+
return new String[0];
174+
}
175+
String nested = fullPath.substring(jsonbColName.length() + 1);
176+
return nested.split("\\.");
177+
}
178+
}

0 commit comments

Comments
 (0)