diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANAColumnStoreDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANAColumnStoreDialect.java index 2cafa3aa12..c1c1832220 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANAColumnStoreDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANAColumnStoreDialect.java @@ -6,10 +6,14 @@ */ package org.hibernate.dialect; +import org.hibernate.dialect.function.SQLFunctionTemplate; +import org.hibernate.dialect.function.StandardSQLFunction; +import org.hibernate.dialect.function.VarArgsSQLFunction; import org.hibernate.hql.spi.id.IdTableSupportStandardImpl; import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; import org.hibernate.hql.spi.id.global.GlobalTemporaryTableBulkIdStrategy; import org.hibernate.hql.spi.id.local.AfterUseAction; +import org.hibernate.type.StandardBasicTypes; /** * An SQL dialect for the SAP HANA column store. @@ -29,6 +33,14 @@ public class HANAColumnStoreDialect extends AbstractHANADialect { public HANAColumnStoreDialect() { super(); + + // full-text search functions + registerFunction( "score", new StandardSQLFunction( "score", StandardBasicTypes.DOUBLE ) ); + registerFunction( "snippets", new StandardSQLFunction( "snippets" ) ); + registerFunction( "highlighted", new StandardSQLFunction( "highlighted" ) ); + registerFunction( "contains", new VarArgsSQLFunction( StandardBasicTypes.BOOLEAN, "contains(", ",", ") /*" ) ); + registerFunction( "contains_rhs", new SQLFunctionTemplate( StandardBasicTypes.BOOLEAN, "*/" ) ); + registerFunction( "not_contains", new VarArgsSQLFunction( StandardBasicTypes.BOOLEAN, "not contains(", ",", ") /*" ) ); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/test/dialect/functional/HANASearchTest.java b/hibernate-core/src/test/java/org/hibernate/test/dialect/functional/HANASearchTest.java new file mode 100644 index 0000000000..6050253fe7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/dialect/functional/HANASearchTest.java @@ -0,0 +1,255 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.dialect.functional; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + +import java.sql.PreparedStatement; + +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.hibernate.Session; +import org.hibernate.dialect.HANAColumnStoreDialect; +import org.hibernate.query.Query; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Test; + +/** + * Tests the correctness of the SAP HANA fulltext-search functions. + * + * @author Jonathan Bregler + */ +@RequiresDialect(value = { HANAColumnStoreDialect.class }) +public class HANASearchTest extends BaseCoreFunctionalTestCase { + + private static final String ENTITY_NAME = "SearchEntity"; + + @Override + protected void prepareTest() throws Exception { + doInHibernate( this::sessionFactory, session -> { + session.doWork( connection -> { + try ( PreparedStatement ps = connection.prepareStatement( "CREATE COLUMN TABLE " + ENTITY_NAME + + " (key INTEGER, t TEXT, c NVARCHAR(255), PRIMARY KEY (key))" ) ) { + ps.execute(); + } + try ( PreparedStatement ps = connection + .prepareStatement( "CREATE FULLTEXT INDEX FTI ON " + ENTITY_NAME + " (c)" ) ) { + ps.execute(); + } + } ); + } ); + } + + @Override + protected void cleanupTest() throws Exception { + doInHibernate( this::sessionFactory, session -> { + session.doWork( connection -> { + try ( PreparedStatement ps = connection.prepareStatement( "DROP TABLE " + ENTITY_NAME + " CASCADE" ) ) { + ps.execute(); + } + catch (Exception e) { + // Ignore + } + } ); + } ); + } + + @Test + @TestForIssue(jiraKey = "HHH-13021") + public void testTextType() throws Exception { + doInHibernate( this::sessionFactory, s -> { + SearchEntity entity = new SearchEntity(); + entity.key = Integer.valueOf( 1 ); + entity.t = "TEST TEXT"; + entity.c = "TEST STRING"; + + s.persist( entity ); + + s.flush(); + + Query legacyQuery = s.createQuery( "select b, snippets(t), highlighted(t), score() from " + + ENTITY_NAME + " b where contains(b.t, 'text') = contains_rhs()", Object[].class ); + + Object[] result = legacyQuery.getSingleResult(); + + SearchEntity retrievedEntity = (SearchEntity) result[0]; + + assertEquals( 4, result.length ); + + assertEquals( Integer.valueOf( 1 ), retrievedEntity.key ); + assertEquals( "TEST TEXT", retrievedEntity.t ); + assertEquals( "TEST STRING", retrievedEntity.c ); + + assertEquals( "TEST TEXT", result[1] ); + assertEquals( "TEST TEXT", result[2] ); + assertEquals( 0.75d, result[3] ); + } ); + } + + @Test + @TestForIssue(jiraKey = "HHH-13021") + public void testTextTypeFalse() throws Exception { + doInHibernate( this::sessionFactory, s -> { + SearchEntity entity = new SearchEntity(); + entity.key = Integer.valueOf( 1 ); + entity.t = "TEST TEXT"; + entity.c = "TEST STRING"; + + s.persist( entity ); + + s.flush(); + + Query legacyQuery = s.createQuery( "select b, snippets(t), highlighted(t), score() from " + ENTITY_NAME + + " b where not_contains(b.t, 'string') = contains_rhs()", Object[].class ); + + Object[] result = legacyQuery.getSingleResult(); + + SearchEntity retrievedEntity = (SearchEntity) result[0]; + + assertEquals( 4, result.length ); + + assertEquals( Integer.valueOf( 1 ), retrievedEntity.key ); + assertEquals( "TEST TEXT", retrievedEntity.t ); + assertEquals( "TEST STRING", retrievedEntity.c ); + + assertEquals( "TEST TEXT", result[1] ); + assertEquals( "TEST TEXT", result[2] ); + assertEquals( 1d, result[3] ); + } ); + } + + @Test + @TestForIssue(jiraKey = "HHH-13021") + public void testCharType() throws Exception { + doInHibernate( this::sessionFactory, s -> { + SearchEntity entity = new SearchEntity(); + entity.key = Integer.valueOf( 1 ); + entity.t = "TEST TEXT"; + entity.c = "TEST STRING"; + + s.persist( entity ); + + s.getTransaction().commit(); + s.beginTransaction(); + + Query legacyQuery = s.createQuery( "select b, snippets(c), highlighted(c), score() from " + ENTITY_NAME + + " b where contains(b.c, 'string') = contains_rhs()", Object[].class ); + + Object[] result = legacyQuery.getSingleResult(); + + SearchEntity retrievedEntity = (SearchEntity) result[0]; + + assertEquals( 4, result.length ); + + assertEquals( Integer.valueOf( 1 ), retrievedEntity.key ); + assertEquals( "TEST TEXT", retrievedEntity.t ); + assertEquals( "TEST STRING", retrievedEntity.c ); + + assertEquals( "TEST STRING", result[1] ); + assertEquals( "TEST STRING", result[2] ); + assertEquals( 0.75d, result[3] ); + } ); + } + + @Test + @TestForIssue(jiraKey = "HHH-13021") + public void testCharTypeComplexQuery() throws Exception { + doInHibernate( this::sessionFactory, s -> { + SearchEntity entity = new SearchEntity(); + entity.key = Integer.valueOf( 1 ); + entity.t = "TEST TEXT"; + entity.c = "TEST STRING"; + + s.persist( entity ); + + s.flush(); + + s.getTransaction().commit(); + s.beginTransaction(); + + Query legacyQuery = s.createQuery( + "select b, snippets(c), highlighted(c), score() from " + ENTITY_NAME + + " b where contains(b.c, 'string') = contains_rhs() and key=1 and score() > 0.5", + Object[].class ); + + Object[] result = legacyQuery.getSingleResult(); + + SearchEntity retrievedEntity = (SearchEntity) result[0]; + + assertEquals( 4, result.length ); + + assertEquals( Integer.valueOf( 1 ), retrievedEntity.key ); + assertEquals( "TEST TEXT", retrievedEntity.t ); + assertEquals( "TEST STRING", retrievedEntity.c ); + + assertEquals( "TEST STRING", result[1] ); + assertEquals( "TEST STRING", result[2] ); + assertEquals( 0.75d, result[3] ); + } ); + } + + @Test + @TestForIssue(jiraKey = "HHH-13021") + public void testFuzzy() throws Exception { + doInHibernate( this::sessionFactory, s -> { + SearchEntity entity = new SearchEntity(); + entity.key = Integer.valueOf( 1 ); + entity.t = "TEST TEXT"; + entity.c = "TEST STRING"; + + s.persist( entity ); + + s.flush(); + + s.getTransaction().commit(); + s.beginTransaction(); + + Query legacyQuery = s.createQuery( "select b, snippets(c), highlighted(c), score() from " + ENTITY_NAME + + " b where contains(b.c, 'string', FUZZY(0.7)) = contains_rhs()", Object[].class ); + + Object[] result = legacyQuery.getSingleResult(); + + SearchEntity retrievedEntity = (SearchEntity) result[0]; + + assertEquals( 4, result.length ); + + assertEquals( Integer.valueOf( 1 ), retrievedEntity.key ); + assertEquals( "TEST TEXT", retrievedEntity.t ); + assertEquals( "TEST STRING", retrievedEntity.c ); + + assertEquals( "TEST STRING", result[1] ); + assertEquals( "TEST STRING", result[2] ); + assertEquals( 0.75d, result[3] ); + } ); + } + + @Override + protected boolean createSchema() { + return false; + } + + @Override + protected java.lang.Class[] getAnnotatedClasses() { + return new java.lang.Class[]{ SearchEntity.class }; + } + + @Entity(name = ENTITY_NAME) + public static class SearchEntity { + + @Id + public Integer key; + + public String t; + + public String c; + } + +}