diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java index 775447a..72a10e0 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java @@ -4,11 +4,15 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler; +import cn.iocoder.yudao.framework.mybatis.core.injector.YudaoSqlInjector; +import cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap; import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer; import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; import com.baomidou.mybatisplus.core.handlers.IJsonTypeHandler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; +import com.baomidou.mybatisplus.core.injector.ISqlInjector; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.incrementer.*; import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal; @@ -17,12 +21,15 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.mapping.ResultMap; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; import org.springframework.core.env.ConfigurableEnvironment; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -59,6 +66,19 @@ public class YudaoMybatisAutoConfiguration { } @Bean + @Primary // 特殊:使用 @Primary 的原因,避免 MyBatis Plus Join 再创建 MPJSqlInjector 包装它 + public ISqlInjector yudaoSqlInjector() { + return new YudaoSqlInjector(); // selectMaps 的结果行,使用 CaseInsensitiveResultMap + } + + @Bean + public ConfigurationCustomizer caseInsensitiveResultMapConfigurationCustomizer() { + // 注册 CaseInsensitiveResultMap 的 resultMap,用于注解 SQL 的 @ResultMap + return configuration -> configuration.addResultMap(new ResultMap.Builder(configuration, + CaseInsensitiveResultMap.RESULT_MAP_ID, CaseInsensitiveResultMap.class, new ArrayList<>()).build()); + } + + @Bean @ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT") public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) { DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment); diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/injector/YudaoSqlInjector.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/injector/YudaoSqlInjector.java new file mode 100644 index 0000000..2fc1f72 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/injector/YudaoSqlInjector.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.framework.mybatis.core.injector; + +import cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.github.yulichang.injector.MPJSqlInjector; +import com.github.yulichang.method.mp.SelectMaps; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.session.Configuration; + +import java.util.List; + +/** + * 自定义 SQL 注入器 + * + * 在 MyBatis Plus Join 的 {@link MPJSqlInjector} 基础上,将 selectMaps 的结果行替换为 {@link CaseInsensitiveResultMap}, + * 解决 PostgreSQL、Oracle 等数据库列标签大小写折叠,导致按固定 key 取不到值的问题 + * + * @author 芋道源码 + */ +public class YudaoSqlInjector extends MPJSqlInjector { + + @Override + public List getMethodList(Configuration configuration, Class mapperClass, TableInfo tableInfo) { + List methods = super.getMethodList(configuration, mapperClass, tableInfo); + methods.replaceAll(method -> method instanceof SelectMaps ? new CaseInsensitiveSelectMaps() : method); + return methods; + } + + /** + * 结果行为 {@link CaseInsensitiveResultMap} 的 selectMaps + * + * 仅修改结果类型,SQL 仍由 MyBatis Plus Join 的 {@link SelectMaps} 生成,兼容 MPJ 的 Wrapper + */ + private static class CaseInsensitiveSelectMaps extends SelectMaps { + + @Override + protected MappedStatement addSelectMappedStatementForOther(Class mapperClass, String id, SqlSource sqlSource, + Class resultType) { + return super.addSelectMappedStatementForOther(mapperClass, id, sqlSource, CaseInsensitiveResultMap.class); + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java index 7dfb518..3b02c54 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.SortablePageParam; import cn.iocoder.yudao.framework.common.pojo.SortingField; import cn.iocoder.yudao.framework.common.util.object.PageUtils; +import cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap; import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; import com.baomidou.mybatisplus.annotation.DbType; @@ -14,6 +15,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; import com.baomidou.mybatisplus.core.toolkit.support.SFunction; import com.baomidou.mybatisplus.extension.toolkit.Db; import com.github.yulichang.base.MPJBaseMapper; @@ -23,6 +25,7 @@ import org.apache.ibatis.annotations.Param; import java.util.Collection; import java.util.List; +import java.util.Map; /** * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 @@ -99,6 +102,24 @@ public interface BaseMapperX extends MPJBaseMapper { return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); } + @Override + @SuppressWarnings("unchecked") + default Map selectJoinMap(@Param(Constants.WRAPPER) MPJBaseJoin wrapper) { + return selectJoinOne(CaseInsensitiveResultMap.class, wrapper); + } + + @Override + @SuppressWarnings("unchecked") + default List> selectJoinMaps(@Param(Constants.WRAPPER) MPJBaseJoin wrapper) { + return (List>) ((Object) selectJoinList(CaseInsensitiveResultMap.class, wrapper)); + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + default

>> P selectJoinMapsPage(P page, @Param(Constants.WRAPPER) MPJBaseJoin wrapper) { + return (P) selectJoinPage((IPage) page, CaseInsensitiveResultMap.class, wrapper); + } + default T selectOne(String field, Object value) { return selectOne(new QueryWrapper().eq(field, value)); } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/CaseInsensitiveResultMap.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/CaseInsensitiveResultMap.java new file mode 100644 index 0000000..2d1243b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/CaseInsensitiveResultMap.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.mybatis.core.util; + +import org.springframework.util.LinkedCaseInsensitiveMap; + +import java.util.Locale; + +/** + * key 大小写不敏感的 MyBatis 查询结果 Map + * + * 背景:使用 Map 接收查询结果时,key 为 JDBC 返回的列标签。PostgreSQL 等会将未加引号的列名、别名折叠为小写, + * Oracle、达梦等会折叠为大写,导致 map.get("conversationId") 这类按固定 key 取值的代码,在 MySQL 以外的数据库取不到值。 + * + * 实现:基于 {@link LinkedCaseInsensitiveMap},get / containsKey / remove 时忽略 key 的大小写, + * 同时保留原始的列标签作为 key(遍历、序列化时不变)。固定使用 {@link Locale#ROOT},避免服务器默认 Locale(例如土耳其语)影响 key 的匹配。 + * + * 使用方式:由 MyBatis 直接创建结果行,Mapper 的返回类型仍声明为 Map,业务无需感知该类 + * 1. MyBatis Plus 的 selectMaps:由 YudaoSqlInjector 注入,自动使用该类 + * 2. MyBatis Plus Join 的 selectJoinMaps 等:由 BaseMapperX 覆盖,自动使用该类 + * 3. XML SQL:将 resultType 设置为该类 + * 4. 注解 SQL:添加 @ResultMap({@link #RESULT_MAP_ID}) + * + * 注意:仅大小写不同的多个 key,会合并为一个,保留后写入的 key 和值 + * + * @author 芋道源码 + * @see 星球讨论:Map 查询结果的跨数据库兼容问题 + */ +public class CaseInsensitiveResultMap extends LinkedCaseInsensitiveMap { + + /** + * 注解 SQL 使用的 resultMap 编号 + * + * 由 YudaoMybatisAutoConfiguration 注册到 MyBatis 全局配置;包含“.”,所以不会拼接 Mapper 的 namespace + */ + public static final String RESULT_MAP_ID = "cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"; + + /** + * 创建空的结果 Map + * + * 用途:MyBatis 通过无参构造方法,实例化结果行 + */ + public CaseInsensitiveResultMap() { + super(Locale.ROOT); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/CaseInsensitiveResultMapTest.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/CaseInsensitiveResultMapTest.java new file mode 100644 index 0000000..700a340 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/CaseInsensitiveResultMapTest.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.framework.mybatis.core.util; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import org.junit.jupiter.api.Test; +import org.springframework.util.LinkedCaseInsensitiveMap; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link CaseInsensitiveResultMap} 的单元测试 + * + * @author 芋道源码 + */ +public class CaseInsensitiveResultMapTest { + + @Test + public void testGet_ignoreCase() { + // 准备参数 + CaseInsensitiveResultMap map = new CaseInsensitiveResultMap<>(); + map.put("conversationId", 1024L); + + // 调用,并断言:PostgreSQL 小写、Oracle 大写,都可以取到值 + assertEquals(1024L, map.get("conversationId")); + assertEquals(1024L, map.get("conversationid")); + assertEquals(1024L, map.get("CONVERSATIONID")); + assertTrue(map.containsKey("CONVERSATIONID")); + assertEquals(1024L, MapUtil.getLong(map, "conversationid")); + // 断言:保留原始的 key + assertEquals(Collections.singletonList("conversationId"), new ArrayList<>(map.keySet())); + } + + @Test + public void testPut_nullValue() { + // 准备参数 + CaseInsensitiveResultMap map = new CaseInsensitiveResultMap<>(); + map.put("sum_count", null); + + // 调用,并断言 + assertTrue(map.containsKey("SUM_COUNT")); + assertNull(map.get("SUM_COUNT")); + assertEquals(0, MapUtil.getInt(map, "SUM_COUNT", 0)); + } + + @Test + public void testPut_sameKeyIgnoreCase() { + // 准备参数 + CaseInsensitiveResultMap map = new CaseInsensitiveResultMap<>(); + + // 调用:仅大小写不同的 key + map.put("id", 1L); + map.put("ID", 2L); + + // 断言:合并为一个,保留后写入的 key 和值 + assertEquals(1, map.size()); + assertEquals(2L, map.get("id")); + assertEquals(Collections.singletonList("ID"), new ArrayList<>(map.keySet())); + } + + @Test + public void testGet_turkishDefaultLocale() { + Locale defaultLocale = Locale.getDefault(); + try { + // 准备参数:土耳其语下,大写 I 转小写为无点的 ı + Locale.setDefault(Locale.forLanguageTag("tr-TR")); + CaseInsensitiveResultMap map = new CaseInsensitiveResultMap<>(); + map.put("CONVERSATIONID", 1024L); + LinkedCaseInsensitiveMap defaultLocaleMap = new LinkedCaseInsensitiveMap<>(); + defaultLocaleMap.put("CONVERSATIONID", 1024L); + + // 调用,并断言:固定 Locale.ROOT,不受默认 Locale 影响 + assertEquals(1024L, map.get("conversationId")); + // 断言:使用默认 Locale 时,取不到值 + assertNull(defaultLocaleMap.get("conversationId")); + } finally { + Locale.setDefault(defaultLocale); + } + } + + @Test + public void testConvertMap_numberKey() { + // 准备参数:模拟 Oracle 返回的结果行 + List> rows = Arrays.asList(buildRow(1024L, 3), buildRow(2048L, 5)); + + // 调用:业务转换为数字 key 的 Map + Map result = CollectionUtils.convertMap(rows, + row -> MapUtil.getLong(row, "conversationId"), + row -> MapUtil.getInt(row, "count")); + + // 断言:数字 key 的业务 Map 不受影响 + assertInstanceOf(HashMap.class, result); + assertEquals(3, result.get(1024L)); + assertEquals(5, result.get(2048L)); + } + + private static Map buildRow(Long conversationId, Integer count) { + CaseInsensitiveResultMap row = new CaseInsensitiveResultMap<>(); + row.put("CONVERSATIONID", conversationId); + row.put("COUNT", count); + return row; + } + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java index 26e3d98..c847e1c 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.im.dal.mysql.statistics; +import cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.ResultMap; import org.apache.ibatis.annotations.Select; import java.time.LocalDateTime; @@ -59,6 +61,7 @@ public interface ImStatisticsManagerMapper { @Select("SELECT DATE(create_time) AS date, COUNT(*) AS count FROM system_users " + "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime} " + "GROUP BY DATE(create_time)") + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) List> selectNewUserDailyCount(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); @@ -74,6 +77,7 @@ public interface ImStatisticsManagerMapper { " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + " AND send_time >= #{beginTime} AND send_time < #{endTime}" + ") t GROUP BY day") + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) List> selectActiveUserDailyCount(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); // ==================== 群 ==================== @@ -111,6 +115,7 @@ public interface ImStatisticsManagerMapper { " GROUP BY g.id" + ") t " + "GROUP BY `range`") + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) List> selectGroupSizeDistribution(); // ==================== 消息 ==================== @@ -140,6 +145,7 @@ public interface ImStatisticsManagerMapper { "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + " AND send_time >= #{beginTime} AND send_time < #{endTime} " + "GROUP BY DATE(send_time)") + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) List> selectPrivateMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); @@ -150,6 +156,7 @@ public interface ImStatisticsManagerMapper { "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + " AND send_time >= #{beginTime} AND send_time < #{endTime} " + "GROUP BY DATE(send_time)") + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) List> selectGroupMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); @@ -167,6 +174,7 @@ public interface ImStatisticsManagerMapper { " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + " AND send_time >= #{beginTime} AND send_time < #{endTime}" + ") t GROUP BY type") + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) List> selectMessageTypeDistribution(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); @@ -184,6 +192,7 @@ public interface ImStatisticsManagerMapper { " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + " AND send_time >= #{beginTime} AND send_time < #{endTime}" + ") t GROUP BY user_id ORDER BY messageCount DESC LIMIT #{limit}") + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) List> selectTopSenders(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime, @Param("limit") int limit); diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapperTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapperTest.java index 7c50033..effc562 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapperTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapperTest.java @@ -228,6 +228,9 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest { assertEquals(3L, ((Number) tops.get(0).get("messageCount")).longValue()); assertEquals(2L, ((Number) tops.get(1).get("userId")).longValue()); assertEquals(2L, ((Number) tops.get(1).get("messageCount")).longValue()); + // 断言:key 大小写不敏感,兼容 PostgreSQL 小写、Oracle 大写的列标签 + assertEquals(1L, ((Number) tops.get(0).get("userid")).longValue()); + assertEquals(3L, ((Number) tops.get(0).get("MESSAGECOUNT")).longValue()); } // ========== 工具方法 ========== diff --git a/yudao-module-mes/src/main/resources/mapper/home/MesHomeStatisticsMapper.xml b/yudao-module-mes/src/main/resources/mapper/home/MesHomeStatisticsMapper.xml index 33d6f00..f0307cf 100644 --- a/yudao-module-mes/src/main/resources/mapper/home/MesHomeStatisticsMapper.xml +++ b/yudao-module-mes/src/main/resources/mapper/home/MesHomeStatisticsMapper.xml @@ -3,7 +3,7 @@ - SELECT status, COUNT(*) AS count FROM mes_pro_work_order WHERE deleted = 0 @@ -11,7 +11,7 @@ - SELECT IFNULL(SUM(feedback_quantity), 0) AS feedbackQuantity, IFNULL(SUM(qualified_quantity), 0) AS qualifiedQuantity, IFNULL(SUM(unqualified_quantity), 0) AS unqualifiedQuantity @@ -25,7 +25,7 @@ - SELECT status, COUNT(*) AS count FROM mes_dv_machinery WHERE deleted = 0 @@ -51,7 +51,7 @@ - SELECT DATE_FORMAT(feedback_time, '%Y-%m-%d') AS date, IFNULL(SUM(feedback_quantity), 0) AS quantity, IFNULL(SUM(qualified_quantity), 0) AS qualifiedQuantity, diff --git a/yudao-module-wms/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml b/yudao-module-wms/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml index 38c7bdf..59813e9 100644 --- a/yudao-module-wms/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml +++ b/yudao-module-wms/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml @@ -3,7 +3,7 @@ - SELECT orderType, status, SUM(count) AS count FROM ( SELECT ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@RECEIPT.type} AS orderType, @@ -51,7 +51,7 @@ - SELECT date, orderType, SUM(count) AS count FROM ( SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS date, @@ -117,7 +117,7 @@ - SELECT item.id AS id, item.name AS name, IFNULL(SUM(inventory.quantity), 0) AS quantity @@ -135,7 +135,7 @@ - SELECT warehouse.id AS id, warehouse.name AS name, IFNULL(SUM(inventory.quantity), 0) AS quantity