Skip to content

Commit cc58468

Browse files
authored
Refactor ARRAY_CONTAINS query syntax (#592)
1 parent 1e6b595 commit cc58468

File tree

3 files changed

+62
-4
lines changed

3 files changed

+62
-4
lines changed

dao-impl/ebean-dao/src/main/java/com/linkedin/metadata/dao/utils/SQLIndexFilterUtils.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.linkedin.metadata.dao.utils;
22

3+
import com.google.common.annotations.VisibleForTesting;
34
import com.linkedin.data.template.GetMode;
45
import com.linkedin.data.template.RecordTemplate;
56
import com.linkedin.data.template.StringArray;
@@ -162,6 +163,28 @@ public static String parseIndexFilter(@Nonnull String entityType, @Nullable Inde
162163

163164
}
164165

166+
/**
167+
* Strip CAST() statement from the input string. The intention for this is to be used for array-based functional
168+
* indexes in which the CAST() statement must be removed before consumption from a JSON_CONTAINS() on the query side.
169+
* Note that the CAST() statement is expected to be the **outermost** statement in the input string.
170+
* Specifics:
171+
* - leading '(' is optional
172+
* - trailing ')' is optional
173+
* - whitespace is lenient -- before and after CAST, AS, between closing parentheses
174+
* - case-insensitive
175+
* - has to account for closing parens from the "AS" statement -- ie. as char(128)
176+
* - has to account for array casting: ... as char(128) array
177+
* @param inputString input string
178+
* ex. (CAST(JSON_EXTRACT(a_asset_labels, '$.aspect.derived_labels') AS CHAR(128) ARRAY))
179+
* @return stripped string, enclosed in parentheses
180+
* ex. (JSON_EXTRACT(a_asset_labels, '$.aspect.derived_labels'))
181+
*/
182+
@VisibleForTesting
183+
@Nonnull
184+
protected static String stripCastStatement(@Nonnull String inputString) {
185+
return inputString.replaceFirst("^\\(?\\s*(?i)CAST\\s*\\(", "(").replaceFirst("\\s+(?i)AS\\s+[^)]*(?:\\([^)]*\\))?[^)]*\\s*\\){1,2}$", ")");
186+
}
187+
165188
/**
166189
* Parse condition expression.
167190
* @param index the name of the virtual generated column OR the actual expression of a functional index
@@ -174,7 +197,7 @@ private static String parseSqlFilter(String index, Condition condition, IndexVal
174197
switch (condition) {
175198
// TODO: add validation to check that the index column value is an array type
176199
case ARRAY_CONTAINS:
177-
return String.format("'%s' MEMBER OF(%s)", parseIndexValue(indexValue), index); // JSON Array
200+
return String.format("JSON_CONTAINS(%s, '%s')", stripCastStatement(index), parseIndexValue(indexValue)); // JSON Array
178201
case CONTAIN:
179202
return String.format("JSON_SEARCH(%s, 'one', '%s') IS NOT NULL", index, parseIndexValue(indexValue)); // JSON String, Array, Struct
180203
case IN:

dao-impl/ebean-dao/src/test/java/com/linkedin/metadata/dao/utils/SQLIndexFilterUtilsTest.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public void testParseIndexFilterWithFunctionalIndex() {
123123
SQLIndexFilterUtils.createIndexCriterion(AspectBar.class, "value_array", Condition.ARRAY_CONTAINS,
124124
IndexValue.create(12L)));
125125
final String expectedSql1 =
126-
"WHERE a_aspectbar IS NOT NULL\nAND JSON_EXTRACT(a_aspectbar, '$.gma_deleted') IS NULL\nAND '12' MEMBER OF((cast(json_extract(`a_aspectbar`, '$.aspect.value_array') as char(128) array)))\nAND deleted_ts IS NULL";
126+
"WHERE a_aspectbar IS NOT NULL\nAND JSON_EXTRACT(a_aspectbar, '$.gma_deleted') IS NULL\nAND JSON_CONTAINS((json_extract(`a_aspectbar`, '$.aspect.value_array')), '12')\nAND deleted_ts IS NULL";
127127
assertValidSql(expectedSql1); // assert that the expected SQL is valid to begin with
128128
assertEquals(SQLIndexFilterUtils.parseIndexFilter(FooUrn.ENTITY_TYPE, indexFilter, false, mockValidator),
129129
expectedSql1);
@@ -240,4 +240,39 @@ public void testGetIndexedExpressionOrColumn() {
240240
false, mockValidator),
241241
"(cast(json_extract(`a_aspectbar`, '$.aspect.value') as char(1024)))");
242242
}
243+
244+
@Test
245+
public void testStripCastStatement() {
246+
// lowercase
247+
assertEquals(SQLIndexFilterUtils.stripCastStatement("(cast(json_extract(`a_aspectbar`, '$.aspect.value') as char(1024)))"),
248+
"(json_extract(`a_aspectbar`, '$.aspect.value'))");
249+
250+
// all caps
251+
assertEquals(SQLIndexFilterUtils.stripCastStatement("(CAST(json_extract(`a_aspectbar`, '$.aspect.value') AS char(1024)))"),
252+
"(json_extract(`a_aspectbar`, '$.aspect.value'))");
253+
254+
// mixed case
255+
assertEquals(SQLIndexFilterUtils.stripCastStatement("(CaSt(json_extract(`a_aspectbar`, '$.aspect.value') As char(1024)))"),
256+
"(json_extract(`a_aspectbar`, '$.aspect.value'))");
257+
258+
// extra spaces (before 'cast', before 'as')
259+
assertEquals(SQLIndexFilterUtils.stripCastStatement("( cast(json_extract(`a_aspectbar`, '$.aspect.value') as char(1024)))"),
260+
"(json_extract(`a_aspectbar`, '$.aspect.value'))");
261+
262+
// extra spaces (after 'cast', after 'as')
263+
assertEquals(SQLIndexFilterUtils.stripCastStatement("(cast (json_extract(`a_aspectbar`, '$.aspect.value') as char(1024)))"),
264+
"(json_extract(`a_aspectbar`, '$.aspect.value'))");
265+
266+
// casting as an array
267+
assertEquals(SQLIndexFilterUtils.stripCastStatement("(cast(json_extract(`a_aspectbar`, '$.aspect.value') as char(1024) array))"),
268+
"(json_extract(`a_aspectbar`, '$.aspect.value'))");
269+
270+
// NOT the outermost statement
271+
assertEquals(SQLIndexFilterUtils.stripCastStatement("(foo(cast(json_extract(`a_aspectbar`, '$.aspect.value') as char(1024))))"),
272+
"(foo(cast(json_extract(`a_aspectbar`, '$.aspect.value') as char(1024))))");
273+
274+
// no enclosing parents in original statement
275+
assertEquals(SQLIndexFilterUtils.stripCastStatement("cast(json_extract(`a_aspectbar`, '$.aspect.value') as char(1024))"),
276+
"(json_extract(`a_aspectbar`, '$.aspect.value'))");
277+
}
243278
}

dao-impl/ebean-dao/src/test/java/com/linkedin/metadata/dao/utils/SQLStatementUtilsTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,10 @@ public void testCreateFilterSqlWithArrayContainsCondition() {
197197

198198
String sql1 = SQLStatementUtils.createFilterSql("foo", indexFilter, true, false, mockValidator);
199199
String expectedSql1 = "SELECT urn, (SELECT COUNT(urn) FROM metadata_entity_foo WHERE a_aspectfoobar IS NOT NULL\n"
200-
+ "AND JSON_EXTRACT(a_aspectfoobar, '$.gma_deleted') IS NULL\n" + "AND 'bar1' MEMBER OF(i_aspectfoobar$bars)\n"
200+
+ "AND JSON_EXTRACT(a_aspectfoobar, '$.gma_deleted') IS NULL\n" + "AND JSON_CONTAINS(i_aspectfoobar$bars, 'bar1')\n"
201201
+ "AND deleted_ts IS NULL)" + " as _total_count FROM metadata_entity_foo\n"
202202
+ "WHERE a_aspectfoobar IS NOT NULL\n" + "AND JSON_EXTRACT(a_aspectfoobar, '$.gma_deleted') IS NULL\n"
203-
+ "AND 'bar1' MEMBER OF(i_aspectfoobar$bars)\n" + "AND deleted_ts IS NULL";
203+
+ "AND JSON_CONTAINS(i_aspectfoobar$bars, 'bar1')\n" + "AND deleted_ts IS NULL";
204204

205205
assertEquals(sql1, expectedSql1);
206206
}

0 commit comments

Comments
 (0)