diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java index e62c34a426ef..fb1bd3ec6612 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java @@ -153,6 +153,7 @@ import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.FullyQualifiedName; import org.openmetadata.service.util.JsonUtils; +import org.openmetadata.service.util.jdbi.BindConcat; import org.openmetadata.service.util.jdbi.BindFQN; import org.openmetadata.service.util.jdbi.BindListFQN; import org.openmetadata.service.util.jdbi.BindUUID; @@ -747,20 +748,27 @@ void insertMany( @SqlQuery( "SELECT id, extension, json " + "FROM entity_extension " - + "WHERE id IN () AND extension LIKE CONCAT(:extensionPrefix, '.%') " + + "WHERE id IN () AND extension LIKE :extension " + "ORDER BY id, extension") @RegisterRowMapper(ExtensionRecordWithIdMapper.class) List getExtensionsBatch( - @BindList("ids") List ids, @Bind("extensionPrefix") String extensionPrefix); + @BindList("ids") List ids, + @BindConcat( + value = "extension", + parts = {":extensionPrefix", ".%"}) + String extensionPrefix); @SqlQuery( "SELECT id, extension, json, jsonschema " + "FROM entity_extension " - + "WHERE extension LIKE CONCAT(:extensionPrefix, '%') " + + "WHERE extension LIKE :extension " + "ORDER BY id, extension") @RegisterRowMapper(ExtensionWithIdAndSchemaRowMapper.class) List getExtensionsByPrefixBatch( - @Bind("extensionPrefix") String extensionPrefix); + @BindConcat( + value = "extension", + parts = {":extensionPrefix", "%"}) + String extensionPrefix); @Transaction @ConnectionAwareSqlBatch( @@ -1492,16 +1500,25 @@ default List listThreadsByEntityLink( @SqlQuery( "SELECT json FROM thread_entity " + "AND hash_id in (SELECT fromFQNHash FROM field_relationship WHERE " - + "(:fqnPrefixHash IS NULL OR toFQNHash LIKE CONCAT(:fqnPrefixHash, '.%') OR toFQNHash=:fqnPrefixHash) AND fromType='THREAD' AND " - + "(:toType IS NULL OR toType LIKE CONCAT(:toType, '.%') OR toType=:toType) AND relation= :relation) " + + "(:fqnPrefixHash IS NULL OR toFQNHash LIKE :concatFqnPrefixHash OR toFQNHash=:fqnPrefixHash) AND fromType='THREAD' AND " + + "(:toType IS NULL OR toType LIKE :concatToType OR toType=:toType) AND relation= :relation) " + "AND (:userName IS NULL OR MD5(id) in (SELECT toFQNHash FROM field_relationship WHERE " + " ((fromType='user' AND fromFQNHash= :userName) OR" + " (fromType='team' AND fromFQNHash IN ())) AND toType='THREAD' AND relation= :filterRelation) )" + " " + "LIMIT :limit") List listThreadsByEntityLink( - @BindFQN("fqnPrefixHash") String fqnPrefixHash, - @Bind("toType") String toType, + @BindConcat( + value = "concatFqnPrefixHash", + original = "fqnPrefixHash", + parts = {":fqnPrefixHash", ".%"}, + hash = true) + String fqnPrefixHash, + @BindConcat( + value = "concatToType", + original = "toType", + parts = {":toType", ".%"}) + String toType, @Bind("limit") int limit, @Bind("relation") int relation, @BindFQN("userName") String userName, @@ -1533,14 +1550,23 @@ default int listCountThreadsByEntityLink( @SqlQuery( "SELECT count(id) FROM thread_entity " + "AND hash_id in (SELECT fromFQNHash FROM field_relationship WHERE " - + "(:fqnPrefixHash IS NULL OR toFQNHash LIKE CONCAT(:fqnPrefixHash, '.%') OR toFQNHash=:fqnPrefixHash) AND fromType='THREAD' AND " - + "(:toType IS NULL OR toType LIKE CONCAT(:toType, '.%') OR toType=:toType) AND relation= :relation) " + + "(:fqnPrefixHash IS NULL OR toFQNHash LIKE :concatFqnPrefixHash OR toFQNHash=:fqnPrefixHash) AND fromType='THREAD' AND " + + "(:toType IS NULL OR toType LIKE :concatToType OR toType=:toType) AND relation= :relation) " + "AND (:userName IS NULL OR id in (SELECT toFQNHash FROM field_relationship WHERE " + " ((fromType='user' AND fromFQNHash= :userName) OR" + " (fromType='team' AND fromFQNHash IN ())) AND toType='THREAD' AND relation= :filterRelation) )") int listCountThreadsByEntityLink( - @BindFQN("fqnPrefixHash") String fqnPrefixHash, - @Bind("toType") String toType, + @BindConcat( + value = "concatFqnPrefixHash", + original = "fqnPrefixHash", + parts = {":fqnPrefixHash", ".%"}, + hash = true) + String fqnPrefixHash, + @BindConcat( + value = "concatToType", + original = "toType", + parts = {":toType", ".%"}) + String toType, @Bind("relation") int relation, @Bind("userName") String userName, @BindList("teamNames") List teamNames, @@ -1562,9 +1588,9 @@ int listCountThreadsByEntityLink( + " WHERE hash_id IN ( " + " SELECT fromFQNHash FROM field_relationship " + " WHERE " - + " (:fqnPrefixHash IS NULL OR toFQNHash LIKE CONCAT(:fqnPrefixHash, '.%') OR toFQNHash = :fqnPrefixHash) " + + " (:fqnPrefixHash IS NULL OR toFQNHash LIKE :concatFqnPrefixHash OR toFQNHash = :fqnPrefixHash) " + " AND fromType = 'THREAD' " - + " AND (:toType IS NULL OR toType LIKE CONCAT(:toType, '.%') OR toType = :toType) " + + " AND (:toType IS NULL OR toType LIKE :concatToType OR toType = :toType) " + " AND relation = 3 " + " ) " + " UNION " @@ -1576,8 +1602,17 @@ int listCountThreadsByEntityLink( @RegisterRowMapper(ThreadCountFieldMapper.class) List> listCountByEntityLink( @BindUUID("entityId") UUID entityId, - @BindFQN("fqnPrefixHash") String fqnPrefixHash, - @Bind("toType") String toType); + @BindConcat( + value = "concatFqnPrefixHash", + original = "fqnPrefixHash", + parts = {":fqnPrefixHash", ".%"}, + hash = true) + String fqnPrefixHash, + @BindConcat( + value = "concatToType", + original = "toType", + parts = {":toType", ".%"}) + String toType); @ConnectionAwareSqlQuery( value = @@ -1790,18 +1825,31 @@ int listCountThreadsByMentions( @SqlQuery( "SELECT json FROM thread_entity " + "AND MD5(id) in (SELECT fromFQNHash FROM field_relationship WHERE " - + "(:fqnPrefixHash IS NULL OR toFQNHash LIKE CONCAT(:fqnPrefixHash, '.%') OR toFQNHash=:fqnPrefixHash) AND fromType='THREAD' AND " - + "((:toType1 IS NULL OR toType LIKE CONCAT(:toType1, '.%') OR toType=:toType1) OR " - + "(:toType2 IS NULL OR toType LIKE CONCAT(:toType2, '.%') OR toType=:toType2)) AND relation= :relation)" + + "(:fqnPrefixHash IS NULL OR toFQNHash LIKE :concatFqnPrefixHash OR toFQNHash=:fqnPrefixHash) AND fromType='THREAD' AND " + + "((:toType1 IS NULL OR toType LIKE :concatToType1 OR toType=:toType1) OR " + + "(:toType2 IS NULL OR toType LIKE :concatToType2 OR toType=:toType2)) AND relation= :relation)" + "AND (:userName IS NULL OR MD5(id) in (SELECT toFQNHash FROM field_relationship WHERE " + " ((fromType='user' AND fromFQNHash= :userName) OR" + " (fromType='team' AND fromFQNHash IN ())) AND toType='THREAD' AND relation= :filterRelation) )" + " " + "LIMIT :limit") List listThreadsByGlossaryAndTerms( - @BindFQN("fqnPrefixHash") String fqnPrefixHash, - @Bind("toType1") String toType1, - @Bind("toType2") String toType2, + @BindConcat( + value = "concatFqnPrefixHash", + original = "fqnPrefixHash", + parts = {":fqnPrefixHash", ".%"}, + hash = true) + String fqnPrefixHash, + @BindConcat( + value = "concatToType1", + original = "toType1", + parts = {":toType1", ".%"}) + String toType1, + @BindConcat( + value = "concatToType2", + original = "toType2", + parts = {":toType2", ".%"}) + String toType2, @Bind("limit") int limit, @Bind("relation") int relation, @BindFQN("userName") String userName, @@ -1833,9 +1881,9 @@ default List> listCountThreadsByGlossaryAndTerms( + " WHERE te.hash_id IN ( " + " SELECT fr.fromFQNHash " + " FROM field_relationship fr " - + " WHERE (:fqnPrefixHash IS NULL OR fr.toFQNHash LIKE CONCAT(:fqnPrefixHash, '.%') OR fr.toFQNHash = :fqnPrefixHash) " + + " WHERE (:fqnPrefixHash IS NULL OR fr.toFQNHash LIKE :concatFqnPrefixHash OR fr.toFQNHash = :fqnPrefixHash) " + " AND fr.fromType = 'THREAD' " - + " AND (:toType1 IS NULL OR fr.toType LIKE CONCAT(:toType1, '.%') OR fr.toType = :toType1) " + + " AND (:toType1 IS NULL OR fr.toType LIKE :concatToType1 OR fr.toType = :toType1) " + " AND fr.relation = 3 " + " ) " + " UNION " @@ -1846,9 +1894,9 @@ default List> listCountThreadsByGlossaryAndTerms( + " SELECT fr.fromFQNHash " + " FROM field_relationship fr " + " JOIN thread_entity te2 ON te2.hash_id = fr.fromFQNHash WHERE fr.fromFQNHash = te.hash_id AND te2.type = 'Task' " - + " AND (:fqnPrefixHash IS NULL OR fr.toFQNHash LIKE CONCAT(:fqnPrefixHash, '.%') OR fr.toFQNHash = :fqnPrefixHash) " + + " AND (:fqnPrefixHash IS NULL OR fr.toFQNHash LIKE :concatFqnPrefixHash OR fr.toFQNHash = :fqnPrefixHash) " + " AND fr.fromType = 'THREAD' " - + " AND (:toType2 IS NULL OR fr.toType LIKE CONCAT(:toType2, '.%') OR fr.toType = :toType2) " + + " AND (:toType2 IS NULL OR fr.toType LIKE :concatToType2 OR fr.toType = :toType2) " + " AND fr.relation = 3 " + " ) " + ") AS combined_results WHERE combined_results.type is not NULL " @@ -1856,9 +1904,22 @@ default List> listCountThreadsByGlossaryAndTerms( @RegisterRowMapper(ThreadCountFieldMapper.class) List> listCountThreadsByGlossaryAndTerms( @BindUUID("entityId") UUID entityId, - @BindFQN("fqnPrefixHash") String fqnPrefixHash, - @Bind("toType1") String toType1, - @Bind("toType2") String toType2); + @BindConcat( + value = "concatFqnPrefixHash", + original = "fqnPrefixHash", + parts = {":fqnPrefixHash", ".%"}, + hash = true) + String fqnPrefixHash, + @BindConcat( + value = "concatToType1", + original = "toType1", + parts = {":toType1", ".%"}) + String toType1, + @BindConcat( + value = "concatToType2", + original = "toType2", + parts = {":toType2", ".%"}) + String toType2); @SqlQuery("select id from thread_entity where entityId = :entityId") List findByEntityId(@Bind("entityId") String entityId); @@ -1963,11 +2024,15 @@ List> findFrom( @SqlQuery( "SELECT fromFQN, toFQN, json FROM field_relationship WHERE " - + "fromFQNHash LIKE CONCAT(:fqnPrefixHash, '%') AND fromType = :fromType AND toType = :toType " + + "fromFQNHash LIKE :concatFqnPrefixHash AND fromType = :fromType AND toType = :toType " + "AND relation = :relation") @RegisterRowMapper(ToFieldMapper.class) List> listToByPrefix( - @BindFQN("fqnPrefixHash") String fqnPrefixHash, + @BindConcat( + value = "concatFqnPrefixHash", + parts = {":fqnPrefixHash", "%"}, + hash = true) + String fqnPrefixHash, @Bind("fromType") String fromType, @Bind("toType") String toType, @Bind("relation") int relation); @@ -1993,13 +2058,17 @@ List> listBidirectional( @SqlQuery( "SELECT fromFQN, toFQN, json FROM field_relationship WHERE " - + "fromFQNHash LIKE CONCAT(:fqnPrefixHash, '%') AND fromType = :type AND toType = :otherType AND relation = :relation " + + "fromFQNHash LIKE :concatFqnPrefixHash AND fromType = :type AND toType = :otherType AND relation = :relation " + "UNION " + "SELECT toFQN, fromFQN, json FROM field_relationship WHERE " - + "toFQNHash LIKE CONCAT(:fqnPrefixHash, '%') AND toType = :type AND fromType = :otherType AND relation = :relation") + + "toFQNHash LIKE :concatFqnPrefixHash AND toType = :type AND fromType = :otherType AND relation = :relation") @RegisterRowMapper(ToFieldMapper.class) List> listBidirectionalByPrefix( - @BindFQN("fqnPrefixHash") String fqnPrefixHash, + @BindConcat( + value = "concatFqnPrefixHash", + parts = {":fqnPrefixHash", "%"}, + hash = true) + String fqnPrefixHash, @Bind("type") String type, @Bind("otherType") String otherType, @Bind("relation") int relation); @@ -2491,12 +2560,12 @@ default int listCount(ListFilter filter) { String directChildrenOf = filter.getQueryParam("directChildrenOf"); if (!nullOrEmpty(directChildrenOf)) { - filter.queryParams.put( - "directChildrenOfHash", FullyQualifiedName.buildHash(directChildrenOf)); - condition = - String.format( - " %s AND fqnHash = CONCAT(:directChildrenOfHash, '.', MD5(CASE WHEN name LIKE '%%.%%' THEN CONCAT('\"', name, '\"') ELSE name END)) ", - condition); + String parentFqnHash = FullyQualifiedName.buildHash(directChildrenOf); + filter.queryParams.put("fqnHashSingleLevel", parentFqnHash + ".%"); + filter.queryParams.put("fqnHashNestedLevel", parentFqnHash + ".%.%"); + + condition += + " AND fqnHash LIKE :fqnHashSingleLevel AND fqnHash NOT LIKE :fqnHashNestedLevel"; } return listCount(getTableName(), getNameHashColumn(), filter.getQueryParams(), condition); @@ -2509,12 +2578,12 @@ default List listBefore( String directChildrenOf = filter.getQueryParam("directChildrenOf"); if (!nullOrEmpty(directChildrenOf)) { - filter.queryParams.put( - "directChildrenOfHash", FullyQualifiedName.buildHash(directChildrenOf)); - condition = - String.format( - " %s AND fqnHash = CONCAT(:directChildrenOfHash, '.', MD5(CASE WHEN name LIKE '%%.%%' THEN CONCAT('\"', name, '\"') ELSE name END)) ", - condition); + String parentFqnHash = FullyQualifiedName.buildHash(directChildrenOf); + filter.queryParams.put("fqnHashSingleLevel", parentFqnHash + ".%"); + filter.queryParams.put("fqnHashNestedLevel", parentFqnHash + ".%.%"); + + condition += + " AND fqnHash LIKE :fqnHashSingleLevel AND fqnHash NOT LIKE :fqnHashNestedLevel"; } return listBefore( @@ -2527,19 +2596,24 @@ default List listAfter(ListFilter filter, int limit, String afterName, S String directChildrenOf = filter.getQueryParam("directChildrenOf"); if (!nullOrEmpty(directChildrenOf)) { - filter.queryParams.put( - "directChildrenOfHash", FullyQualifiedName.buildHash(directChildrenOf)); - condition = - String.format( - " %s AND fqnHash = CONCAT(:directChildrenOfHash, '.', MD5(CASE WHEN name LIKE '%%.%%' THEN CONCAT('\"', name, '\"') ELSE name END)) ", - condition); + String parentFqnHash = FullyQualifiedName.buildHash(directChildrenOf); + filter.queryParams.put("fqnHashSingleLevel", parentFqnHash + ".%"); + filter.queryParams.put("fqnHashNestedLevel", parentFqnHash + ".%.%"); + + condition += + " AND fqnHash LIKE :fqnHashSingleLevel AND fqnHash NOT LIKE :fqnHashNestedLevel"; } return listAfter( getTableName(), filter.getQueryParams(), condition, limit, afterName, afterId); } - @SqlQuery("select json FROM glossary_term_entity where fqnhash LIKE CONCAT(:fqnhash, '.%')") - List getNestedTerms(@BindFQN("fqnhash") String fqnhash); + @SqlQuery("select json FROM glossary_term_entity where fqnhash LIKE :concatFqnhash ") + List getNestedTerms( + @BindConcat( + value = "concatFqnhash", + parts = {":fqnhash", ".%"}, + hash = true) + String fqnhash); } interface IngestionPipelineDAO extends EntityDAO { @@ -3170,8 +3244,13 @@ default List listAfter(ListFilter filter, int limit, String afterName, S afterId); } - @SqlQuery("select json FROM tag where fqnhash LIKE CONCAT(:fqnhash, '.%')") - List getTagsStartingWithPrefix(@BindFQN("fqnhash") String fqnhash); + @SqlQuery("select json FROM tag where fqnhash LIKE :concatFqnhash") + List getTagsStartingWithPrefix( + @BindConcat( + value = "concatFqnhash", + parts = {":fqnhash", ".%"}, + hash = true) + String fqnhash); } @RegisterRowMapper(TagLabelMapper.class) @@ -3200,10 +3279,11 @@ default List getTags(String targetFQN) { default Map> getTagsByPrefix( String targetFQNPrefix, String postfix, boolean requiresFqnHash) { - String fqnHash = + String targetFQNPrefixHash = requiresFqnHash ? FullyQualifiedName.buildHash(targetFQNPrefix) : targetFQNPrefix; Map> resultSet = new LinkedHashMap<>(); - List> tags = getTagsInternalByPrefix(fqnHash, postfix); + List> tags = + getTagsInternalByPrefix(new String[] {targetFQNPrefixHash, postfix}); tags.forEach( pair -> { String targetHash = pair.getLeft(); @@ -3249,7 +3329,7 @@ List getTagsInternalBatch( + " ON ta.fqnHash = tu.tagFQNHash " + " WHERE tu.source = 0 " + ") AS combined_data " - + "WHERE combined_data.targetFQNHash LIKE CONCAT(:targetFQNHashPrefix, :postfix)", + + "WHERE combined_data.targetFQNHash LIKE :targetFQNHash", connectionType = MYSQL) @ConnectionAwareSqlQuery( value = @@ -3265,11 +3345,14 @@ List getTagsInternalBatch( + " JOIN tag_usage AS tu ON ta.fqnHash = tu.tagFQNHash " + " WHERE tu.source = 0 " + ") AS combined_data " - + "WHERE combined_data.targetFQNHash LIKE CONCAT(:targetFQNHashPrefix, :postfix)", + + "WHERE combined_data.targetFQNHash LIKE :targetFQNHash", connectionType = POSTGRES) @RegisterRowMapper(TagLabelRowMapperWithTargetFqnHash.class) List> getTagsInternalByPrefix( - @Bind("targetFQNHashPrefix") String targetFQNHashPrefix, @Bind("postfix") String postfix); + @BindConcat( + value = "targetFQNHash", + parts = {":targetFQNHashPrefix", ":postfix"}) + String... targetFQNHash); @SqlQuery("SELECT * FROM tag_usage") @Deprecated(since = "Release 1.1") @@ -3278,17 +3361,29 @@ List> getTagsInternalByPrefix( @SqlQuery( "SELECT COUNT(*) FROM tag_usage " - + "WHERE (tagFQNHash LIKE CONCAT(:tagFqnHash, '.%') OR tagFQNHash = :tagFqnHash) " + + "WHERE (tagFQNHash LIKE :concatTagFQNHash OR tagFQNHash = :tagFqnHash) " + "AND source = :source") - int getTagCount(@Bind("source") int source, @BindFQN("tagFqnHash") String tagFqnHash); + int getTagCount( + @Bind("source") int source, + @BindConcat( + value = "concatTagFQNHash", + original = "tagFqnHash", + parts = {":tagFqnHash", ".%"}, + hash = true) + String tagFqnHash); @SqlUpdate("DELETE FROM tag_usage where targetFQNHash = :targetFQNHash") void deleteTagsByTarget(@BindFQN("targetFQNHash") String targetFQNHash); @SqlUpdate( - "DELETE FROM tag_usage where tagFQNHash = :tagFqnHash AND targetFQNHash LIKE CONCAT(:targetFQNHash, '%')") + "DELETE FROM tag_usage where tagFQNHash = :tagFqnHash AND targetFQNHash LIKE :targetFQNHash") void deleteTagsByTagAndTargetEntity( - @BindFQN("tagFqnHash") String tagFqnHash, @BindFQN("targetFQNHash") String targetFQNHash); + @BindFQN("tagFqnHash") String tagFqnHash, + @BindConcat( + value = "targetFQNHash", + parts = {":targetFQNHashPrefix", "%"}, + hash = true) + String targetFQNHashPrefix); @SqlUpdate("DELETE FROM tag_usage where tagFQNHash = :tagFQNHash AND source = :source") void deleteTagLabels(@Bind("source") int source, @BindFQN("tagFQNHash") String tagFQNHash); @@ -3297,8 +3392,14 @@ void deleteTagsByTagAndTargetEntity( void deleteTagLabelsByFqn(@BindFQN("tagFQNHash") String tagFQNHash); @SqlUpdate( - "DELETE FROM tag_usage where targetFQNHash = :targetFQNHash OR targetFQNHash LIKE CONCAT(:targetFQNHash, '.%')") - void deleteTagLabelsByTargetPrefix(@BindFQN("targetFQNHash") String targetFQNHash); + "DELETE FROM tag_usage where targetFQNHash = :targetFQNHash OR targetFQNHash LIKE :concatTargetFQNHash") + void deleteTagLabelsByTargetPrefix( + @BindConcat( + value = "concatTargetFQNHash", + original = "targetFQNHash", + parts = {":targetFQNHashPrefix", ".%"}, + hash = true) + String targetFQNHashPrefix); @Deprecated(since = "Release 1.1") @ConnectionAwareSqlUpdate( @@ -3380,9 +3481,14 @@ void renameInternal( @RegisterRowMapper(TagLabelMapper.class) List getTargetFQNHashForTag(@BindFQN("tagFQNHash") String tagFQNHash); - @SqlQuery("select targetFQNHash FROM tag_usage where tagFQNHash LIKE CONCAT(:tagFQNHash, '.%')") + @SqlQuery("select targetFQNHash FROM tag_usage where tagFQNHash LIKE :tagFQNHash") @RegisterRowMapper(TagLabelMapper.class) - List getTargetFQNHashForTagPrefix(@BindFQN("tagFQNHash") String tagFQNHash); + List getTargetFQNHashForTagPrefix( + @BindConcat( + value = "tagFQNHash", + parts = {":tagFQNHashPrefix", ".%"}, + hash = true) + String tagFQNHashPrefix); class TagLabelMapper implements RowMapper { @Override @@ -5586,8 +5692,8 @@ default List listBefore( if (fqnPrefix != null) { String fqnPrefixHash = FullyQualifiedName.buildHash(fqnPrefix); filter.queryParams.put("fqnPrefixHash", fqnPrefixHash); - String fqnCond = - " AND (fqnHash LIKE CONCAT(:fqnPrefixHash, '.%') OR fqnHash=:fqnPrefixHash)"; + filter.queryParams.put("concatFqnPrefixHash", fqnPrefixHash + ".%"); + String fqnCond = " AND (fqnHash LIKE :concatFqnPrefixHash OR fqnHash=:fqnPrefixHash)"; mysqlCondition.append(fqnCond); psqlCondition.append(fqnCond); } @@ -5625,8 +5731,8 @@ default List listAfter(ListFilter filter, int limit, String afterName, S if (fqnPrefix != null) { String fqnPrefixHash = FullyQualifiedName.buildHash(fqnPrefix); filter.queryParams.put("fqnPrefixHash", fqnPrefixHash); - String fqnCond = - " AND (fqnHash LIKE CONCAT(:fqnPrefixHash, '.%') OR fqnHash=:fqnPrefixHash)"; + filter.queryParams.put("concatFqnPrefixHash", fqnPrefixHash + ".%"); + String fqnCond = " AND (fqnHash LIKE :concatFqnPrefixHash OR fqnHash=:fqnPrefixHash)"; mysqlCondition.append(fqnCond); psqlCondition.append(fqnCond); } @@ -5663,8 +5769,8 @@ default int listCount(ListFilter filter) { if (fqnPrefix != null) { String fqnPrefixHash = FullyQualifiedName.buildHash(fqnPrefix); filter.queryParams.put("fqnPrefixHash", fqnPrefixHash); - String fqnCond = - " AND (fqnHash LIKE CONCAT(:fqnPrefixHash, '.%') OR fqnHash=:fqnPrefixHash)"; + filter.queryParams.put("concatFqnPrefixHash", fqnPrefixHash + ".%"); + String fqnCond = " AND (fqnHash LIKE :concatFqnPrefixHash OR fqnHash=:fqnPrefixHash)"; mysqlCondition.append(fqnCond); psqlCondition.append(fqnCond); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/BindConcat.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/BindConcat.java new file mode 100644 index 000000000000..733aa0515975 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/jdbi/BindConcat.java @@ -0,0 +1,80 @@ +package org.openmetadata.service.util.jdbi; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizerFactory; +import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizingAnnotation; +import org.jdbi.v3.sqlobject.customizer.SqlStatementParameterCustomizer; +import org.openmetadata.service.util.FullyQualifiedName; + +/** Concatenate parts of a string to bind as a parameter, and avoid usage of CONCAT() in where clause */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +@SqlStatementCustomizingAnnotation(BindConcat.Factory.class) +public @interface BindConcat { + String value(); // Name of the concatenated parameter to bind + + String original() default + ""; // Optional: Use when both the original and concatenated values are needed + + String[] parts() default {}; // Parts to concatenate (placeholders or static values) + + boolean hash() default false; // Optional: Apply FullyQualifiedName.buildHash if true + + class Factory implements SqlStatementCustomizerFactory { + + @Override + public SqlStatementParameterCustomizer createForParameter( + Annotation annotation, + Class sqlObjectType, + Method method, + Parameter param, + int index, + Type type) { + BindConcat bind = (BindConcat) annotation; + + return (stmt, arg) -> { + String[] partValues = + (arg instanceof String[]) ? (String[]) arg : new String[] {String.valueOf(arg)}; + StringBuilder concatenatedResult = new StringBuilder(); + boolean containsNull = false; + + for (int i = 0; i < bind.parts().length; i++) { + String part = bind.parts()[i]; + if (part.startsWith(":")) { // Dynamic value in argument list to replace placeholder + if (i >= partValues.length) + throw new IllegalArgumentException( + "Not enough values for placeholders in @BindConcat. Expected at least " + + (i + 1) + + " but got " + + partValues.length); + String dynamicValue = partValues[i]; + if (dynamicValue == null) { + containsNull = true; + break; + } + concatenatedResult.append( + bind.hash() ? FullyQualifiedName.buildHash(dynamicValue) : dynamicValue); + } else { // Static part of the string, defined directly in the annotation + concatenatedResult.append(part); + } + } + + String finalValue = containsNull ? null : concatenatedResult.toString(); + stmt.bind(bind.value(), finalValue); + if (!bind.original().isEmpty() && partValues.length > 0) { + String originalValue = partValues[0]; + stmt.bind( + bind.original(), + bind.hash() ? FullyQualifiedName.buildHash(originalValue) : originalValue); + } + }; + } + } +}