Revise quoted identifier support in SimpleJdbcInsert
Prior to this commit and the previous commit, SimpleJdbcInsert did not provide built-in support for "quoted identifiers". Consequently, if any column names conflicted with keywords or functions from the underlying database, you had to manually quote the column names when specifying them via `usingColumns(...)`, and there was unfortunately no way to quote schema and table names. The previous commit provided rudimentary support for quoted SQL identifiers (schema, table, and column names) by querying java.sql.DatabaseMetaData.getIdentifierQuoteString() to determine the quote string. It also introduced `usingEscaping(boolean)` in `SimpleJdbcInsertOperations` to enable the feature. However, it incorrectly quoted the schema and table names together, and it did not take into account the fact that a quoted identifier should respect the casing (uppercase vs. lowercase) of the underlying database's metadata. This commit revises quoted identifier support in `SimpleJdbcInsert` by: - renaming `usingEscaping(boolean)` to `usingQuotedIdentifiers()` - quoting schema and table names separately - respecting the casing (uppercase vs. lowercase) of the underlying database's metadata when quoting identifiers - introducing integration tests against an in-memory H2 database See gh-13874 Closes gh-24013
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(?, ?)");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
Reference in New Issue
Block a user