Skip to content

Commit

Permalink
Add duplicate column detection in EntityProvider (#1267)
Browse files Browse the repository at this point in the history
* Add duplicate column detection in EntityProvider

* Add DuplicateColumnHandler interface and its implementations to make DuplicateColumnException optional

* Replace wildcard imports with explicit imports

* current behavior set as the default implementation and DuplicateColumnException optional

* Change duplicate column handler to use lower case column name

* update test method name
  • Loading branch information
okurashoichi authored Jan 15, 2025
1 parent e2b6180 commit 03272c7
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.seasar.doma.jdbc.DuplicateColumnHandler;
import org.seasar.doma.jdbc.JdbcMappingVisitor;
import org.seasar.doma.jdbc.Naming;
import org.seasar.doma.jdbc.ResultMappingException;
Expand All @@ -49,6 +50,8 @@ public class EntityProvider<ENTITY> extends AbstractObjectProvider<ENTITY> {

protected final UnknownColumnHandler unknownColumnHandler;

protected final DuplicateColumnHandler duplicateColumnHandler;

protected Map<Integer, EntityPropertyType<ENTITY, ?>> indexMap;

public EntityProvider(EntityType<ENTITY> entityType, Query query, boolean resultMappingEnsured) {
Expand All @@ -58,6 +61,7 @@ public EntityProvider(EntityType<ENTITY> entityType, Query query, boolean result
this.resultMappingEnsured = resultMappingEnsured;
this.jdbcMappingVisitor = query.getConfig().getDialect().getJdbcMappingVisitor();
this.unknownColumnHandler = query.getConfig().getUnknownColumnHandler();
this.duplicateColumnHandler = query.getConfig().getDuplicateColumnHandler();
}

@Override
Expand Down Expand Up @@ -91,10 +95,14 @@ protected ENTITY build(ResultSet resultSet) throws SQLException {
HashMap<String, EntityPropertyType<ENTITY, ?>> columnNameMap = createColumnNameMap(entityType);
Set<EntityPropertyType<ENTITY, ?>> unmappedPropertySet =
resultMappingEnsured ? new HashSet<>(columnNameMap.values()) : new HashSet<>();
Set<String> seenColumnNames = new HashSet<>();
int count = resultSetMeta.getColumnCount();
for (int i = 1; i < count + 1; i++) {
String columnName = resultSetMeta.getColumnLabel(i);
String lowerCaseColumnName = columnName.toLowerCase();
if (!seenColumnNames.add(lowerCaseColumnName)) {
duplicateColumnHandler.handle(query, lowerCaseColumnName);
}
EntityPropertyType<ENTITY, ?> propertyType = columnNameMap.get(lowerCaseColumnName);
if (propertyType == null) {
if (ROWNUMBER_COLUMN_NAME.equals(lowerCaseColumnName)) {
Expand Down
9 changes: 9 additions & 0 deletions doma-core/src/main/java/org/seasar/doma/jdbc/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ default UnknownColumnHandler getUnknownColumnHandler() {
return ConfigSupport.defaultUnknownColumnHandler;
}

/**
* Returns the duplicate column handler.
*
* @return the duplicate column handler
*/
default DuplicateColumnHandler getDuplicateColumnHandler() {
return ConfigSupport.defaultDuplicateColumnHandler;
}

/**
* Returns the naming convention controller.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public final class ConfigSupport {
public static final UnknownColumnHandler defaultUnknownColumnHandler =
new UnknownColumnHandler() {};

public static final DuplicateColumnHandler defaultDuplicateColumnHandler =
new DuplicateColumnHandler() {};

public static final Naming defaultNaming = Naming.DEFAULT;

public static final MapKeyNaming defaultMapKeyNaming = new MapKeyNaming() {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright Doma 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.seasar.doma.jdbc;

import org.seasar.doma.message.Message;

/** Thrown to indicate that a column is duplicated in a result set. */
public class DuplicateColumnException extends JdbcException {

private static final long serialVersionUID = 1L;

protected final String columnName;

protected final String rawSql;

protected final String formattedSql;

protected final String sqlFilePath;

public DuplicateColumnException(
SqlLogType logType,
String columnName,
String rawSql,
String formattedSql,
String sqlFilePath) {
super(Message.DOMA2237, columnName, sqlFilePath, choiceSql(logType, rawSql, formattedSql));
this.columnName = columnName;
this.rawSql = rawSql;
this.formattedSql = formattedSql;
this.sqlFilePath = sqlFilePath;
}

/**
* Returns the unknown column name.
*
* @return the unknown column name
*/
public String getColumnName() {
return columnName;
}

/**
* Returns the raw SQL string.
*
* @return the raw SQL string
*/
public String getRawSql() {
return rawSql;
}

/**
* Returns the formatted SQL string
*
* @return the formatted SQL or {@code null} if this exception is thrown in the batch process
*/
public String getFormattedSql() {
return formattedSql;
}

/**
* Returns the SQL file path.
*
* @return the SQL file path or {@code null} if the SQL is auto generated
*/
public String getSqlFilePath() {
return sqlFilePath;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Doma 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.seasar.doma.jdbc;

import org.seasar.doma.jdbc.query.Query;

/** A handler for the column that is duplicated in a result set. */
public interface DuplicateColumnHandler {

/**
* Handles the duplicate column.
*
* @param query the query
* @param duplicateColumnName the name of the unknown column
*/
default void handle(Query query, String duplicateColumnName) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Doma 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.seasar.doma.jdbc;

import org.seasar.doma.jdbc.query.Query;

/** A handler for the column that is duplicated in a result set. */
public class ThrowingDuplicateColumnHandler implements DuplicateColumnHandler {

/**
* Handles the duplicate column.
*
* @param query the query
* @param duplicateColumnName the name of the duplicate column
* @throws DuplicateColumnException if this handler does not allow the duplicate column
*/
@Override
public void handle(Query query, String duplicateColumnName) {
Sql<?> sql = query.getSql();
throw new DuplicateColumnException(
query.getConfig().getExceptionSqlLogType(),
duplicateColumnName,
sql.getRawSql(),
sql.getFormattedSql(),
sql.getSqlFilePath());
}
}
3 changes: 3 additions & 0 deletions doma-core/src/main/java/org/seasar/doma/message/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ public enum Message implements MessageResource {
DOMA2235("The dialect \"{0}\" does not support auto-increment when inserting multiple rows."),

DOMA2236("The dialect \"{0}\" does not support multi-row insert statement."),
DOMA2237(
"Duplicate column name \"{0}\" found in ResultSetMetaData. Column names must be unique."
+ "\nPATH=[{1}].\nSQL=[{2}]"),

// expression
DOMA3001(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/
package org.seasar.doma.internal.jdbc.command;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.seasar.doma.internal.util.AssertionUtil.assertEquals;

import example.entity.Emp;
import example.entity._Emp;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.seasar.doma.FetchType;
Expand All @@ -31,10 +33,13 @@
import org.seasar.doma.internal.jdbc.mock.MockResultSetMetaData;
import org.seasar.doma.internal.jdbc.mock.RowData;
import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.DuplicateColumnException;
import org.seasar.doma.jdbc.DuplicateColumnHandler;
import org.seasar.doma.jdbc.PreparedSql;
import org.seasar.doma.jdbc.SelectOptions;
import org.seasar.doma.jdbc.SqlKind;
import org.seasar.doma.jdbc.SqlLogType;
import org.seasar.doma.jdbc.ThrowingDuplicateColumnHandler;
import org.seasar.doma.jdbc.UnknownColumnException;
import org.seasar.doma.jdbc.UnknownColumnHandler;
import org.seasar.doma.jdbc.entity.EntityType;
Expand Down Expand Up @@ -111,6 +116,46 @@ public void testGetEntity_EmptyUnknownColumnHandler() throws Exception {
assertEquals(100, emp.getVersion());
}

@Test
public void testCreateIndexMap_WithDuplicateColumnName() throws SQLException {
MockResultSetMetaData metaData = new MockResultSetMetaData();
metaData.columns.add(new ColumnMetaData("id"));
metaData.columns.add(new ColumnMetaData("name"));
metaData.columns.add(new ColumnMetaData("name")); // Duplicate column name
metaData.columns.add(new ColumnMetaData("version"));
MockResultSet resultSet = new MockResultSet(metaData);
resultSet.rows.add(new RowData(1, "aaa", "bbb", 100));
resultSet.next();

_Emp entityType = _Emp.getSingletonInternal();
EntityProvider<Emp> provider =
new EntityProvider<>(entityType, new MySelectQuery(new MockConfig()), false);

provider.createIndexMap(metaData, entityType);
Emp emp = provider.get(resultSet);

assertEquals(1, emp.getId());
assertEquals("bbb", emp.getName());
assertEquals(100, emp.getVersion());
}

@Test
public void testCreateIndexMap_DuplicateColumnHandler() throws SQLException {
MockResultSetMetaData metaData = new MockResultSetMetaData();
metaData.columns.add(new ColumnMetaData("id"));
metaData.columns.add(new ColumnMetaData("name"));
metaData.columns.add(new ColumnMetaData("name")); // Duplicate column name
metaData.columns.add(new ColumnMetaData("version"));

_Emp entityType = _Emp.getSingletonInternal();
EntityProvider<Emp> provider =
new EntityProvider<>(
entityType, new MySelectQuery(new SetDuplicateColumnHandlerConfig()), false);

assertThrows(
DuplicateColumnException.class, () -> provider.createIndexMap(metaData, entityType));
}

protected static class MySelectQuery implements SelectQuery {

private final Config config;
Expand Down Expand Up @@ -213,4 +258,11 @@ public UnknownColumnHandler getUnknownColumnHandler() {
return new EmptyUnknownColumnHandler();
}
}

protected static class SetDuplicateColumnHandlerConfig extends MockConfig {
@Override
public DuplicateColumnHandler getDuplicateColumnHandler() {
return new ThrowingDuplicateColumnHandler();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Doma 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.seasar.doma.jdbc;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

public class DuplicateColumnExceptionTest {

@Test
public void test() {
DuplicateColumnException e =
new DuplicateColumnException(SqlLogType.FORMATTED, "aaa", "bbb", "ccc", "ddd");
System.out.println(e.getMessage());
assertEquals("aaa", e.getColumnName());
assertEquals("bbb", e.getRawSql());
assertEquals("ccc", e.getFormattedSql());
assertEquals("ddd", e.getSqlFilePath());
}
}

0 comments on commit 03272c7

Please sign in to comment.