Test quoted identifiers in schema in SimpleJdbcInsert

This commit introduces additional tests for "quoted identifier" support
in SimpleJdbcInsert when the schema itself is defined using quoted
identifiers -- for example, to use keywords as column names.

See gh-31208
This commit is contained in:
Sam Brannen 2023-10-24 14:36:04 +02:00
parent 2864c12887
commit 54839a7126
4 changed files with 268 additions and 97 deletions

View File

@ -16,6 +16,7 @@
package org.springframework.jdbc.core.simple; package org.springframework.jdbc.core.simple;
import java.sql.Types;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
@ -24,12 +25,16 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassRelativeResourceLoader; import org.springframework.core.io.ClassRelativeResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.core.SqlTypeValue;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jdbc.datasource.init.DatabasePopulator; import org.springframework.jdbc.datasource.init.DatabasePopulator;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/** /**
* Integration tests for {@link SimpleJdbcInsert} using an embedded H2 database. * Integration tests for {@link SimpleJdbcInsert} using an embedded H2 database.
@ -41,114 +46,249 @@ import static org.assertj.core.api.Assertions.assertThat;
class SimpleJdbcInsertIntegrationTests { class SimpleJdbcInsertIntegrationTests {
@Nested @Nested
class DefaultSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests { class DefaultSchemaTests {
@Test @Nested
void retrieveColumnNamesFromMetadata() throws Exception { class UnquotedIdentifiersInSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests {
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withTableName("users")
.usingGeneratedKeyColumns("id");
insert.compile(); @Test
// NOTE: column names looked up via metadata in H2/HSQL will be UPPERCASE! void retrieveColumnNamesFromMetadata() throws Exception {
assertThat(insert.getInsertString()).isEqualTo("INSERT INTO users (FIRST_NAME, LAST_NAME) VALUES(?, ?)"); SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withTableName("users")
.usingGeneratedKeyColumns("id");
insertJaneSmith(insert); insert.compile();
assertThat(insert.getInsertTypes()).containsExactly(Types.VARCHAR, Types.VARCHAR);
// 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)
.withoutTableColumnMetaDataAccess()
.withTableName("users")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
insert.compile();
assertThat(insert.getInsertString()).isEqualTo("INSERT INTO users (first_name, last_name) VALUES(?, ?)");
insertJaneSmith(insert);
}
@Test // gh-24013
void usingColumnsAndQuotedIdentifiers() throws Exception {
// NOTE: unquoted identifiers in H2/HSQL must be converted to UPPERCASE
// since that's how they are stored in the DB metadata.
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withoutTableColumnMetaDataAccess()
.withTableName("USERS")
.usingColumns("FIRST_NAME", "LAST_NAME")
.usingGeneratedKeyColumns("id")
.usingQuotedIdentifiers();
insert.compile();
assertThat(insert.getInsertString()).isEqualToIgnoringNewLines("""
INSERT INTO "USERS" ("FIRST_NAME", "LAST_NAME") VALUES(?, ?)
""");
insertJaneSmith(insert);
}
@Override
protected String getSchemaScript() {
return "users-schema.sql";
}
@Override
protected String getDataScript() {
return "users-data.sql";
}
@Override
protected String getTableName() {
return "users";
}
} }
@Test @Nested
void usingColumns() { class QuotedIdentifiersInSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests {
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withoutTableColumnMetaDataAccess()
.withTableName("users")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
insert.compile(); @Test
assertThat(insert.getInsertString()).isEqualTo("INSERT INTO users (first_name, last_name) VALUES(?, ?)"); void retrieveColumnNamesFromMetadata() throws Exception {
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withTableName("Order")
.usingGeneratedKeyColumns("id");
insertJaneSmith(insert); insert.compile();
// Since we are not quoting identifiers, the column names lookup for the "Order"
// table fails to find anything, and insert types are not populated.
assertThat(insert.getInsertTypes()).isEmpty();
// Consequently, any subsequent attempt to execute the INSERT statement should fail.
assertThatExceptionOfType(BadSqlGrammarException.class)
.isThrownBy(() -> insert.executeAndReturnKey(Map.of("from", "start", "date", "1999")));
}
@Test // gh-24013
void usingColumnsAndQuotedIdentifiers() throws Exception {
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withoutTableColumnMetaDataAccess()
.withTableName("Order")
.usingColumns("from", "Date")
.usingGeneratedKeyColumns("id")
.usingQuotedIdentifiers();
insert.compile();
assertThat(insert.getInsertString()).isEqualToIgnoringNewLines("""
INSERT INTO "Order" ("from", "Date") VALUES(?, ?)
""");
insertOrderEntry(insert);
}
@Override
protected ResourceLoader getResourceLoader() {
return new ClassRelativeResourceLoader(getClass());
}
@Override
protected String getSchemaScript() {
return "order-schema.sql";
}
@Override
protected String getDataScript() {
return "order-data.sql";
}
@Override
protected String getTableName() {
return "\"Order\"";
}
} }
@Test // gh-24013
void usingColumnsAndQuotedIdentifiers() throws Exception {
// NOTE: unquoted identifiers in H2/HSQL must be converted to UPPERCASE
// since that's how they are stored in the DB metadata.
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withoutTableColumnMetaDataAccess()
.withTableName("USERS")
.usingColumns("FIRST_NAME", "LAST_NAME")
.usingGeneratedKeyColumns("id")
.usingQuotedIdentifiers();
insert.compile();
assertThat(insert.getInsertString()).isEqualToIgnoringNewLines("""
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 @Nested
class CustomSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests { class CustomSchemaTests {
@Test @Nested
void usingColumnsWithSchemaName() { class UnquotedIdentifiersInSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests {
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withoutTableColumnMetaDataAccess()
.withSchemaName("my_schema")
.withTableName("users")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
insert.compile(); @Test
assertThat(insert.getInsertString()).isEqualTo("INSERT INTO my_schema.users (first_name, last_name) VALUES(?, ?)"); void usingColumnsWithSchemaName() {
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withoutTableColumnMetaDataAccess()
.withSchemaName("my_schema")
.withTableName("users")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
insertJaneSmith(insert); 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 {
// NOTE: unquoted identifiers in H2/HSQL must be converted to UPPERCASE
// since that's how they are stored in the DB metadata.
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withoutTableColumnMetaDataAccess()
.withSchemaName("MY_SCHEMA")
.withTableName("USERS")
.usingColumns("FIRST_NAME", "LAST_NAME")
.usingGeneratedKeyColumns("id")
.usingQuotedIdentifiers();
insert.compile();
assertThat(insert.getInsertString()).isEqualToIgnoringNewLines("""
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 getDataScript() {
return "users-data.sql";
}
@Override
protected String getTableName() {
return "my_schema.users";
}
} }
@Test // gh-24013 @Nested
void usingColumnsAndQuotedIdentifiersWithSchemaName() throws Exception { class QuotedIdentifiersInSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests {
// NOTE: unquoted identifiers in H2/HSQL must be converted to UPPERCASE
// since that's how they are stored in the DB metadata.
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withoutTableColumnMetaDataAccess()
.withSchemaName("MY_SCHEMA")
.withTableName("USERS")
.usingColumns("FIRST_NAME", "LAST_NAME")
.usingGeneratedKeyColumns("id")
.usingQuotedIdentifiers();
insert.compile(); @Test
assertThat(insert.getInsertString()).isEqualToIgnoringNewLines(""" void usingColumnsWithSchemaName() {
INSERT INTO "MY_SCHEMA"."USERS" ("FIRST_NAME", "LAST_NAME") VALUES(?, ?) SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
"""); .withoutTableColumnMetaDataAccess()
.withSchemaName("My_Schema")
.withTableName("Order")
.usingColumns("from", "Date")
.usingGeneratedKeyColumns("id");
insertJaneSmith(insert); insert.compile();
// Since we are not quoting identifiers, the column names lookup for the
// My_Schema.Order table results in unknown insert types.
assertThat(insert.getInsertTypes()).containsExactly(SqlTypeValue.TYPE_UNKNOWN, SqlTypeValue.TYPE_UNKNOWN);
// Consequently, any subsequent attempt to execute the INSERT statement should fail.
assertThatExceptionOfType(BadSqlGrammarException.class)
.isThrownBy(() -> insert.executeAndReturnKey(Map.of("from", "start", "date", "1999")));
}
@Test // gh-24013
void usingColumnsAndQuotedIdentifiersWithSchemaName() throws Exception {
SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase)
.withoutTableColumnMetaDataAccess()
.withSchemaName("My_Schema")
.withTableName("Order")
.usingColumns("from", "Date")
.usingGeneratedKeyColumns("id")
.usingQuotedIdentifiers();
insert.compile();
assertThat(insert.getInsertString()).isEqualToIgnoringNewLines("""
INSERT INTO "My_Schema"."Order" ("from", "Date") VALUES(?, ?)
""");
insertOrderEntry(insert);
}
@Override
protected ResourceLoader getResourceLoader() {
return new ClassRelativeResourceLoader(getClass());
}
@Override
protected String getSchemaScript() {
return "order-schema-with-custom-schema.sql";
}
@Override
protected String getDataScript() {
return "order-data.sql";
}
@Override
protected String getTableName() {
return "\"My_Schema\".\"Order\"";
}
} }
@Override
protected String getSchemaScript() {
return "users-schema-with-custom-schema.sql";
}
@Override
protected String getUsersTableName() {
return "my_schema.users";
}
} }
private abstract static class AbstractSimpleJdbcInsertIntegrationTests { private abstract static class AbstractSimpleJdbcInsertIntegrationTests {
@ -157,13 +297,13 @@ class SimpleJdbcInsertIntegrationTests {
@BeforeEach @BeforeEach
void createDatabase() { void createDatabase() {
this.embeddedDatabase = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(DatabasePopulator.class)) this.embeddedDatabase = new EmbeddedDatabaseBuilder(getResourceLoader())
.setType(EmbeddedDatabaseType.H2) .setType(EmbeddedDatabaseType.H2)
.addScript(getSchemaScript()) .addScript(getSchemaScript())
.addScript("users-data.sql") .addScript(getDataScript())
.build(); .build();
assertNumUsers(1); assertNumRows(1);
} }
@AfterEach @AfterEach
@ -171,21 +311,33 @@ class SimpleJdbcInsertIntegrationTests {
this.embeddedDatabase.shutdown(); this.embeddedDatabase.shutdown();
} }
protected void assertNumUsers(long count) { protected ResourceLoader getResourceLoader() {
return new ClassRelativeResourceLoader(DatabasePopulator.class);
}
protected void assertNumRows(long count) {
JdbcClient jdbcClient = JdbcClient.create(this.embeddedDatabase); JdbcClient jdbcClient = JdbcClient.create(this.embeddedDatabase);
long numUsers = jdbcClient.sql("select count(*) from " + getUsersTableName()).query(Long.class).single(); long numRows = jdbcClient.sql("select count(*) from " + getTableName()).query(Long.class).single();
assertThat(numUsers).isEqualTo(count); assertThat(numRows).isEqualTo(count);
} }
protected void insertJaneSmith(SimpleJdbcInsert insert) { protected void insertJaneSmith(SimpleJdbcInsert insert) {
Number id = insert.executeAndReturnKey(Map.of("first_name", "Jane", "last_name", "Smith")); Number id = insert.executeAndReturnKey(Map.of("first_name", "Jane", "last_name", "Smith"));
assertThat(id.intValue()).isEqualTo(2); assertThat(id.intValue()).isEqualTo(2);
assertNumUsers(2); assertNumRows(2);
}
protected void insertOrderEntry(SimpleJdbcInsert insert) {
Number id = insert.executeAndReturnKey(Map.of("from", "start", "date", "1999"));
assertThat(id.intValue()).isEqualTo(2);
assertNumRows(2);
} }
protected abstract String getSchemaScript(); protected abstract String getSchemaScript();
protected abstract String getUsersTableName(); protected abstract String getDataScript();
protected abstract String getTableName();
} }

View File

@ -0,0 +1 @@
INSERT INTO "Order" ("from", "Date") values('start', '1999');

View File

@ -0,0 +1,11 @@
CREATE SCHEMA IF NOT EXISTS "My_Schema";
SET SCHEMA "My_Schema";
DROP TABLE "Order" IF EXISTS;
CREATE TABLE "Order" (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
"from" VARCHAR(50) NOT NULL,
"Date" VARCHAR(50) NOT NULL
);

View File

@ -0,0 +1,7 @@
DROP TABLE "Order" IF EXISTS;
CREATE TABLE "Order" (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
"from" VARCHAR(50) NOT NULL,
"Date" VARCHAR(50) NOT NULL
);