Skip to content

Commit 19ff4ce

Browse files
committed
Use SQLTemplates.quoteIdentifier for native SQL identifiers
Apply dialect-specific identifier quoting to table and column names in JpaInsertNativeHelper, consistent with NativeSQLSerializer. This avoids issues when identifiers contain SQL reserved words, spaces, or dialect-specific characters. - buildNativeInsertSQL now accepts SQLTemplates parameter - Schema-qualified table names quote schema and table parts separately - JPAInsertClause and HibernateInsertClause pass SQLTemplates.DEFAULT - Deprecate the overload without SQLTemplates - Add test covering always-quote templates
1 parent 94f4d1a commit 19ff4ce

File tree

4 files changed

+75
-14
lines changed

4 files changed

+75
-14
lines changed

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JpaInsertNativeHelper.java

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.querydsl.core.types.ParamNotSetException;
1919
import com.querydsl.core.types.Path;
2020
import com.querydsl.core.types.dsl.Param;
21+
import com.querydsl.sql.SQLTemplates;
2122
import jakarta.persistence.Column;
2223
import jakarta.persistence.Table;
2324
import java.sql.PreparedStatement;
@@ -41,10 +42,10 @@ public final class JpaInsertNativeHelper {
4142
private JpaInsertNativeHelper() {}
4243

4344
/**
44-
* Resolve the SQL table name for an entity class.
45+
* Resolve the SQL table name for an entity class (unquoted).
4546
*
4647
* @param entityClass the JPA entity class
47-
* @return the SQL table name
48+
* @return the raw SQL table name (schema.table if schema is set)
4849
*/
4950
public static String resolveTableName(Class<?> entityClass) {
5051
if (entityClass.isAnnotationPresent(Table.class)) {
@@ -62,11 +63,11 @@ public static String resolveTableName(Class<?> entityClass) {
6263
}
6364

6465
/**
65-
* Resolve the SQL column name for a path. Reads {@code @Column} annotation if present, otherwise
66-
* falls back to the path metadata name.
66+
* Resolve the SQL column name for a path (unquoted). Reads {@code @Column} annotation if present,
67+
* otherwise falls back to the path metadata name.
6768
*
6869
* @param path the query path
69-
* @return the SQL column name
70+
* @return the raw SQL column name
7071
*/
7172
public static String resolveColumnName(Path<?> path) {
7273
if (path.getAnnotatedElement() != null
@@ -80,23 +81,45 @@ public static String resolveColumnName(Path<?> path) {
8081
}
8182

8283
/**
83-
* Build a native SQL INSERT statement from entity metadata and column paths.
84+
* Quote a table name using the given {@link SQLTemplates}. Handles schema-qualified names by
85+
* quoting schema and table parts separately.
8486
*
87+
* @param templates the SQL templates providing dialect-specific quoting rules
88+
* @param tableName the raw table name (may be schema-qualified as "schema.table")
89+
* @return the properly quoted table name
90+
*/
91+
private static String quoteTableName(SQLTemplates templates, String tableName) {
92+
var dotIndex = tableName.indexOf('.');
93+
if (dotIndex > 0) {
94+
var schema = tableName.substring(0, dotIndex);
95+
var table = tableName.substring(dotIndex + 1);
96+
return templates.quoteIdentifier(schema) + "." + templates.quoteIdentifier(table, true);
97+
}
98+
return templates.quoteIdentifier(tableName);
99+
}
100+
101+
/**
102+
* Build a native SQL INSERT statement from entity metadata and column paths, with identifiers
103+
* properly quoted using the given {@link SQLTemplates}.
104+
*
105+
* @param templates the SQL templates providing dialect-specific quoting rules
85106
* @param entityClass the entity class (for table name resolution)
86107
* @param columns the columns to insert
87108
* @return the native SQL INSERT string with positional parameters
88109
*/
89-
public static String buildNativeInsertSQL(Class<?> entityClass, Collection<Path<?>> columns) {
90-
var tableName = resolveTableName(entityClass);
110+
public static String buildNativeInsertSQL(
111+
SQLTemplates templates, Class<?> entityClass, Collection<Path<?>> columns) {
91112
var sb = new StringBuilder();
92-
sb.append("INSERT INTO ").append(tableName).append(" (");
113+
sb.append("INSERT INTO ")
114+
.append(quoteTableName(templates, resolveTableName(entityClass)))
115+
.append(" (");
93116

94117
var first = true;
95118
for (Path<?> col : columns) {
96119
if (!first) {
97120
sb.append(", ");
98121
}
99-
sb.append(resolveColumnName(col));
122+
sb.append(templates.quoteIdentifier(resolveColumnName(col)));
100123
first = false;
101124
}
102125

@@ -114,6 +137,20 @@ public static String buildNativeInsertSQL(Class<?> entityClass, Collection<Path<
114137
return sb.toString();
115138
}
116139

140+
/**
141+
* Build a native SQL INSERT statement using {@link SQLTemplates#DEFAULT} for identifier quoting.
142+
*
143+
* @param entityClass the entity class (for table name resolution)
144+
* @param columns the columns to insert
145+
* @return the native SQL INSERT string with positional parameters
146+
* @deprecated prefer {@link #buildNativeInsertSQL(SQLTemplates, Class, Collection)} with explicit
147+
* templates so dialect-specific quoting is applied
148+
*/
149+
@Deprecated
150+
public static String buildNativeInsertSQL(Class<?> entityClass, Collection<Path<?>> columns) {
151+
return buildNativeInsertSQL(SQLTemplates.DEFAULT, entityClass, columns);
152+
}
153+
117154
/**
118155
* Resolve constant values from the serializer, unwrapping {@link Param} expressions.
119156
*

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/HibernateInsertClause.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.querydsl.jpa.JPQLSerializer;
2828
import com.querydsl.jpa.JPQLTemplates;
2929
import com.querydsl.jpa.JpaInsertNativeHelper;
30+
import com.querydsl.sql.SQLTemplates;
3031
import java.sql.SQLException;
3132
import java.util.ArrayList;
3233
import java.util.Arrays;
@@ -153,7 +154,9 @@ public <T> T executeWithKey(Class<T> type) {
153154
JpaInsertNativeHelper.resolveConstants(
154155
serializer.getConstants(), queryMixin.getMetadata().getParams());
155156

156-
var sql = JpaInsertNativeHelper.buildNativeInsertSQL(entityClass, effectiveColumns);
157+
var sql =
158+
JpaInsertNativeHelper.buildNativeInsertSQL(
159+
SQLTemplates.DEFAULT, entityClass, effectiveColumns);
157160

158161
return session.doReturningWork(
159162
connection -> {

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/JPAInsertClause.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.querydsl.jpa.JPQLSerializer;
2727
import com.querydsl.jpa.JPQLTemplates;
2828
import com.querydsl.jpa.JpaInsertNativeHelper;
29+
import com.querydsl.sql.SQLTemplates;
2930
import jakarta.persistence.EntityManager;
3031
import jakarta.persistence.LockModeType;
3132
import java.util.ArrayList;
@@ -139,7 +140,9 @@ public <T> T executeWithKey(Class<T> type) {
139140
JpaInsertNativeHelper.resolveConstants(
140141
serializer.getConstants(), queryMixin.getMetadata().getParams());
141142

142-
var sql = JpaInsertNativeHelper.buildNativeInsertSQL(entityClass, effectiveColumns);
143+
var sql =
144+
JpaInsertNativeHelper.buildNativeInsertSQL(
145+
SQLTemplates.DEFAULT, entityClass, effectiveColumns);
143146

144147
try {
145148
return entityManager

querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JpaInsertNativeHelperTest.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.querydsl.jpa.domain.QAuthor;
2222
import com.querydsl.jpa.domain.QGeneratedKeyEntity;
2323
import com.querydsl.jpa.domain.QNumeric;
24+
import com.querydsl.sql.SQLTemplates;
2425
import java.util.List;
2526
import org.junit.Test;
2627

@@ -64,16 +65,33 @@ public void resolveColumnName_without_column_annotation() {
6465
public void buildNativeInsertSQL() {
6566
var entity = QGeneratedKeyEntity.generatedKeyEntity;
6667
var sql =
67-
JpaInsertNativeHelper.buildNativeInsertSQL(GeneratedKeyEntity.class, List.of(entity.name));
68+
JpaInsertNativeHelper.buildNativeInsertSQL(
69+
SQLTemplates.DEFAULT, GeneratedKeyEntity.class, List.of(entity.name));
6870

71+
// SQLTemplates.DEFAULT uses double quotes only when required (reserved words or special chars)
6972
assertThat(sql).isEqualTo("INSERT INTO generated_key_entity (name_) VALUES (?)");
7073
}
7174

7275
@Test
7376
public void buildNativeInsertSQL_multiple_columns() {
7477
var numeric = QNumeric.numeric;
75-
var sql = JpaInsertNativeHelper.buildNativeInsertSQL(Numeric.class, List.of(numeric.value));
78+
var sql =
79+
JpaInsertNativeHelper.buildNativeInsertSQL(
80+
SQLTemplates.DEFAULT, Numeric.class, List.of(numeric.value));
7681

7782
assertThat(sql).isEqualTo("INSERT INTO numeric_ (value_) VALUES (?)");
7883
}
84+
85+
@Test
86+
public void buildNativeInsertSQL_quotes_reserved_words() {
87+
// Custom SQLTemplates with useQuotes=true always quotes identifiers
88+
var alwaysQuote = new SQLTemplates("\"", '\\', true) {};
89+
90+
var entity = QGeneratedKeyEntity.generatedKeyEntity;
91+
var sql =
92+
JpaInsertNativeHelper.buildNativeInsertSQL(
93+
alwaysQuote, GeneratedKeyEntity.class, List.of(entity.name));
94+
95+
assertThat(sql).isEqualTo("INSERT INTO \"generated_key_entity\" (\"name_\") VALUES (?)");
96+
}
7997
}

0 commit comments

Comments
 (0)