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:
parent
2864c12887
commit
54839a7126
|
|
@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
INSERT INTO "Order" ("from", "Date") values('start', '1999');
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
Loading…
Reference in New Issue