@@ -808,6 +808,170 @@ public void testUnsupportedFieldReferenceExpression() {
808808 .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
809809 }
810810
811+ // -------------------------------------------------------------------------
812+ // Tests for convertSimilarToLike
813+ // -------------------------------------------------------------------------
814+
815+ @ Test
816+ public void testConvertSimilarToLike () {
817+ // '%' stays as '%' (SQL LIKE wildcard)
818+ assertThat (convertSimilarToLike ("abc%" , null )).isEqualTo ("abc%" );
819+ // '_' stays as '_' (SQL LIKE wildcard)
820+ assertThat (convertSimilarToLike ("a_c" , null )).isEqualTo ("a_c" );
821+ // Literal characters pass through unchanged
822+ assertThat (convertSimilarToLike ("hello" , null )).isEqualTo ("hello" );
823+ // '.' is just a literal dot in SIMILAR TO — kept as-is in the LIKE output
824+ assertThat (convertSimilarToLike ("3.14" , null )).isEqualTo ("3.14" );
825+ // Backslash in pattern is doubled so downstream Like sees it as a literal '\'
826+ assertThat (convertSimilarToLike ("a\\ b" , null )).isEqualTo ("a\\ \\ b" );
827+ // Empty pattern returns empty
828+ assertThat (convertSimilarToLike ("" , null )).isEqualTo ("" );
829+
830+ // --- SIMILAR TO-only features must throw UnsupportedExpression ---
831+ assertThatThrownBy (() -> convertSimilarToLike ("a|b" , null ))
832+ .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
833+ assertThatThrownBy (() -> convertSimilarToLike ("(a|b)%" , null ))
834+ .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
835+ assertThatThrownBy (() -> convertSimilarToLike ("a+" , null ))
836+ .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
837+ assertThatThrownBy (() -> convertSimilarToLike ("[a-z]" , null ))
838+ .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
839+
840+ // --- Escape-character tests (using '=' as the escape char) ---
841+ // '=_' -> escaped underscore: output '\_' so Like treats '_' as literal
842+ assertThat (convertSimilarToLike ("a=_b" , "=" )).isEqualTo ("a\\ _b" );
843+ // '=%' -> escaped percent: output '\%' so Like treats '%' as literal
844+ assertThat (convertSimilarToLike ("a=%b" , "=" )).isEqualTo ("a\\ %b" );
845+ // '==' -> literal '=' (the escape char)
846+ assertThat (convertSimilarToLike ("a==b" , "=" )).isEqualTo ("a=b" );
847+ // Trailing escape char alone must throw
848+ assertThatThrownBy (() -> convertSimilarToLike ("a=" , "=" ))
849+ .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
850+ // Unknown escape sequence (e.g. '=a') must throw
851+ assertThatThrownBy (() -> convertSimilarToLike ("a=b" , "=" ))
852+ .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
853+ }
854+
855+ /** Invokes the private {@code convertSimilarToLike} via reflection. */
856+ private static String convertSimilarToLike (String sqlPattern , String escape ) {
857+ try {
858+ java .lang .reflect .Method m =
859+ PredicateConverter .class .getDeclaredMethod (
860+ "convertSimilarToLike" , String .class , String .class );
861+ m .setAccessible (true );
862+ return (String ) m .invoke (new PredicateConverter (BUILDER ), sqlPattern , escape );
863+ } catch (java .lang .reflect .InvocationTargetException e ) {
864+ Throwable cause = e .getCause ();
865+ if (cause instanceof RuntimeException ) {
866+ throw (RuntimeException ) cause ;
867+ }
868+ throw new RuntimeException (cause );
869+ } catch (Exception e ) {
870+ throw new RuntimeException (e );
871+ }
872+ }
873+
874+ // -------------------------------------------------------------------------
875+ // Tests for the SIMILAR (SIMILAR TO) branch in visit(CallExpression)
876+ // -------------------------------------------------------------------------
877+
878+ @ Test
879+ public void testSimilarExpressionBasic () {
880+ // 'abc%' SIMILAR TO: any string starting with 'abc'
881+ PredicateConverter converter = new PredicateConverter (RowType .of (new VarCharType ()));
882+ CallExpression expr =
883+ call (
884+ BuiltInFunctionDefinitions .SIMILAR ,
885+ field (0 , STRING ()),
886+ literal ("abc%" , STRING ()));
887+ Predicate predicate = expr .accept (converter );
888+
889+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("abc" )))).isTrue ();
890+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("abcdef" )))).isTrue ();
891+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("ab" )))).isFalse ();
892+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("ABC" )))).isFalse ();
893+ assertThat (predicate .test (GenericRow .of ((Object ) null ))).isFalse ();
894+ }
895+
896+ @ Test
897+ public void testSimilarExpressionUnderscore () {
898+ // 'a_c' SIMILAR TO: exactly 3 chars starting with 'a', ending with 'c'
899+ PredicateConverter converter = new PredicateConverter (RowType .of (new VarCharType ()));
900+ CallExpression expr =
901+ call (
902+ BuiltInFunctionDefinitions .SIMILAR ,
903+ field (0 , STRING ()),
904+ literal ("a_c" , STRING ()));
905+ Predicate predicate = expr .accept (converter );
906+
907+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("abc" )))).isTrue ();
908+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("axc" )))).isTrue ();
909+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("ac" )))).isFalse ();
910+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("abbc" )))).isFalse ();
911+ }
912+
913+ @ Test
914+ public void testSimilarExpressionAlternation () {
915+ // '(cat|dog)%' uses SIMILAR TO-only syntax -> UnsupportedExpression
916+ PredicateConverter converter = new PredicateConverter (RowType .of (new VarCharType ()));
917+ assertThatThrownBy (
918+ () ->
919+ call (
920+ BuiltInFunctionDefinitions .SIMILAR ,
921+ field (0 , STRING ()),
922+ literal ("(cat|dog)%" , STRING ()))
923+ .accept (converter ))
924+ .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
925+ }
926+
927+ @ Test
928+ public void testSimilarExpressionCharacterClass () {
929+ // '[a-z]+' uses SIMILAR TO-only syntax -> UnsupportedExpression
930+ PredicateConverter converter = new PredicateConverter (RowType .of (new VarCharType ()));
931+ assertThatThrownBy (
932+ () ->
933+ call (
934+ BuiltInFunctionDefinitions .SIMILAR ,
935+ field (0 , STRING ()),
936+ literal ("[a-z]+" , STRING ()))
937+ .accept (converter ))
938+ .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
939+ }
940+
941+ @ Test
942+ public void testSimilarExpressionEscapedWildcards () {
943+ // 'a=%b' SIMILAR TO with escape '=': literal 'a%b' (% is not a wildcard)
944+ // convertSimilarToLike converts this to 'a\%b', which Like then processes as 'a' + literal
945+ // '%' + 'b'
946+ PredicateConverter converter = new PredicateConverter (RowType .of (new VarCharType ()));
947+ CallExpression expr =
948+ call (
949+ BuiltInFunctionDefinitions .SIMILAR ,
950+ field (0 , STRING ()),
951+ literal ("a=%b" , STRING ()),
952+ literal ("=" , STRING ()));
953+ Predicate predicate = expr .accept (converter );
954+
955+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("a%b" )))).isTrue ();
956+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("axb" )))).isFalse ();
957+ assertThat (predicate .test (GenericRow .of (BinaryString .fromString ("ab" )))).isFalse ();
958+ }
959+
960+ @ Test
961+ public void testSimilarExpressionNonStringTypeThrows () {
962+ // SIMILAR on a non-string column must throw UnsupportedExpression
963+ assertThatThrownBy (
964+ () ->
965+ call (
966+ BuiltInFunctionDefinitions .SIMILAR ,
967+ field (0 , DataTypes .INT ()),
968+ literal (5 ))
969+ .accept (
970+ new PredicateConverter (
971+ RowType .of (new IntType ()))))
972+ .isInstanceOf (PredicateConverter .UnsupportedExpression .class );
973+ }
974+
811975 private static FieldReferenceExpression field (int i , DataType type ) {
812976 return new FieldReferenceExpression ("f" + i , type , Integer .MAX_VALUE , Integer .MAX_VALUE );
813977 }
0 commit comments