diff --git a/hsweb-commons/hsweb-commons-crud/pom.xml b/hsweb-commons/hsweb-commons-crud/pom.xml index 8b53d6d44..3efcbec25 100644 --- a/hsweb-commons/hsweb-commons-crud/pom.xml +++ b/hsweb-commons/hsweb-commons-crud/pom.xml @@ -111,6 +111,12 @@ test + + org.postgresql + r2dbc-postgresql + test + + com.h2database h2 @@ -166,4 +172,4 @@ - \ No newline at end of file + diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java index 9d2ad6dd4..4c85cb18f 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java @@ -78,7 +78,16 @@ public Select select() { @Override public Optional findColumn(String name) { - return Optional.ofNullable(getColumnMappings().get(name)); + if (name == null) { + return Optional.empty(); + } + Map mappings = getColumnMappings(); + Column column = mappings.get(name); + if (column == null) { + // 精确别名优先,仅在未命中时兼容实体属性名到数据库列名的转换。 + column = mappings.get(QueryHelperUtils.toSnake(name)); + } + return Optional.ofNullable(column); } @Override @@ -177,7 +186,7 @@ private Column getColumnOrSelectColumn(String name) { return column; } - return getColumnMappings().get(name); + return findColumn(name).orElse(null); } @SneakyThrows @@ -652,7 +661,7 @@ public SqlFragments createTermFragments(QueryAnalyzerImpl impl, Term term) { Table table = impl.select.table; String column = term.getColumn(); - Column col = impl.getColumnMappings().get(column); + Column col = impl.findColumn(column).orElse(null); // // if (col == null) { // if (column.contains(".")) { @@ -697,9 +706,12 @@ public SqlFragments createTermFragments(QueryAnalyzerImpl impl, Term term) { String colName = col.metadata != null ? col.metadata.getRealName() : col.name; - String fullName = col.metadata != null - ? col.getMetadata().getFullName(table.alias) - : table.alias + "." + dialect.quote(colName, false); + // CTE和子查询会复用真实表列元数据,其内部可能已缓存原表全名, + // 条件列必须以当前查询分析出的表别名为准。 + String fullName = dialect.buildColumnFullName( + table.alias, + colName, + col.metadata != null && !col.metadata.realNameDetected()); return metadata .findFeature(createFeatureId(term.getTermType())) diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java index 8a9f30999..590598620 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java @@ -1,12 +1,14 @@ package org.hswebframework.web.crud.query; +import org.hswebframework.ezorm.core.param.Sort; +import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers; import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; -import org.hswebframework.ezorm.core.param.Sort; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.crud.TestApplication; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -15,6 +17,7 @@ import reactor.test.StepVerifier; import java.util.ArrayList; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -24,6 +27,17 @@ public class QueryAnalyzerImplTest { @Autowired private DatabaseOperator database; + @Before + public void clearTestData() { + database + .sql() + .reactive() + .update("delete from s_test") + .then() + .as(StepVerifier::create) + .verifyComplete(); + } + /** * 执行SQL并验证是否有错误 */ @@ -762,6 +776,15 @@ public void testNestedUnion() { @Test public void testMultipleCTE() { + database + .getMetadata() + .getCurrentSchema() + .getTableOrView("s_test", false) + .orElseThrow() + .getColumn("name") + .orElseThrow() + .getFullName(); + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( database, "WITH " + @@ -779,6 +802,7 @@ public void testMultipleCTE() { assertNotNull(request.getSql(), "SQL should be generated"); assertNotNull(request.getParameters(), "Parameters should be set"); assertTrue(request.getSql().contains("), cte2 AS"), "multiple CTEs should be separated by comma"); + assertTrue(request.getSql().contains("cte1.\"NAME\""), "filter column should use current CTE alias"); executeAndVerify(request); } @@ -1128,4 +1152,69 @@ public void testCustomSortFunctionFallbackToColumn() { executeAndVerify(request); } + + @Test + public void testCamelCaseFilterColumnFallback() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select * from (select t.name as alarm_time from s_test t) a"); + + SqlRequest request = analyzer.refactor( + QueryParamEntity + .newQuery() + .and("alarmTime", "eq", "test") + .getParam()); + + assertTrue(analyzer.findColumn("alarmTime").isPresent()); + assertTrue(request.getSql().contains("alarm_time")); + assertArrayEquals(new Object[]{"test"}, request.getParameters()); + executeAndVerify(request); + } + + @Test + public void testQualifiedCamelCaseNestedFilterColumnFallback() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select * from (select t.name as alarm_time from s_test t) a"); + Term nested = new Term() + .nest() + .and("a.alarmTime", "eq", "test") + .clone(); + QueryParamEntity param = QueryParamEntity.of(); + param.setTerms(List.of(nested)); + + SqlRequest request = analyzer.refactor(param); + + assertTrue(request.getSql().contains("alarm_time")); + assertArrayEquals(new Object[]{"test"}, request.getParameters()); + executeAndVerify(request); + } + + @Test + public void testExactFilterAliasTakesPriorityOverSnakeCaseFallback() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select * from (select t.name as alarm_time, t.age as alarmTime from s_test t) a"); + + QueryAnalyzer.Column column = analyzer.findColumn("alarmTime").orElseThrow(); + + assertEquals("alarmTime", column.getAlias()); + } + + @Test + public void testUnknownCamelCaseFilterColumnIsRejected() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl( + database, + "select * from (select t.name as alarm_time from s_test t) a"); + QueryParamEntity param = QueryParamEntity + .newQuery() + .and("unknownAlarmTime", "eq", "test") + .getParam(); + + IllegalArgumentException error = assertThrows( + IllegalArgumentException.class, + () -> analyzer.refactor(param)); + + assertEquals("undefined column [unknownAlarmTime]", error.getMessage()); + } }