From 225cd1c4133cc1dccc9b1f11206ffbb406851ecd Mon Sep 17 00:00:00 2001 From: youdaosu <1098526175@qq.com> Date: Tue, 22 Sep 2026 12:05:22 +0800 Subject: [PATCH 1/2] fix: include cross-JOIN ON referenced columns in count subquery for MySQL When HEAD (count pagination) uses multiple JOINs, the subquery for each JOIN table only selected its own ON key (on.getKey()). If another JOIN's ON clause references a column of this table (e.g. Warehose_info.id@ = /Location_info/warehouse_id), that column was missing from the subquery, causing an Unknown column error on MySQL. Now, for MySQL-family databases, also collect the targetKey of any ON clause (from any JOIN in the list) whose target table/alias matches the current JOIN table. PostgreSQL keeps its SELECT * fallback. issue #824 fixes #824 --- .../java/apijson/orm/AbstractSQLConfig.java | 23 +- .../orm/AbstractSQLConfigJoinCountTest.java | 261 ++++++++++++++++++ 2 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/test/java/apijson/orm/AbstractSQLConfigJoinCountTest.java diff --git a/src/main/java/apijson/orm/AbstractSQLConfig.java b/src/main/java/apijson/orm/AbstractSQLConfig.java index ca47bdf2..6ef778b1 100755 --- a/src/main/java/apijson/orm/AbstractSQLConfig.java +++ b/src/main/java/apijson/orm/AbstractSQLConfig.java @@ -6585,9 +6585,30 @@ LEFT JOIN ( SELECT count(*) AS count FROM sys.Comment ) AS Comment ON Comment.m ///* SELECT count(*) AS count FROM sys.Moment AS Moment // LEFT JOIN ( SELECT * FROM sys.Comment ) AS Comment ON Comment.momentId = Moment.id LIMIT 1 OFFSET 0 */ if (column != null && joinConfig.isMSQL()) { // 暂时这样兼容 PostgreSQL 等不支持 SELECT 中不包含对应 key 的隐式 ON 关联字段的数据库 + // 当前副表自身的关联键(joinConfig 是当前副表) + Set columnSet = new LinkedHashSet<>(); for (On on : onList) { - column.add(on.getKey()); // TODO PostgreSQL 等需要找到具体的 targetTable 对应 targetKey 来加到 SELECT,比直接 SELECT * 性能更好 + columnSet.add(on.getKey()); } + // issue #824:多表 JOIN 时,其它副表的 ON 可能引用当前副表的字段(如 + // Warehose_info.id@ = /Location_info/warehouse_id 引用 Location_info.warehouse_id)。 + // 只收集当前副表自身的 on.getKey() 会漏掉被引用的字段,生成子查询缺列导致 Unknown column。 + // 这里把所有 JOIN 的 ON 里 target 指向当前副表的 targetKey 也补进 SELECT 列。 + for (Join other : joinList) { + List otherOnList = other.getOnList(); + if (otherOnList == null) { + continue; + } + // 当前副表的别名(含主表引用自身场景) + String currentAlias = SQLConfig.gainSQLAlias(table, alias); + for (On otherOn : otherOnList) { + if (currentAlias.equals(SQLConfig.gainSQLAlias(otherOn.getTargetTable(), otherOn.getTargetAlias()))) { + columnSet.add(otherOn.getTargetKey()); + } + } + } + column.addAll(columnSet); + // TODO PostgreSQL 等需要找到具体的 targetTable 对应 targetKey 来加到 SELECT,比直接 SELECT * 性能更好 } joinConfig.setMethod(GET); // 子查询不能为 SELECT count(*) ,而应该是 SELECT momentId diff --git a/src/test/java/apijson/orm/AbstractSQLConfigJoinCountTest.java b/src/test/java/apijson/orm/AbstractSQLConfigJoinCountTest.java new file mode 100644 index 00000000..e960ad9d --- /dev/null +++ b/src/test/java/apijson/orm/AbstractSQLConfigJoinCountTest.java @@ -0,0 +1,261 @@ +package apijson.orm; + +import apijson.JSON; +import apijson.JSONParser; +import apijson.RequestMethod; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * 复现 issue #824:PostgreSQL + HEAD(count 分页) + JOIN 时, + * LEFT JOIN 子查询 SELECT 只含副表关联键,缺 ON 引用的主表字段,PG 报 column not found。 + */ +public class AbstractSQLConfigJoinCountTest { + private static final String MAIN_TABLE = "Inventory"; + private static final String JOIN_TABLE = "Location_info"; + private static final String JOIN_TABLE2 = "Warehose_info"; + private static final String DATABASE = SQLConfig.DATABASE_POSTGRESQL; + + private static JSONParser, ? extends List> previousJSONParser; + + @BeforeClass + public static void installJSONParser() { + previousJSONParser = JSON.DEFAULT_JSON_PARSER; + JSON.DEFAULT_JSON_PARSER = new JSONParser, List>() { + @Override + public Map createJSONObject() { + return new LinkedHashMap<>(); + } + + @Override + public List createJSONArray() { + return new ArrayList<>(); + } + + @Override + public Object parse(Object json) { + return json; + } + + @Override + @SuppressWarnings("unchecked") + public Map parseObject(Object json) { + return (Map) json; + } + + @Override + public T parseObject(Object json, Class clazz) { + return clazz.cast(json); + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json) { + return (List) json; + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json, Class clazz) { + return (List) json; + } + + @Override + public String toJSONString(Object obj, boolean format) { + return String.valueOf(obj); + } + }; + } + + @AfterClass + public static void restoreJSONParser() { + JSON.DEFAULT_JSON_PARSER = previousJSONParser; + } + + /** + * 复现 issue #824 的 count 分页 SQL(PostgreSQL 场景): + * SELECT count(*) AS "count" FROM "Inventory" AS "Inventory" + * LEFT JOIN ( SELECT * FROM "Location_info" ) AS "Location_info" + * ON "Location_info"."code" = "Inventory"."location_code" + * LEFT JOIN ( SELECT * FROM "Warehose_info" ) AS "Warehose_info" + * ON "Warehose_info"."id" = "Location_info"."warehouse_id" + * + * main 分支已对 PostgreSQL 回退为 SELECT *(isMSQL 门控),子查询不缺列,PG 不再报错。 + * 本用例断言该行为(回归保护)。 + */ + @Test + public void headJoinCountPostgreSQLFallsBackToSelectStar() throws Exception { + String sql = buildHeadJoinSQL(); + + System.out.println("Generated SQL:\n" + sql + "\n"); + + // 断言:LEFT JOIN 子查询结构存在 + int leftJoin1 = sql.indexOf("LEFT JOIN ( SELECT"); + assertTrue("应为 LEFT JOIN 子查询结构: " + sql, leftJoin1 >= 0); + + // 副表1子查询部分(PG 应回退 SELECT *,天然包含 code + warehouse_id) + String sub1 = extractSubquery(sql, "Location_info"); + System.out.println("Subquery 1: " + sub1); + assertTrue("PG 下 Location_info 子查询应回退 SELECT *(含所有列,天然覆盖 ON 引用): " + sub1, + sub1.contains("SELECT *")); + + // 副表2子查询部分 + String sub2 = extractSubquery(sql, "Warehose_info"); + System.out.println("Subquery 2: " + sub2); + assertTrue("PG 下 Warehose_info 子查询应回退 SELECT *: " + sub2, + sub2.contains("SELECT *")); + + // ON 条件完整性 + assertTrue("ON 应引用 Location_info.code = Inventory.location_code: " + sql, + sql.contains("\"Location_info\".\"code\" = \"Inventory\".\"location_code\"")); + assertTrue("ON 应引用 Warehose_info.id = Location_info.warehouse_id: " + sql, + sql.contains("\"Warehose_info\".\"id\" = \"Location_info\".\"warehouse_id\"")); + } + + /** + * 回归:MySQL 下副表子查询只含自身关联键 code/id(isMSQL 门控逻辑)。 + * 本用例断言当前行为,暴露 MySQL 多 join 交叉引用 bug(第二个 ON 引用 + * Location_info.warehouse_id,但子查询没有该列 -> Unknown column)。 + */ + @Test + public void headJoinCountMySQLOnlySelectsSelfOnKey() throws Exception { + String sql = buildHeadJoinSQLWithDatabase(SQLConfig.DATABASE_MYSQL); + + System.out.println("MySQL SQL:\n" + sql + "\n"); + + String sub1 = extractSubquery(sql, "Location_info"); + assertTrue("MySQL 下 Location_info 子查询应包含 code: " + sub1, sub1.contains("`code`")); + // 关键:MySQL 下子查询缺 warehouse_id,ON 引用它 → 复现 Unknown column + assertTrue("MySQL 下 Location_info 子查询应包含 warehouse_id(issue #824 反向场景,被 ON 引用的列): " + sub1, + sub1.contains("`warehouse_id`")); + } + + private static String buildHeadJoinSQL() throws Exception { + return buildHeadJoinSQLWithDatabase(DATABASE); + } + + private static String extractSubquery(String sql, String table) { + String marker = "JOIN ( "; + int joinIdx = sql.indexOf(marker); + while (joinIdx >= 0) { + int closeIdx = sql.indexOf(" ) ", joinIdx); + if (closeIdx < 0) { + break; + } + String sub = sql.substring(joinIdx + marker.length(), closeIdx); + if (sub.contains(table)) { + return sub; + } + joinIdx = sql.indexOf(marker, closeIdx); + } + return ""; + } + + private static String buildHeadJoinSQLWithDatabase(String database) throws Exception { + // 主表 + Map mainRequest = new LinkedHashMap<>(); + + // join 副表1:Location_info.code@ = /Inventory/location_code + // 注意:request 中不能包含 @ 引用键(真实解析器把它们剥离进 onList), + // 只把 onList 作为唯一关联信息来源(与 AbstractParser.onJoinParse 一致) + Map joinRequest1 = new LinkedHashMap<>(); + + // join 副表2:Warehose_info.id@ = /Location_info/warehouse_id + Map joinRequest2 = new LinkedHashMap<>(); + + // 组装 joinList(on 定义显式传入,与 AbstractParser.onJoinParse 剥离后的 onList 等价) + List, List>> joinList = new ArrayList<>(); + joinList.add(newJoin(", List> callback = + new AbstractSQLConfig.SimpleCallback, List>() { + @Override + public SQLConfig, List> getSQLConfig(RequestMethod method, + String database, String datasource, String namespace, String catalog, String schema, + String table) { + return new AbstractSQLConfig, List>(method, table) { + @Override + public String gainDBVersion() { + return "16.0"; + } + + @Override + public String gainDBUri() { + return "jdbc:postgresql://localhost/test"; + } + + @Override + public String gainDBAccount() { + return "test"; + } + + @Override + public String gainDBPassword() { + return "test"; + } + }; + } + }; + + SQLConfig, List> config = + AbstractSQLConfig.newSQLConfig(RequestMethod.HEAD, MAIN_TABLE, null, mainRequest, null, false, callback); + config.setDatabase(database); + + config = AbstractSQLConfig.parseJoin(RequestMethod.HEAD, config, joinList, callback); + + return config.gainSQL(false); + } + + /**手工构建 Join(模拟 AbstractParser.onJoinParse 剥离 @ 引用键后的产物) + * @param path join 路径 , List> newJoin(String path, String table, + String originKey, String targetPath) throws Exception { + Join, List> j = new Join<>(); + j.setPath(path); + j.setJoinType("<"); + j.setTable(table); + j.setRequest(new LinkedHashMap<>()); // 真实解析器剥离 @ 引用键,request 为空 + + List onList = new ArrayList<>(); + Join.On on = new Join.On(); + // originKey = "code@" -> key = "code"(setKeyAndType 会剥掉 @) + on.setKeyAndType("<", table, originKey); + on.setOriginKey(originKey); + on.setOriginValue(targetPath); + + // 解析 /TargetTable/targetKey + String path0 = targetPath.startsWith("/") ? targetPath.substring(1) : targetPath; + int idx = path0.lastIndexOf("/"); + String targetKey = idx < 0 ? path0 : path0.substring(idx + 1); + String targetTableKey = idx < 0 ? "" : path0.substring(0, idx); + // targetTableKey 可能带别名 TargetTable:alias + int cIdx = targetTableKey.indexOf(":"); + String targetTable = cIdx < 0 ? targetTableKey : targetTableKey.substring(0, cIdx); + String targetAlias = cIdx < 0 ? null : targetTableKey.substring(cIdx + 1); + + on.setTargetTableKey(targetTableKey); + on.setTargetTable(targetTable); + on.setTargetAlias(targetAlias); + on.setTargetKey(targetKey); + + onList.add(on); + j.setOnList(onList); + + return j; + } +} From 498fcf5a58d2e5bee51e6154bbaf76d23b5eda5a Mon Sep 17 00:00:00 2001 From: youdaosu <1098526175@qq.com> Date: Tue, 22 Sep 2026 14:15:16 +0800 Subject: [PATCH 2/2] test: clarify JoinCountTest purpose and remove unused imports --- .../java/apijson/orm/AbstractSQLConfigJoinCountTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/apijson/orm/AbstractSQLConfigJoinCountTest.java b/src/test/java/apijson/orm/AbstractSQLConfigJoinCountTest.java index e960ad9d..2d98f551 100644 --- a/src/test/java/apijson/orm/AbstractSQLConfigJoinCountTest.java +++ b/src/test/java/apijson/orm/AbstractSQLConfigJoinCountTest.java @@ -12,13 +12,12 @@ import java.util.List; import java.util.Map; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; /** - * 复现 issue #824:PostgreSQL + HEAD(count 分页) + JOIN 时, - * LEFT JOIN 子查询 SELECT 只含副表关联键,缺 ON 引用的主表字段,PG 报 column not found。 + * 复现 issue #824:多表 JOIN + HEAD(count 分页) 子查询字段缺失。 + * - PostgreSQL:LEFT JOIN 子查询回退 SELECT *(main 已修复原报告场景,此处作回归) + * - MySQL:子查询只选自身关联键,缺被其它 ON 引用的字段,仍会报 Unknown column(本次修复) */ public class AbstractSQLConfigJoinCountTest { private static final String MAIN_TABLE = "Inventory";