From ab100a6d5ff681863fcaa315603b1278897d981c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=98=81?= Date: Wed, 25 Mar 2026 12:34:16 +0900 Subject: [PATCH 1/5] Add scalar function call methods to SQLExpressions Add convenience methods for creating scalar function call expressions that support fully qualified (multi-part) function names such as schema.function, database.schema.function, or linked_server.database.schema.function. New methods: - function(Class, String, Object...) - generic scalar function call - stringFunction(String, Object...) - String-returning function call - numberFunction(Class, String, Object...) - Number-returning function call These complement the existing relationalFunctionCall() which is designed for table-valued functions in FROM/JOIN clauses. --- .../java/com/querydsl/sql/SQLExpressions.java | 92 ++++++++++++ .../sql/SQLExpressionsFunctionTest.java | 133 ++++++++++++++++++ .../com/querydsl/sql/SerializationTest.java | 34 +++++ 3 files changed, 259 insertions(+) create mode 100644 querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java diff --git a/querydsl-libraries/querydsl-sql/src/main/java/com/querydsl/sql/SQLExpressions.java b/querydsl-libraries/querydsl-sql/src/main/java/com/querydsl/sql/SQLExpressions.java index 535b077b44..7578dbeca8 100644 --- a/querydsl-libraries/querydsl-sql/src/main/java/com/querydsl/sql/SQLExpressions.java +++ b/querydsl-libraries/querydsl-sql/src/main/java/com/querydsl/sql/SQLExpressions.java @@ -20,14 +20,20 @@ import com.querydsl.core.types.Ops; import com.querydsl.core.types.Path; import com.querydsl.core.types.SubQueryExpression; +import com.querydsl.core.types.Template; +import com.querydsl.core.types.TemplateFactory; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.DateExpression; import com.querydsl.core.types.dsl.DateTimeExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.core.types.dsl.NumberTemplate; import com.querydsl.core.types.dsl.SimpleExpression; +import com.querydsl.core.types.dsl.SimpleTemplate; import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.core.types.dsl.StringTemplate; import com.querydsl.core.types.dsl.Wildcard; +import java.util.Arrays; import java.util.EnumMap; import java.util.List; import java.util.Map; @@ -278,6 +284,92 @@ public static RelationalFunctionCall relationalFunctionCall( return new RelationalFunctionCall<>(type, function, args); } + /** + * Create a function call template string for the given function name and argument count. + * + *

The function name is inserted as-is, which allows fully qualified names such as {@code + * schema.function}, {@code database.schema.function}, or {@code + * linked_server.database.schema.function}. + * + * @param function function name (may contain dots for qualified names) + * @param argCount number of arguments + * @return template for the function call + */ + private static Template createFunctionCallTemplate(String function, int argCount) { + var builder = new StringBuilder(); + builder.append(function); + builder.append("("); + for (var i = 0; i < argCount; i++) { + if (i > 0) { + builder.append(", "); + } + builder.append("{").append(i).append("}"); + } + builder.append(")"); + return TemplateFactory.DEFAULT.create(builder.toString()); + } + + /** + * Create a scalar function call expression. + * + *

Unlike {@link #relationalFunctionCall(Class, String, Object...)}, this method creates a + * scalar expression suitable for use in SELECT, WHERE, INSERT VALUES, and other non-FROM + * contexts. + * + *

Supports fully qualified function names including cross-database calls: + * + *

    + *
  • {@code function("my_function", arg)} → {@code my_function(?)} + *
  • {@code function("dbo.my_function", arg1, arg2)} → {@code dbo.my_function(?, ?)} + *
  • {@code function("other_db.dbo.my_function", arg)} → {@code + * other_db.dbo.my_function(?)} + *
+ * + * @param return type + * @param type return type class + * @param function function name (may contain dots for schema-qualified names) + * @param args function arguments + * @return scalar function call expression + */ + public static SimpleTemplate function( + Class type, String function, Object... args) { + return Expressions.template( + type, createFunctionCallTemplate(function, args.length), Arrays.asList(args)); + } + + /** + * Create a scalar function call expression that returns a String. + * + *

Supports fully qualified function names including cross-database calls. + * + * @param function function name (may contain dots for schema-qualified names) + * @param args function arguments + * @return string function call expression + * @see #function(Class, String, Object...) + */ + public static StringTemplate stringFunction(String function, Object... args) { + return Expressions.stringTemplate( + createFunctionCallTemplate(function, args.length), Arrays.asList(args)); + } + + /** + * Create a scalar function call expression that returns a Number. + * + *

Supports fully qualified function names including cross-database calls. + * + * @param number type + * @param type return type class + * @param function function name (may contain dots for schema-qualified names) + * @param args function arguments + * @return number function call expression + * @see #function(Class, String, Object...) + */ + public static > NumberTemplate numberFunction( + Class type, String function, Object... args) { + return Expressions.numberTemplate( + type, createFunctionCallTemplate(function, args.length), Arrays.asList(args)); + } + /** * Create a nextval(sequence) expression * diff --git a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java new file mode 100644 index 0000000000..8442a07a85 --- /dev/null +++ b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.querydsl.sql; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberTemplate; +import com.querydsl.core.types.dsl.SimpleTemplate; +import com.querydsl.core.types.dsl.StringTemplate; +import com.querydsl.sql.domain.QEmployee; +import com.querydsl.sql.domain.QSurvey; +import org.junit.Test; + +public class SQLExpressionsFunctionTest { + + private static final QSurvey survey = QSurvey.survey; + + private static final QEmployee employee = QEmployee.employee; + + @Test + public void simpleFunction() { + SimpleTemplate expr = + SQLExpressions.function(String.class, "my_function", survey.name); + assertThat(expr.toString()).isEqualTo("my_function(SURVEY.NAME)"); + } + + @Test + public void twoPartName() { + StringTemplate expr = + SQLExpressions.stringFunction("dbo.my_function", survey.name, survey.name2); + assertThat(expr.toString()).isEqualTo("dbo.my_function(SURVEY.NAME, SURVEY.NAME2)"); + } + + @Test + public void threePartName() { + StringTemplate expr = + SQLExpressions.stringFunction( + "external_db.dbo.my_function", + ConstantImpl.create("PARAM"), + survey.name, + ConstantImpl.create("")); + assertThat(expr.toString()) + .isEqualTo("external_db.dbo.my_function(PARAM, SURVEY.NAME, )"); + } + + @Test + public void numberFunction() { + NumberTemplate expr = + SQLExpressions.numberFunction(Integer.class, "dbo.calculate", survey.id); + assertThat(expr.toString()).isEqualTo("dbo.calculate(SURVEY.ID)"); + } + + @Test + public void stringFunction() { + StringTemplate expr = SQLExpressions.stringFunction("my_encrypt", survey.name); + assertThat(expr.toString()).isEqualTo("my_encrypt(SURVEY.NAME)"); + } + + @Test + public void functionInSelect() { + SQLQuery query = new SQLQuery(SQLServerTemplates.DEFAULT); + query + .select(SQLExpressions.stringFunction("dbo.my_function", survey.name)) + .from(survey); + assertThat(query.toString()) + .isEqualTo( + """ + select dbo.my_function(SURVEY.NAME) + from SURVEY SURVEY"""); + } + + @Test + public void functionInWhere() { + SQLQuery query = new SQLQuery(SQLServerTemplates.DEFAULT); + query + .select(survey.name) + .from(survey) + .where( + SQLExpressions.stringFunction("dbo.decrypt", survey.name) + .eq("expected")); + assertThat(query.toString()) + .isEqualTo( + """ + select SURVEY.NAME + from SURVEY SURVEY + where dbo.decrypt(SURVEY.NAME) = ?"""); + } + + @Test + public void functionInInsertValues() { + SQLInsertClause insert = + new SQLInsertClause(null, new Configuration(SQLServerTemplates.DEFAULT), survey); + insert.set(survey.name, SQLExpressions.stringFunction("dbo.encrypt", Expressions.constant("value"))); + + assertThat(insert.toString()) + .contains("dbo.encrypt(?)"); + } + + @Test + public void fourPartName() { + StringTemplate expr = + SQLExpressions.stringFunction( + "linked_server.external_db.dbo.my_function", survey.name); + assertThat(expr.toString()) + .isEqualTo("linked_server.external_db.dbo.my_function(SURVEY.NAME)"); + } + + @Test + public void multipleArguments() { + StringTemplate expr = + SQLExpressions.stringFunction( + "schema.func", + ConstantImpl.create("A"), + survey.name, + survey.id, + ConstantImpl.create("B")); + assertThat(expr.toString()) + .isEqualTo("schema.func(A, SURVEY.NAME, SURVEY.ID, B)"); + } +} diff --git a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SerializationTest.java b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SerializationTest.java index 7e7317348d..18d3b98144 100644 --- a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SerializationTest.java +++ b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SerializationTest.java @@ -157,6 +157,40 @@ on not (SURVEY.NAME like tokFunc.prop escape '\\')\ """); } + @Test + public void scalarFunctionCall() { + var expr = + SQLExpressions.select( + SQLExpressions.stringFunction("external_db.dbo.my_function", survey.name)) + .from(survey); + + var serializer = new SQLSerializer(new Configuration(new SQLServerTemplates())); + serializer.serialize(expr.getMetadata(), false); + assertThat(serializer.toString()) + .isEqualTo( + """ + select external_db.dbo.my_function(SURVEY.NAME) + from SURVEY SURVEY\ + """); + } + + @Test + public void scalarFunctionCallInWhere() { + var query = new SQLQuery(new Configuration(new SQLServerTemplates())); + query + .select(survey.name) + .from(survey) + .where(SQLExpressions.stringFunction("dbo.decrypt", survey.name).eq("test")); + + assertThat(query.toString()) + .isEqualTo( + """ + select SURVEY.NAME + from SURVEY SURVEY + where dbo.decrypt(SURVEY.NAME) = ?\ + """); + } + @Test public void functionCall3() { RelationalFunctionCall func = From ace933654ee3bc7762bea413111b10ee8fe6a731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=98=81?= Date: Wed, 25 Mar 2026 12:49:44 +0900 Subject: [PATCH 2/5] Fix missing SQLInsertClause import in test --- .../test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java index 8442a07a85..7dc8419049 100644 --- a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java +++ b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java @@ -20,6 +20,7 @@ import com.querydsl.core.types.dsl.NumberTemplate; import com.querydsl.core.types.dsl.SimpleTemplate; import com.querydsl.core.types.dsl.StringTemplate; +import com.querydsl.sql.dml.SQLInsertClause; import com.querydsl.sql.domain.QEmployee; import com.querydsl.sql.domain.QSurvey; import org.junit.Test; From 7b8dc6fd5624570e88c40eaa6d2f6dcd1021055f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=98=81?= Date: Wed, 25 Mar 2026 12:54:11 +0900 Subject: [PATCH 3/5] Fix ambiguous SQLInsertClause constructor call in test --- .../test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java index 7dc8419049..75adeb8227 100644 --- a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java +++ b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java @@ -103,7 +103,7 @@ public void functionInWhere() { @Test public void functionInInsertValues() { SQLInsertClause insert = - new SQLInsertClause(null, new Configuration(SQLServerTemplates.DEFAULT), survey); + new SQLInsertClause((java.sql.Connection) null, new Configuration(SQLServerTemplates.DEFAULT), survey); insert.set(survey.name, SQLExpressions.stringFunction("dbo.encrypt", Expressions.constant("value"))); assertThat(insert.toString()) From f973bae04a44d4a6f40203539aa382aed589f2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=98=81?= Date: Wed, 25 Mar 2026 15:19:53 +0900 Subject: [PATCH 4/5] Fix code formatting to pass google-java-format validation --- .../sql/SQLExpressionsFunctionTest.java | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java index 75adeb8227..5df757aa17 100644 --- a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java +++ b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java @@ -21,7 +21,6 @@ import com.querydsl.core.types.dsl.SimpleTemplate; import com.querydsl.core.types.dsl.StringTemplate; import com.querydsl.sql.dml.SQLInsertClause; -import com.querydsl.sql.domain.QEmployee; import com.querydsl.sql.domain.QSurvey; import org.junit.Test; @@ -29,12 +28,9 @@ public class SQLExpressionsFunctionTest { private static final QSurvey survey = QSurvey.survey; - private static final QEmployee employee = QEmployee.employee; - @Test public void simpleFunction() { - SimpleTemplate expr = - SQLExpressions.function(String.class, "my_function", survey.name); + SimpleTemplate expr = SQLExpressions.function(String.class, "my_function", survey.name); assertThat(expr.toString()).isEqualTo("my_function(SURVEY.NAME)"); } @@ -53,8 +49,7 @@ public void threePartName() { ConstantImpl.create("PARAM"), survey.name, ConstantImpl.create("")); - assertThat(expr.toString()) - .isEqualTo("external_db.dbo.my_function(PARAM, SURVEY.NAME, )"); + assertThat(expr.toString()).isEqualTo("external_db.dbo.my_function(PARAM, SURVEY.NAME, )"); } @Test @@ -73,14 +68,13 @@ public void stringFunction() { @Test public void functionInSelect() { SQLQuery query = new SQLQuery(SQLServerTemplates.DEFAULT); - query - .select(SQLExpressions.stringFunction("dbo.my_function", survey.name)) - .from(survey); + query.select(SQLExpressions.stringFunction("dbo.my_function", survey.name)).from(survey); assertThat(query.toString()) .isEqualTo( """ select dbo.my_function(SURVEY.NAME) - from SURVEY SURVEY"""); + from SURVEY SURVEY\ + """); } @Test @@ -89,34 +83,31 @@ public void functionInWhere() { query .select(survey.name) .from(survey) - .where( - SQLExpressions.stringFunction("dbo.decrypt", survey.name) - .eq("expected")); + .where(SQLExpressions.stringFunction("dbo.decrypt", survey.name).eq("expected")); assertThat(query.toString()) .isEqualTo( """ select SURVEY.NAME from SURVEY SURVEY - where dbo.decrypt(SURVEY.NAME) = ?"""); + where dbo.decrypt(SURVEY.NAME) = ?\ + """); } @Test public void functionInInsertValues() { - SQLInsertClause insert = - new SQLInsertClause((java.sql.Connection) null, new Configuration(SQLServerTemplates.DEFAULT), survey); - insert.set(survey.name, SQLExpressions.stringFunction("dbo.encrypt", Expressions.constant("value"))); + var config = new Configuration(SQLServerTemplates.DEFAULT); + SQLInsertClause insert = new SQLInsertClause((java.sql.Connection) null, config, survey); + insert.set( + survey.name, SQLExpressions.stringFunction("dbo.encrypt", Expressions.constant("value"))); - assertThat(insert.toString()) - .contains("dbo.encrypt(?)"); + assertThat(insert.toString()).contains("dbo.encrypt(?)"); } @Test public void fourPartName() { StringTemplate expr = - SQLExpressions.stringFunction( - "linked_server.external_db.dbo.my_function", survey.name); - assertThat(expr.toString()) - .isEqualTo("linked_server.external_db.dbo.my_function(SURVEY.NAME)"); + SQLExpressions.stringFunction("linked_server.external_db.dbo.my_function", survey.name); + assertThat(expr.toString()).isEqualTo("linked_server.external_db.dbo.my_function(SURVEY.NAME)"); } @Test @@ -128,7 +119,6 @@ public void multipleArguments() { survey.name, survey.id, ConstantImpl.create("B")); - assertThat(expr.toString()) - .isEqualTo("schema.func(A, SURVEY.NAME, SURVEY.ID, B)"); + assertThat(expr.toString()).isEqualTo("schema.func(A, SURVEY.NAME, SURVEY.ID, B)"); } } From 63fbef3cbbb1d47f34a88e61cdf7c3b50232c1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=8A=B9=EC=98=81?= Date: Wed, 25 Mar 2026 15:29:07 +0900 Subject: [PATCH 5/5] Fix test assertions to be case-insensitive for column names --- .../querydsl/sql/SQLExpressionsFunctionTest.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java index 5df757aa17..5273f3f589 100644 --- a/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java +++ b/querydsl-libraries/querydsl-sql/src/test/java/com/querydsl/sql/SQLExpressionsFunctionTest.java @@ -31,14 +31,14 @@ public class SQLExpressionsFunctionTest { @Test public void simpleFunction() { SimpleTemplate expr = SQLExpressions.function(String.class, "my_function", survey.name); - assertThat(expr.toString()).isEqualTo("my_function(SURVEY.NAME)"); + assertThat(expr.toString()).containsIgnoringCase("my_function(SURVEY.NAME)"); } @Test public void twoPartName() { StringTemplate expr = SQLExpressions.stringFunction("dbo.my_function", survey.name, survey.name2); - assertThat(expr.toString()).isEqualTo("dbo.my_function(SURVEY.NAME, SURVEY.NAME2)"); + assertThat(expr.toString()).containsIgnoringCase("dbo.my_function(SURVEY.NAME, SURVEY.NAME2)"); } @Test @@ -49,20 +49,21 @@ public void threePartName() { ConstantImpl.create("PARAM"), survey.name, ConstantImpl.create("")); - assertThat(expr.toString()).isEqualTo("external_db.dbo.my_function(PARAM, SURVEY.NAME, )"); + assertThat(expr.toString()) + .containsIgnoringCase("external_db.dbo.my_function(PARAM, SURVEY.NAME, )"); } @Test public void numberFunction() { NumberTemplate expr = SQLExpressions.numberFunction(Integer.class, "dbo.calculate", survey.id); - assertThat(expr.toString()).isEqualTo("dbo.calculate(SURVEY.ID)"); + assertThat(expr.toString()).containsIgnoringCase("dbo.calculate(SURVEY.ID)"); } @Test public void stringFunction() { StringTemplate expr = SQLExpressions.stringFunction("my_encrypt", survey.name); - assertThat(expr.toString()).isEqualTo("my_encrypt(SURVEY.NAME)"); + assertThat(expr.toString()).containsIgnoringCase("my_encrypt(SURVEY.NAME)"); } @Test @@ -107,7 +108,8 @@ public void functionInInsertValues() { public void fourPartName() { StringTemplate expr = SQLExpressions.stringFunction("linked_server.external_db.dbo.my_function", survey.name); - assertThat(expr.toString()).isEqualTo("linked_server.external_db.dbo.my_function(SURVEY.NAME)"); + assertThat(expr.toString()) + .containsIgnoringCase("linked_server.external_db.dbo.my_function(SURVEY.NAME)"); } @Test @@ -119,6 +121,6 @@ public void multipleArguments() { survey.name, survey.id, ConstantImpl.create("B")); - assertThat(expr.toString()).isEqualTo("schema.func(A, SURVEY.NAME, SURVEY.ID, B)"); + assertThat(expr.toString()).containsIgnoringCase("schema.func(A, SURVEY.NAME, SURVEY.ID, B)"); } }