diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java index ead4359d278..37eac28addd 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,8 +77,8 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider { /** Collection of TableParameterMetaData objects. */ private final List tableParameterMetaData = new ArrayList<>(); - /** the string used to quote SQL identifiers. */ - private String identifierQuoteString = ""; + /** The string used to quote SQL identifiers. */ + private String identifierQuoteString = " "; /** * Constructor used to initialize with provided database meta-data. @@ -305,7 +305,9 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider { } /** - * Provide access to identifier quote string. + * Provide access to the identifier quote string. + * @since 6.1 + * @see java.sql.DatabaseMetaData#getIdentifierQuoteString() */ @Override public String getIdentifierQuoteString() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java index 04b8f89a91a..6b975999c73 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.jdbc.support.JdbcUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; /** * Class to manage context meta-data used for the configuration @@ -72,16 +73,16 @@ public class TableMetaDataContext { // Should we override default for including synonyms for meta-data lookups private boolean overrideIncludeSynonymsDefault = false; + // Are we using generated key columns? + private boolean generatedKeyColumnsUsed = false; + + // Are we quoting identifiers? + private boolean quoteIdentifiers = false; + // The provider of table meta-data @Nullable private TableMetaDataProvider metaDataProvider; - // Are we using generated key columns - private boolean generatedKeyColumnsUsed = false; - - // Are we using escaping for SQL identifiers - private boolean usingEscaping = false; - /** * Set the name of the table for this context. @@ -142,7 +143,6 @@ public class TableMetaDataContext { return this.accessTableColumnMetaData; } - /** * Specify whether we should override default for accessing synonyms. */ @@ -157,6 +157,28 @@ public class TableMetaDataContext { return this.overrideIncludeSynonymsDefault; } + /** + * Specify whether we are quoting SQL identifiers. + *

Defaults to {@code false}. If set to {@code true}, the identifier + * quote string for the underlying database will be used to quote SQL + * identifiers in generated SQL statements. + * @param quoteIdentifiers whether identifiers should be quoted + * @since 6.1 + * @see java.sql.DatabaseMetaData#getIdentifierQuoteString() + */ + public void setQuoteIdentifiers(boolean quoteIdentifiers) { + this.quoteIdentifiers = quoteIdentifiers; + } + + /** + * Are we quoting identifiers? + * @since 6.1 + * @see #setQuoteIdentifiers(boolean) + */ + public boolean isQuoteIdentifiers() { + return this.quoteIdentifiers; + } + /** * Get a List of the table column names. */ @@ -269,7 +291,6 @@ public class TableMetaDataContext { return values; } - /** * Build the insert string based on configuration and meta-data information. * @return the insert string to be used @@ -279,24 +300,37 @@ public class TableMetaDataContext { for (String key : generatedKeyNames) { keys.add(key.toUpperCase()); } - String identifierQuoteString = ""; - if (this.metaDataProvider != null && this.usingEscaping) { - identifierQuoteString = this.metaDataProvider.getIdentifierQuoteString(); - } + + String identifierQuoteString = (isQuoteIdentifiers() ? + obtainMetaDataProvider().getIdentifierQuoteString() : null); + boolean quoting = StringUtils.hasText(identifierQuoteString); + StringBuilder insertStatement = new StringBuilder(); insertStatement.append("INSERT INTO "); - if (getSchemaName() != null) { - insertStatement.append(identifierQuoteString); - insertStatement.append(getSchemaName()); + + String schemaName = getSchemaName(); + if (schemaName != null) { + if (quoting) { + insertStatement.append(identifierQuoteString); + insertStatement.append(this.metaDataProvider.schemaNameToUse(schemaName)); + insertStatement.append(identifierQuoteString); + } + else { + insertStatement.append(schemaName); + } insertStatement.append('.'); - insertStatement.append(getTableName()); + } + + String tableName = getTableName(); + if (quoting) { + insertStatement.append(identifierQuoteString); + insertStatement.append(this.metaDataProvider.tableNameToUse(tableName)); insertStatement.append(identifierQuoteString); } else { - insertStatement.append(identifierQuoteString); - insertStatement.append(getTableName()); - insertStatement.append(identifierQuoteString); + insertStatement.append(tableName); } + insertStatement.append(" ("); int columnCount = 0; for (String columnName : getTableColumns()) { @@ -305,9 +339,14 @@ public class TableMetaDataContext { if (columnCount > 1) { insertStatement.append(", "); } - insertStatement.append(identifierQuoteString); - insertStatement.append(columnName); - insertStatement.append(identifierQuoteString); + if (quoting) { + insertStatement.append(identifierQuoteString); + insertStatement.append(this.metaDataProvider.columnNameToUse(columnName)); + insertStatement.append(identifierQuoteString); + } + else { + insertStatement.append(columnName); + } } } insertStatement.append(") VALUES("); @@ -315,11 +354,11 @@ public class TableMetaDataContext { if (this.generatedKeyColumnsUsed) { if (logger.isDebugEnabled()) { logger.debug("Unable to locate non-key columns for table '" + - getTableName() + "' so an empty insert statement is generated"); + tableName + "' so an empty insert statement is generated"); } } else { - String message = "Unable to locate columns for table '" + getTableName() + String message = "Unable to locate columns for table '" + tableName + "' so an insert statement can't be generated."; if (isAccessTableColumnMetaData()) { message += " Consider specifying explicit column names -- for example, via SimpleJdbcInsert#usingColumns()."; @@ -397,11 +436,4 @@ public class TableMetaDataContext { return obtainMetaDataProvider().isGeneratedKeysColumnNameArraySupported(); } - public boolean isUsingEscaping() { - return this.usingEscaping; - } - - public void setUsingEscaping(boolean usingEscaping) { - this.usingEscaping = usingEscaping; - } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java index d776de207b9..d96278217a6 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -140,9 +140,12 @@ public interface TableMetaDataProvider { List getTableParameterMetaData(); /** - * Retrieves the string used to quote SQL identifiers. This method returns a space " " if identifier quoting is not supported. - * {@link DatabaseMetaData#getIdentifierQuoteString()} - * @return database identifier quote string. + * Get the string used to quote SQL identifiers. + *

This method returns a space ({@code " "}) if identifier quoting is not + * supported. + * @return database identifier quote string + * @since 6.1 + * @see DatabaseMetaData#getIdentifierQuoteString() */ String getIdentifierQuoteString(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java index 454a38ab657..0b45a7af6eb 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java @@ -237,17 +237,25 @@ public abstract class AbstractJdbcInsert { } /** - * Set using using escaping. + * Specify whether SQL identifiers should be quoted. + *

Defaults to {@code false}. If set to {@code true}, the identifier + * quote string for the underlying database will be used to quote SQL + * identifiers in generated SQL statements. + * @param quoteIdentifiers whether identifiers should be quoted + * @since 6.1 + * @see java.sql.DatabaseMetaData#getIdentifierQuoteString() */ - public void setUsingEscaping(boolean usingEscaping) { - this.tableMetaDataContext.setUsingEscaping(usingEscaping); + public void setQuoteIdentifiers(boolean quoteIdentifiers) { + this.tableMetaDataContext.setQuoteIdentifiers(quoteIdentifiers); } /** - * Get using escaping. + * Get the {@code quoteIdentifiers} flag. + * @since 6.1 + * @see #setQuoteIdentifiers(boolean) */ - public boolean isUsingEscaping() { - return this.tableMetaDataContext.isUsingEscaping(); + public boolean isQuoteIdentifiers() { + return this.tableMetaDataContext.isQuoteIdentifiers(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java index b940cc47edf..0284639bbaa 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java @@ -102,6 +102,12 @@ public class SimpleJdbcInsert extends AbstractJdbcInsert implements SimpleJdbcIn return this; } + @Override + public SimpleJdbcInsert usingQuotedIdentifiers() { + setQuoteIdentifiers(true); + return this; + } + @Override public SimpleJdbcInsertOperations withoutTableColumnMetaDataAccess() { setAccessTableColumnMetaData(false); @@ -114,12 +120,6 @@ public class SimpleJdbcInsert extends AbstractJdbcInsert implements SimpleJdbcIn return this; } - @Override - public SimpleJdbcInsert usingEscaping(boolean usingEscaping) { - setUsingEscaping(usingEscaping); - return this; - } - @Override public int execute(Map args) { return doExecute(args); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java index 00364faf090..b2e611b704f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,6 +68,17 @@ public interface SimpleJdbcInsertOperations { */ SimpleJdbcInsertOperations usingGeneratedKeyColumns(String... columnNames); + /** + * Specify that SQL identifiers should be quoted. + *

If this method is invoked, the identifier quote string for the underlying + * database will be used to quote SQL identifiers in generated SQL statements. + * In this context, SQL identifiers refer to schema, table, and column names. + * @return this {@code SimpleJdbcInsert} (for method chaining) + * @since 6.1 + * @see java.sql.DatabaseMetaData#getIdentifierQuoteString() + */ + SimpleJdbcInsertOperations usingQuotedIdentifiers(); + /** * Turn off any processing of column meta-data information obtained via JDBC. * @return this {@code SimpleJdbcInsert} (for method chaining) @@ -82,14 +93,6 @@ public interface SimpleJdbcInsertOperations { */ SimpleJdbcInsertOperations includeSynonymsForTableColumnMetaData(); - /** - * Specify should sql identifiers be quoted. - * @param usingEscaping should sql identifiers be quoted - * @return the instance of this SimpleJdbcInsert - */ - SimpleJdbcInsertOperations usingEscaping(boolean usingEscaping); - - /** * Execute the insert using the values passed in. * @param args a Map containing column names and corresponding value diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertIntegrationTests.java new file mode 100644 index 00000000000..c168f925b02 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertIntegrationTests.java @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.jdbc.core.simple; + +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassRelativeResourceLoader; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.jdbc.datasource.init.DatabasePopulator; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link SimpleJdbcInsert} using an embedded H2 database. + * + * @author Sam Brannen + * @since 6.1 + */ +class SimpleJdbcInsertIntegrationTests { + + @Nested + class DefaultSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests { + + @Test + void retrieveColumnNamesFromMetadata() throws Exception { + SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase) + .withTableName("users") + .usingGeneratedKeyColumns("id"); + + insert.compile(); + // NOTE: column names looked up via metadata in H2/HSQL will be UPPERCASE! + assertThat(insert.getInsertString()).isEqualTo("INSERT INTO users (FIRST_NAME, LAST_NAME) VALUES(?, ?)"); + + insertJaneSmith(insert); + } + + @Test + void usingColumns() { + SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase) + .withTableName("users") + .usingColumns("first_name", "last_name"); + + insert.compile(); + assertThat(insert.getInsertString()).isEqualTo("INSERT INTO users (first_name, last_name) VALUES(?, ?)"); + + insertJaneSmith(insert); + } + + @Test // gh-24013 + void usingColumnsAndQuotedIdentifiers() throws Exception { + SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase) + .withTableName("users") + .usingColumns("first_name", "last_name") + .usingQuotedIdentifiers(); + + insert.compile(); + // NOTE: quoted identifiers in H2/HSQL will be UPPERCASE! + assertThat(insert.getInsertString()).isEqualTo("INSERT INTO \"USERS\" (\"FIRST_NAME\", \"LAST_NAME\") VALUES(?, ?)"); + + insertJaneSmith(insert); + } + + @Override + protected String getSchemaScript() { + return "users-schema.sql"; + } + + @Override + protected String getUsersTableName() { + return "users"; + } + + } + + @Nested + class CustomSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests { + + @Test + void usingColumnsWithSchemaName() { + SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase) + .withSchemaName("my_schema") + .withTableName("users") + .usingColumns("first_name", "last_name"); + + insert.compile(); + assertThat(insert.getInsertString()).isEqualTo("INSERT INTO my_schema.users (first_name, last_name) VALUES(?, ?)"); + + insertJaneSmith(insert); + } + + @Test // gh-24013 + void usingColumnsAndQuotedIdentifiersWithSchemaName() throws Exception { + SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase) + .withSchemaName("my_schema") + .withTableName("users") + .usingColumns("first_name", "last_name") + .usingQuotedIdentifiers(); + + insert.compile(); + // NOTE: quoted identifiers in H2/HSQL will be UPPERCASE! + assertThat(insert.getInsertString()).isEqualTo("INSERT INTO \"MY_SCHEMA\".\"USERS\" (\"FIRST_NAME\", \"LAST_NAME\") VALUES(?, ?)"); + + insertJaneSmith(insert); + } + + @Override + protected String getSchemaScript() { + return "users-schema-with-custom-schema.sql"; + } + + @Override + protected String getUsersTableName() { + return "my_schema.users"; + } + + } + + private static abstract class AbstractSimpleJdbcInsertIntegrationTests { + + protected EmbeddedDatabase embeddedDatabase; + + + protected abstract String getSchemaScript(); + + protected abstract String getUsersTableName(); + + protected EmbeddedDatabase createEmbeddedDatabase() { + return new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(DatabasePopulator.class)) + .setType(EmbeddedDatabaseType.H2) + .addScript(getSchemaScript()) + .addScript("users-data.sql") + .build(); + } + + + @BeforeEach + void checkDatabaseSetup() { + this.embeddedDatabase = createEmbeddedDatabase(); + assertNumUsers(1); + } + + @AfterEach + void shutdown() { + this.embeddedDatabase.shutdown(); + } + + protected void assertNumUsers(long count) { + JdbcClient jdbcClient = JdbcClient.create(this.embeddedDatabase); + long numUsers = jdbcClient.sql("select count(*) from " + getUsersTableName()).query().singleValue(); + assertThat(numUsers).isEqualTo(count); + } + + protected void insertJaneSmith(SimpleJdbcInsert insert) { + insert.execute(Map.of("first_name", "Jane", "last_name", "Smith")); + assertNumUsers(2); + } + + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java index 12d1c70e6e5..ae35ec172e1 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java @@ -139,36 +139,41 @@ class SimpleJdbcInsertTests { } @Test - public void testSimpleJdbcInsert() { - SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(dataSource).withTableName("T").usingColumns("F", "S"); - jdbcInsert.compile(); - String expected = "INSERT INTO T (F, S) VALUES(?, ?)"; - String actual = jdbcInsert.getInsertString(); - assertThat(actual).isEqualTo(expected); + void usingColumns() { + SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource) + .withTableName("my_table") + .usingColumns("col1", "col2"); + + insert.compile(); + + assertThat(insert.getInsertString()).isEqualTo("INSERT INTO my_table (col1, col2) VALUES(?, ?)"); } - @Test - public void testSimpleJdbcInsertWithEscapingWithSchemaName() throws Exception { - SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(dataSource).withSchemaName("S").withTableName("T").usingColumns("F", "S").usingEscaping(true); + @Test // gh-24013 + void usingColumnsAndQuotedIdentifiers() throws Exception { + SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource) + .withTableName("my_table") + .usingColumns("col1", "col2") + .usingQuotedIdentifiers(); given(databaseMetaData.getIdentifierQuoteString()).willReturn("`"); - jdbcInsert.compile(); - String expected = "INSERT INTO `S.T` (`F`, `S`) VALUES(?, ?)"; - String actual = jdbcInsert.getInsertString(); - assertThat(actual).isEqualTo(expected); + insert.compile(); + assertThat(insert.getInsertString()).isEqualTo("INSERT INTO `my_table` (`col1`, `col2`) VALUES(?, ?)"); } - @Test - public void testSimpleJdbcInsertWithEscapingWithoutSchemaName() throws Exception { - SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(dataSource).withTableName("T").usingColumns("F", "S").usingEscaping(true); + @Test // gh-24013 + void usingColumnsAndQuotedIdentifiersWithSchemaName() throws Exception { + SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource) + .withSchemaName("my_schema") + .withTableName("my_table") + .usingColumns("col1", "col2") + .usingQuotedIdentifiers(); given(databaseMetaData.getIdentifierQuoteString()).willReturn("`"); - jdbcInsert.compile(); - String expected = "INSERT INTO `T` (`F`, `S`) VALUES(?, ?)"; - String actual = jdbcInsert.getInsertString(); - assertThat(actual).isEqualTo(expected); + insert.compile(); + assertThat(insert.getInsertString()).isEqualTo("INSERT INTO `my_schema`.`my_table` (`col1`, `col2`) VALUES(?, ?)"); } } diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/users-schema-with-custom-schema.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/users-schema-with-custom-schema.sql new file mode 100644 index 00000000000..6da1c297820 --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/users-schema-with-custom-schema.sql @@ -0,0 +1,11 @@ +CREATE SCHEMA IF NOT EXISTS my_schema; + +SET SCHEMA my_schema; + +DROP TABLE users IF EXISTS; + +CREATE TABLE users ( + id INTEGER GENERATED BY DEFAULT AS IDENTITY, + first_name VARCHAR(50) NOT NULL, + last_name VARCHAR(50) NOT NULL +);