From 54839a7126645991db12a29cb008e3d75fbf64c6 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 24 Oct 2023 14:36:04 +0200 Subject: [PATCH] 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 --- .../SimpleJdbcInsertIntegrationTests.java | 366 +++++++++++++----- .../jdbc/core/simple/order-data.sql | 1 + .../order-schema-with-custom-schema.sql | 11 + .../jdbc/core/simple/order-schema.sql | 7 + 4 files changed, 278 insertions(+), 107 deletions(-) create mode 100644 spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-data.sql create mode 100644 spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-schema-with-custom-schema.sql create mode 100644 spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-schema.sql 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 index 52b8977a2d..bdb499126d 100644 --- 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 @@ -16,6 +16,7 @@ package org.springframework.jdbc.core.simple; +import java.sql.Types; import java.util.Map; import org.junit.jupiter.api.AfterEach; @@ -24,12 +25,16 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; 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.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.jdbc.datasource.init.DatabasePopulator; 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. @@ -41,114 +46,249 @@ import static org.assertj.core.api.Assertions.assertThat; 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) - .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); + class DefaultSchemaTests { + + @Nested + class UnquotedIdentifiersInSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests { + + @Test + void retrieveColumnNamesFromMetadata() throws Exception { + SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase) + .withTableName("users") + .usingGeneratedKeyColumns("id"); + + 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 // 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); + @Nested + class QuotedIdentifiersInSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests { + + @Test + void retrieveColumnNamesFromMetadata() throws Exception { + SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase) + .withTableName("Order") + .usingGeneratedKeyColumns("id"); + + 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\""; + } } - - @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) - .withoutTableColumnMetaDataAccess() - .withSchemaName("my_schema") - .withTableName("users") - .usingColumns("first_name", "last_name") - .usingGeneratedKeyColumns("id"); - - 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); + class CustomSchemaTests { + + @Nested + class UnquotedIdentifiersInSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests { + + @Test + void usingColumnsWithSchemaName() { + SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase) + .withoutTableColumnMetaDataAccess() + .withSchemaName("my_schema") + .withTableName("users") + .usingColumns("first_name", "last_name") + .usingGeneratedKeyColumns("id"); + + 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"; + } } - @Override - protected String getSchemaScript() { - return "users-schema-with-custom-schema.sql"; + @Nested + class QuotedIdentifiersInSchemaTests extends AbstractSimpleJdbcInsertIntegrationTests { + + @Test + void usingColumnsWithSchemaName() { + SimpleJdbcInsert insert = new SimpleJdbcInsert(embeddedDatabase) + .withoutTableColumnMetaDataAccess() + .withSchemaName("My_Schema") + .withTableName("Order") + .usingColumns("from", "Date") + .usingGeneratedKeyColumns("id"); + + 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 getUsersTableName() { - return "my_schema.users"; - } - } private abstract static class AbstractSimpleJdbcInsertIntegrationTests { @@ -157,13 +297,13 @@ class SimpleJdbcInsertIntegrationTests { @BeforeEach void createDatabase() { - this.embeddedDatabase = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(DatabasePopulator.class)) + this.embeddedDatabase = new EmbeddedDatabaseBuilder(getResourceLoader()) .setType(EmbeddedDatabaseType.H2) .addScript(getSchemaScript()) - .addScript("users-data.sql") + .addScript(getDataScript()) .build(); - assertNumUsers(1); + assertNumRows(1); } @AfterEach @@ -171,21 +311,33 @@ class SimpleJdbcInsertIntegrationTests { 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); - long numUsers = jdbcClient.sql("select count(*) from " + getUsersTableName()).query(Long.class).single(); - assertThat(numUsers).isEqualTo(count); + long numRows = jdbcClient.sql("select count(*) from " + getTableName()).query(Long.class).single(); + assertThat(numRows).isEqualTo(count); } protected void insertJaneSmith(SimpleJdbcInsert insert) { Number id = insert.executeAndReturnKey(Map.of("first_name", "Jane", "last_name", "Smith")); 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 getUsersTableName(); + protected abstract String getDataScript(); + + protected abstract String getTableName(); } diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-data.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-data.sql new file mode 100644 index 0000000000..02c8274d54 --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-data.sql @@ -0,0 +1 @@ +INSERT INTO "Order" ("from", "Date") values('start', '1999'); diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-schema-with-custom-schema.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-schema-with-custom-schema.sql new file mode 100644 index 0000000000..440ad908af --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-schema-with-custom-schema.sql @@ -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 +); diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-schema.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-schema.sql new file mode 100644 index 0000000000..3b325321f4 --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/core/simple/order-schema.sql @@ -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 +);