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:
Sam Brannen
2023-09-04 12:53:47 +02:00
parent d39034754f
commit 7dc0653f38
9 changed files with 326 additions and 81 deletions

View File

@@ -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);
}
}
}

View File

@@ -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(?, ?)");
}
}

View File

@@ -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
);