├── src ├── main │ └── java │ │ └── xyz │ │ └── erupt │ │ └── linq │ │ ├── consts │ │ ├── OrderByDirection.java │ │ ├── JoinExchange.java │ │ ├── CompareSymbol.java │ │ └── JoinMethod.java │ │ ├── lambda │ │ ├── Th.java │ │ ├── SFunction.java │ │ ├── LambdaInfo.java │ │ └── LambdaSee.java │ │ ├── exception │ │ └── LinqException.java │ │ ├── grammar │ │ ├── GroupBy.java │ │ ├── OrderBy.java │ │ ├── Select.java │ │ ├── Join.java │ │ ├── Where.java │ │ └── Write.java │ │ ├── util │ │ ├── VirtualColumn.java │ │ ├── CompareUtil.java │ │ ├── ReflectField.java │ │ ├── Columns.java │ │ └── RowUtil.java │ │ ├── schema │ │ ├── OrderBySchema.java │ │ ├── WhereSchema.java │ │ ├── JoinSchema.java │ │ ├── Dql.java │ │ ├── Column.java │ │ └── Row.java │ │ ├── engine │ │ ├── Engine.java │ │ └── EruptEngine.java │ │ └── Linq.java └── test │ └── java │ └── xyz │ └── erupt │ └── linq │ ├── data │ ├── student │ │ ├── BaseModel.java │ │ ├── StudentSubject.java │ │ ├── StudentVo.java │ │ ├── Student.java │ │ ├── StudentScore.java │ │ └── StudentScoreAnalysis.java │ ├── source │ │ ├── TestSourceParent.java │ │ ├── TestSourceExt.java │ │ ├── TestSourceExt2.java │ │ ├── TestSource.java │ │ └── TestSourceGroupByVo.java │ ├── Table.java │ ├── customer │ │ ├── CustomerInfo.java │ │ └── CustomerChurnModel.java │ └── TestTo.java │ ├── LambdaParseTest.java │ ├── LambdaTest.java │ ├── PerformanceTest.java │ ├── DatasetCustomerTest.java │ ├── DatasetStudentTest.java │ └── LinqTest.java ├── .gitignore ├── LICENSE ├── pom.xml ├── README-zh.md └── README.md /src/main/java/xyz/erupt/linq/consts/OrderByDirection.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.consts; 2 | 3 | public enum OrderByDirection { 4 | 5 | ASC, 6 | DESC 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/consts/JoinExchange.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.consts; 2 | 3 | public enum JoinExchange { 4 | 5 | HASH, 6 | 7 | NESTED_LOOP 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/consts/CompareSymbol.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.consts; 2 | 3 | public enum CompareSymbol { 4 | 5 | GT, 6 | LT, 7 | GTE, 8 | LTE 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/consts/JoinMethod.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.consts; 2 | 3 | public enum JoinMethod { 4 | 5 | LEFT, 6 | RIGHT, 7 | INNER, 8 | FULL 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/lambda/Th.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.lambda; 2 | 3 | public class Th { 4 | 5 | private final Object is = null; 6 | 7 | public Object is() { 8 | return null; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/lambda/SFunction.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.lambda; 2 | 3 | import java.io.Serializable; 4 | import java.util.function.Function; 5 | 6 | @FunctionalInterface 7 | public interface SFunction extends Function, Serializable { 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/student/BaseModel.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.student; 2 | 3 | /** 4 | * @author YuePeng 5 | * date 2024/4/3 21:41 6 | */ 7 | public class BaseModel{ 8 | 9 | private Long test; 10 | 11 | public Long getTest() { 12 | return test; 13 | } 14 | 15 | public void setTest(Long test) { 16 | this.test = test; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/source/TestSourceParent.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.source; 2 | 3 | /** 4 | * @author YuePeng 5 | * date 2024/4/3 23:11 6 | */ 7 | public class TestSourceParent { 8 | 9 | private Integer id; 10 | 11 | public Integer getId() { 12 | return id; 13 | } 14 | 15 | public void setId(Integer id) { 16 | this.id = id; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/exception/LinqException.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.exception; 2 | 3 | public class LinqException extends RuntimeException { 4 | 5 | public LinqException(String message) { 6 | super(message); 7 | } 8 | 9 | public LinqException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | public LinqException(String message, Throwable e) { 14 | super(message, e); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/Table.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data; 2 | 3 | public class Table { 4 | 5 | private String NAME; 6 | 7 | private String name1; 8 | 9 | private String Name2; 10 | 11 | private String AAA_BBB; 12 | 13 | public String getNAME() { 14 | return NAME; 15 | } 16 | 17 | public String getName1() { 18 | return name1; 19 | } 20 | 21 | public String getName2() { 22 | return Name2; 23 | } 24 | 25 | public String getAAA_BBB() { 26 | return AAA_BBB; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/LambdaParseTest.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq; 2 | 3 | import org.junit.Test; 4 | import xyz.erupt.linq.data.Table; 5 | import xyz.erupt.linq.lambda.LambdaSee; 6 | 7 | import java.util.Objects; 8 | 9 | public class LambdaParseTest { 10 | 11 | @Test 12 | public void parseTest() { 13 | assert Objects.equals(LambdaSee.field(Table::getNAME), "NAME"); 14 | assert Objects.equals(LambdaSee.field(Table::getName1), "name1"); 15 | assert Objects.equals(LambdaSee.field(Table::getName2), "name2"); 16 | assert Objects.equals(LambdaSee.field(Table::getAAA_BBB), "AAA_BBB"); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/student/StudentSubject.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.student; 2 | 3 | public class StudentSubject { 4 | 5 | private Long id; 6 | 7 | private String name; 8 | 9 | public StudentSubject(Long id, String name) { 10 | this.id = id; 11 | this.name = name; 12 | } 13 | 14 | public Long getId() { 15 | return id; 16 | } 17 | 18 | public void setId(Long id) { 19 | this.id = id; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/source/TestSourceExt.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.source; 2 | 3 | public class TestSourceExt { 4 | 5 | private Integer id; 6 | 7 | private String name; 8 | 9 | public TestSourceExt(Integer id, String name) { 10 | this.id = id; 11 | this.name = name; 12 | } 13 | 14 | public Integer getId() { 15 | return id; 16 | } 17 | 18 | public void setId(Integer id) { 19 | this.id = id; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/source/TestSourceExt2.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.source; 2 | 3 | public class TestSourceExt2 { 4 | 5 | private Integer id; 6 | 7 | private Object value; 8 | 9 | public TestSourceExt2(Integer id, Object value) { 10 | this.id = id; 11 | this.value = value; 12 | } 13 | 14 | public Integer getId() { 15 | return id; 16 | } 17 | 18 | public void setId(Integer id) { 19 | this.id = id; 20 | } 21 | 22 | public Object getValue() { 23 | return value; 24 | } 25 | 26 | public void setValue(Object value) { 27 | this.value = value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/lambda/LambdaInfo.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.lambda; 2 | 3 | public class LambdaInfo { 4 | 5 | private final Class clazz; 6 | 7 | private final String method; 8 | 9 | private final String field; 10 | 11 | public LambdaInfo(Class clazz, String method, String field) { 12 | this.clazz = clazz; 13 | this.method = method; 14 | this.field = field; 15 | } 16 | 17 | public Class getClazz() { 18 | return clazz; 19 | } 20 | 21 | public String getMethod() { 22 | return method; 23 | } 24 | 25 | public String getField() { 26 | return field; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/grammar/GroupBy.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.grammar; 2 | 3 | import xyz.erupt.linq.Linq; 4 | import xyz.erupt.linq.lambda.SFunction; 5 | import xyz.erupt.linq.schema.Column; 6 | import xyz.erupt.linq.schema.Row; 7 | 8 | import java.util.function.BiFunction; 9 | import java.util.function.Function; 10 | 11 | public interface GroupBy { 12 | 13 | Linq groupBy(Column... columns); 14 | 15 | Linq groupBy(SFunction... columns); 16 | 17 | Linq groupBy(SFunction column, BiFunction convert); 18 | 19 | Linq having(Function condition); 20 | 21 | default Linq having(SFunction fun, Function condition) { 22 | return having(row -> condition.apply(row.get(fun))); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/util/VirtualColumn.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.util; 2 | 3 | import xyz.erupt.linq.lambda.LambdaInfo; 4 | import xyz.erupt.linq.lambda.LambdaSee; 5 | 6 | public class VirtualColumn { 7 | 8 | public String col() { 9 | return null; 10 | } 11 | 12 | public Integer number() { 13 | return null; 14 | } 15 | 16 | public String string() { 17 | return null; 18 | } 19 | 20 | public static LambdaInfo lambdaColumn() { 21 | return LambdaSee.info(VirtualColumn::col); 22 | } 23 | 24 | public static LambdaInfo lambdaStr() { 25 | return LambdaSee.info(VirtualColumn::string); 26 | } 27 | 28 | public static LambdaInfo lambdaNumber() { 29 | return LambdaSee.info(VirtualColumn::number); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/customer/CustomerInfo.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.customer; 2 | 3 | public class CustomerInfo { 4 | 5 | private Long customerId; 6 | 7 | private String nickName; 8 | 9 | public CustomerInfo(Long customerId, String nickName) { 10 | this.customerId = customerId; 11 | this.nickName = nickName; 12 | } 13 | 14 | public CustomerInfo() { 15 | } 16 | 17 | public Long getCustomerId() { 18 | return customerId; 19 | } 20 | 21 | public void setCustomerId(Long customerId) { 22 | this.customerId = customerId; 23 | } 24 | 25 | public String getNickName() { 26 | return nickName; 27 | } 28 | 29 | public void setNickName(String nickName) { 30 | this.nickName = nickName; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/schema/OrderBySchema.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.schema; 2 | 3 | import xyz.erupt.linq.consts.OrderByDirection; 4 | 5 | public class OrderBySchema { 6 | 7 | private Column column; 8 | 9 | private OrderByDirection direction; 10 | 11 | public OrderBySchema() { 12 | } 13 | 14 | public OrderBySchema(Column column, OrderByDirection direction) { 15 | this.column = column; 16 | this.direction = direction; 17 | } 18 | 19 | public Column getColumn() { 20 | return column; 21 | } 22 | 23 | public void setColumn(Column column) { 24 | this.column = column; 25 | } 26 | 27 | public OrderByDirection getDirection() { 28 | return direction; 29 | } 30 | 31 | public void setDirection(OrderByDirection direction) { 32 | this.direction = direction; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/schema/WhereSchema.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.schema; 2 | 3 | import java.util.function.Function; 4 | 5 | public class WhereSchema { 6 | 7 | private Function condition; 8 | 9 | private Column relationColumn; 10 | 11 | public WhereSchema(Function condition, Column relationColumn) { 12 | this.condition = condition; 13 | this.relationColumn = relationColumn; 14 | } 15 | 16 | public Function getCondition() { 17 | return condition; 18 | } 19 | 20 | public void setCondition(Function condition) { 21 | this.condition = condition; 22 | } 23 | 24 | public Column getRelationColumn() { 25 | return relationColumn; 26 | } 27 | 28 | public void setRelationColumn(Column relationColumn) { 29 | this.relationColumn = relationColumn; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/util/CompareUtil.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.util; 2 | 3 | import xyz.erupt.linq.consts.CompareSymbol; 4 | 5 | public class CompareUtil { 6 | 7 | public static boolean compare(Object value, Object compareTo, CompareSymbol compareSymbol) { 8 | if (null == compareTo) return false; 9 | if (value instanceof Comparable) { 10 | Comparable comparable = ((Comparable) value); 11 | switch (compareSymbol) { 12 | case GT: 13 | return comparable.compareTo(compareTo) > 0; 14 | case LT: 15 | return comparable.compareTo(compareTo) < 0; 16 | case GTE: 17 | return comparable.compareTo(compareTo) >= 0; 18 | case LTE: 19 | return comparable.compareTo(compareTo) <= 0; 20 | } 21 | } 22 | return false; 23 | } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/student/StudentVo.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.student; 2 | 3 | public class StudentVo { 4 | 5 | private String name; 6 | 7 | private String subjectName; 8 | 9 | private Integer score; 10 | 11 | public String getName() { 12 | return name; 13 | } 14 | 15 | public void setName(String name) { 16 | this.name = name; 17 | } 18 | 19 | public String getSubjectName() { 20 | return subjectName; 21 | } 22 | 23 | public void setSubjectName(String subjectName) { 24 | this.subjectName = subjectName; 25 | } 26 | 27 | public Integer getScore() { 28 | return score; 29 | } 30 | 31 | public void setScore(Integer score) { 32 | this.score = score; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "StudentVo{" + 38 | "name='" + name + '\'' + 39 | ", subjectName='" + subjectName + '\'' + 40 | ", score=" + score + 41 | '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/engine/Engine.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.engine; 2 | 3 | import xyz.erupt.linq.exception.LinqException; 4 | import xyz.erupt.linq.schema.Dql; 5 | import xyz.erupt.linq.schema.JoinSchema; 6 | import xyz.erupt.linq.schema.Row; 7 | 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public abstract class Engine { 13 | 14 | public void preprocessor(Dql dql) { 15 | if (dql.getColumns().isEmpty()) { 16 | throw new LinqException("Missing select definition"); 17 | } 18 | // join check 19 | Map, Void> joinMap = new HashMap<>(); 20 | for (JoinSchema schema : dql.getJoinSchemas()) { 21 | if (joinMap.containsKey(schema.getClazz())) { 22 | throw new LinqException("The same object join is not supported " + " → " + schema.getClazz().getSimpleName()); 23 | } 24 | joinMap.put(schema.getClazz(), null); 25 | } 26 | } 27 | 28 | 29 | public abstract List query(Dql dql); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/source/TestSource.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.source; 2 | 3 | import java.util.Date; 4 | 5 | public class TestSource extends TestSourceParent { 6 | 7 | private String name; 8 | 9 | private Date date; 10 | 11 | private String[] tags; 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public String name() { 18 | return name; 19 | } 20 | 21 | public void setName(String name) { 22 | this.name = name; 23 | } 24 | 25 | public Date getDate() { 26 | return date; 27 | } 28 | 29 | public void setDate(Date date) { 30 | this.date = date; 31 | } 32 | 33 | public String[] getTags() { 34 | return tags; 35 | } 36 | 37 | public void setTags(String[] tags) { 38 | this.tags = tags; 39 | } 40 | 41 | public TestSource(Integer id, String name, Date date, String[] tags) { 42 | super.setId(id); 43 | this.name = name; 44 | this.date = date; 45 | this.tags = tags; 46 | } 47 | 48 | public TestSource() { 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 YuePeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/grammar/OrderBy.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.grammar; 2 | 3 | import xyz.erupt.linq.Linq; 4 | import xyz.erupt.linq.consts.OrderByDirection; 5 | import xyz.erupt.linq.lambda.SFunction; 6 | import xyz.erupt.linq.schema.OrderBySchema; 7 | import xyz.erupt.linq.util.Columns; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public interface OrderBy { 13 | 14 | Linq orderBy(List orderBySchemas); 15 | 16 | default Linq orderBy(SFunction column, OrderByDirection direction) { 17 | List orderBySchemas = new ArrayList<>(); 18 | orderBySchemas.add(new OrderBySchema(Columns.of(column), direction)); 19 | return orderBy(orderBySchemas); 20 | } 21 | 22 | default Linq orderBy(SFunction column) { 23 | return orderBy(column, OrderByDirection.ASC); 24 | } 25 | 26 | default Linq orderByAsc(SFunction column) { 27 | return orderBy(column); 28 | } 29 | 30 | default Linq orderByDesc(SFunction column) { 31 | return orderBy(column, OrderByDirection.DESC); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/student/Student.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.student; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class Student extends BaseModel{ 6 | 7 | private Long id; 8 | 9 | private String name; 10 | 11 | private Integer age; 12 | 13 | private LocalDateTime enrollmentTime; 14 | 15 | public Student(Long id, String name, Integer age, LocalDateTime enrollmentTime) { 16 | this.id = id; 17 | this.name = name; 18 | this.age = age; 19 | this.enrollmentTime = enrollmentTime; 20 | } 21 | 22 | public Long getId() { 23 | return id; 24 | } 25 | 26 | public void setId(Long id) { 27 | this.id = id; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public void setName(String name) { 35 | this.name = name; 36 | } 37 | 38 | public Integer getAge() { 39 | return age; 40 | } 41 | 42 | public void setAge(Integer age) { 43 | this.age = age; 44 | } 45 | 46 | public LocalDateTime getEnrollmentTime() { 47 | return enrollmentTime; 48 | } 49 | 50 | public void setEnrollmentTime(LocalDateTime enrollmentTime) { 51 | this.enrollmentTime = enrollmentTime; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/student/StudentScore.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.student; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class StudentScore { 6 | 7 | private Long studentId; 8 | 9 | private Long subjectId; 10 | 11 | private Integer score; 12 | 13 | private LocalDateTime createdAt; 14 | 15 | public StudentScore(Long studentId, Long subjectId, Integer score, LocalDateTime createdAt) { 16 | this.studentId = studentId; 17 | this.subjectId = subjectId; 18 | this.score = score; 19 | this.createdAt = createdAt; 20 | } 21 | 22 | public Long getStudentId() { 23 | return studentId; 24 | } 25 | 26 | public void setStudentId(Long studentId) { 27 | this.studentId = studentId; 28 | } 29 | 30 | public Long getSubjectId() { 31 | return subjectId; 32 | } 33 | 34 | public void setSubjectId(Long subjectId) { 35 | this.subjectId = subjectId; 36 | } 37 | 38 | public Integer getScore() { 39 | return score; 40 | } 41 | 42 | public void setScore(Integer score) { 43 | this.score = score; 44 | } 45 | 46 | public LocalDateTime getCreatedAt() { 47 | return createdAt; 48 | } 49 | 50 | public void setCreatedAt(LocalDateTime createdAt) { 51 | this.createdAt = createdAt; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/grammar/Select.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.grammar; 2 | 3 | import xyz.erupt.linq.Linq; 4 | import xyz.erupt.linq.lambda.SFunction; 5 | import xyz.erupt.linq.schema.Column; 6 | import xyz.erupt.linq.schema.Row; 7 | 8 | import java.util.function.BiFunction; 9 | import java.util.function.Function; 10 | 11 | public interface Select { 12 | 13 | Linq distinct(); 14 | 15 | Linq select(Column... columns); 16 | 17 | // select * 18 | Linq select(Class table); 19 | 20 | // select a, b, c 21 | Linq select(SFunction... columns); 22 | 23 | // select fn(x) 24 | Linq select(SFunction column, BiFunction convert); 25 | 26 | // select a as n 27 | Linq selectAs(SFunction column, String alias); 28 | 29 | // select a as fun 30 | Linq selectAs(SFunction column, SFunction alias); 31 | 32 | // select fn(x) as n 33 | Linq selectAs(SFunction column, BiFunction convert, String alias); 34 | 35 | // select fn(x) as fun 36 | Linq selectAs(SFunction column, BiFunction convert, SFunction alias); 37 | 38 | Linq selectRowAs(Function convert, String alias); 39 | 40 | Linq selectRowAs(Function convert, SFunction alias); 41 | 42 | Linq selectExclude(SFunction... columns); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/LambdaTest.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq; 2 | 3 | import org.junit.Test; 4 | import xyz.erupt.linq.data.source.TestSource; 5 | import xyz.erupt.linq.lambda.LambdaSee; 6 | import xyz.erupt.linq.lambda.Th; 7 | 8 | import java.math.BigDecimal; 9 | 10 | public class LambdaTest { 11 | 12 | @Test 13 | public void testLambdaInfo() { 14 | assert "id".equals(LambdaSee.info(TestSource::getId).getField()); 15 | assert "name".equals(LambdaSee.info(TestSource::getName).getField()); 16 | assert "getName".equals(LambdaSee.info(TestSource::getName).getMethod()); 17 | assert "is".equals(LambdaSee.info(Th::is).getField()); 18 | assert "is".equals(LambdaSee.info(Th::is).getMethod()); 19 | assert TestSource.class == LambdaSee.info(TestSource::getName).getClazz(); 20 | } 21 | 22 | @Test 23 | public void lambdaCacheTest() { 24 | for (int i = 0; i < 1000000; i++) { 25 | LambdaSee.info(TestSource::getName); 26 | LambdaSee.info(TestSource::name); 27 | LambdaSee.info(TestSource::getId); 28 | LambdaSee.info(TestSource::getDate); 29 | } 30 | } 31 | 32 | @Test 33 | public void isAssignableFrom(){ 34 | assert Number[].class.isAssignableFrom(Byte[].class); 35 | assert Number[].class.isAssignableFrom(Integer[].class); 36 | assert Number.class.isAssignableFrom(BigDecimal.class); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/grammar/Join.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.grammar; 2 | 3 | import xyz.erupt.linq.Linq; 4 | import xyz.erupt.linq.consts.JoinMethod; 5 | import xyz.erupt.linq.lambda.SFunction; 6 | import xyz.erupt.linq.schema.JoinSchema; 7 | import xyz.erupt.linq.schema.Row; 8 | 9 | import java.util.List; 10 | import java.util.function.BiFunction; 11 | 12 | public interface Join { 13 | 14 | Linq join(JoinSchema joinSchema); 15 | 16 | 17 | Linq join(JoinMethod joinMethod, List target, SFunction onL, SFunction onR); 18 | 19 | 20 | default Linq join(JoinMethod joinMethod, List target, BiFunction on) { 21 | return join(new JoinSchema<>(joinMethod, target, on)); 22 | } 23 | 24 | default Linq innerJoin(List t, SFunction lon, SFunction ron) { 25 | return this.join(JoinMethod.INNER, t, lon, ron); 26 | } 27 | 28 | default Linq leftJoin(List t, SFunction lon, SFunction ron) { 29 | return this.join(JoinMethod.LEFT, t, lon, ron); 30 | } 31 | 32 | default Linq rightJoin(List t, SFunction lon, SFunction ron) { 33 | return this.join(JoinMethod.RIGHT, t, lon, ron); 34 | } 35 | 36 | default Linq fullJoin(List t, SFunction lon, SFunction ron) { 37 | return this.join(JoinMethod.FULL, t, lon, ron); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/TestTo.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data; 2 | 3 | /** 4 | * @author YuePeng 5 | * date 2024/4/6 22:23 6 | */ 7 | public class TestTo { 8 | 9 | private Integer id; 10 | 11 | private String name; 12 | 13 | // private String name2; 14 | // private String name3; 15 | // private String name4; 16 | // private String name5; 17 | // private String name6; 18 | // private String name7; 19 | // private String name8; 20 | // private String name9; 21 | // private String name10; 22 | // private String name11; 23 | // private String name12; 24 | // private String name13; 25 | // private String name14; 26 | // private String name15; 27 | // private String name21; 28 | // private String name31; 29 | // private String name41; 30 | // private String name51; 31 | // private String name61; 32 | // private String name71; 33 | // private String name81; 34 | // private String name91; 35 | // private String name101; 36 | // private String name111; 37 | // private String name121; 38 | // private String name131; 39 | // private String name141; 40 | // private String name151; 41 | 42 | public TestTo() { 43 | } 44 | 45 | public TestTo(Integer id, String name) { 46 | this.id = id; 47 | this.name = name; 48 | } 49 | 50 | public Integer getId() { 51 | return id; 52 | } 53 | 54 | public String getName() { 55 | return name; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/student/StudentScoreAnalysis.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.student; 2 | 3 | public class StudentScoreAnalysis { 4 | 5 | private String name; 6 | 7 | private Integer totalScore; 8 | 9 | private Float avgScore; 10 | 11 | private Float maxScore; 12 | 13 | private Integer minScore; 14 | 15 | private Integer subjectCount; 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public void setName(String name) { 22 | this.name = name; 23 | } 24 | 25 | public Integer getTotalScore() { 26 | return totalScore; 27 | } 28 | 29 | public void setTotalScore(Integer totalScore) { 30 | this.totalScore = totalScore; 31 | } 32 | 33 | public Float getAvgScore() { 34 | return avgScore; 35 | } 36 | 37 | public void setAvgScore(Float avgScore) { 38 | this.avgScore = avgScore; 39 | } 40 | 41 | public Float getMaxScore() { 42 | return maxScore; 43 | } 44 | 45 | public void setMaxScore(Float maxScore) { 46 | this.maxScore = maxScore; 47 | } 48 | 49 | public Integer getMinScore() { 50 | return minScore; 51 | } 52 | 53 | public void setMinScore(Integer minScore) { 54 | this.minScore = minScore; 55 | } 56 | 57 | public Integer getSubjectCount() { 58 | return subjectCount; 59 | } 60 | 61 | public void setSubjectCount(Integer subjectCount) { 62 | this.subjectCount = subjectCount; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/util/ReflectField.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.util; 2 | 3 | import java.lang.reflect.Field; 4 | import java.util.*; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | /** 8 | * @author YuePeng 9 | * date 2024/4/3 20:55 10 | */ 11 | public class ReflectField { 12 | 13 | private static final Map, List> FIELDS_CACHE = new ConcurrentHashMap<>(); 14 | 15 | public static List getFields(Class clazz) throws SecurityException { 16 | return FIELDS_CACHE.computeIfAbsent(clazz, (it) -> getFieldsDirectly(it, true, true)); 17 | } 18 | 19 | public static List getFieldsDirectly(Class clazz, boolean distinctField, boolean withSuperClassFields) throws SecurityException { 20 | List allFields = new ArrayList<>(); 21 | Map distinctFieldMap = new HashMap<>(0); 22 | for (Class searchType = clazz; searchType != null; searchType = withSuperClassFields ? searchType.getSuperclass() : null) { 23 | if (distinctField) { 24 | for (Field field : searchType.getDeclaredFields()) { 25 | if (!distinctFieldMap.containsKey(field.getName())) { 26 | allFields.add(field); 27 | distinctFieldMap.put(field.getName(), null); 28 | } 29 | } 30 | } else { 31 | allFields.addAll(Arrays.asList(searchType.getDeclaredFields())); 32 | } 33 | } 34 | return allFields; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/source/TestSourceGroupByVo.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.source; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | public class TestSourceGroupByVo { 7 | 8 | private String name; 9 | 10 | private List ids; 11 | 12 | private Date max; 13 | 14 | private Date min; 15 | 16 | private Double avg; 17 | 18 | private Integer count; 19 | 20 | private Integer nameCount; 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | 30 | public List getIds() { 31 | return ids; 32 | } 33 | 34 | public void setIds(List ids) { 35 | this.ids = ids; 36 | } 37 | 38 | public Date getMax() { 39 | return max; 40 | } 41 | 42 | public void setMax(Date max) { 43 | this.max = max; 44 | } 45 | 46 | public Date getMin() { 47 | return min; 48 | } 49 | 50 | public void setMin(Date min) { 51 | this.min = min; 52 | } 53 | 54 | public Double getAvg() { 55 | return avg; 56 | } 57 | 58 | public void setAvg(Double avg) { 59 | this.avg = avg; 60 | } 61 | 62 | public Integer getCount() { 63 | return count; 64 | } 65 | 66 | public void setCount(Integer count) { 67 | this.count = count; 68 | } 69 | 70 | public Integer getNameCount() { 71 | return nameCount; 72 | } 73 | 74 | public void setNameCount(Integer nameCount) { 75 | this.nameCount = nameCount; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/PerformanceTest.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import xyz.erupt.linq.data.TestTo; 6 | import xyz.erupt.linq.lambda.Th; 7 | import xyz.erupt.linq.util.Columns; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | /** 14 | * Performance test 15 | * 16 | * @author YuePeng 17 | * date 2024/4/3 22:57 18 | */ 19 | public class PerformanceTest { 20 | 21 | private final List testTos = new ArrayList<>(); 22 | 23 | private final List integers = new ArrayList<>(); 24 | 25 | @Before 26 | public void before() { 27 | for (int i = 0; i < 10000000; i++) { 28 | testTos.add(new TestTo(i, String.valueOf((char) i))); 29 | integers.add(i); 30 | } 31 | } 32 | 33 | @Test 34 | public void javaSelectTest() { 35 | testTos.stream().map(TestTo::getName).collect(Collectors.toList()); 36 | } 37 | 38 | @Test 39 | public void linqSelectTest() { 40 | Linq.from(testTos).select(TestTo::getName, TestTo::getId).write(TestTo.class); 41 | } 42 | 43 | @Test 44 | public void linqSelectMapTest() { 45 | Linq.from(testTos).select(TestTo::getName, TestTo::getId).orderBy(TestTo::getId).writeMap(); 46 | } 47 | 48 | 49 | @Test 50 | public void linqSumTest() { 51 | Linq.from(testTos).select(Columns.avg(TestTo::getId, "avg")).writeOne(Integer.class); 52 | } 53 | 54 | @Test 55 | public void linqSimpleSelectTest() { 56 | Linq.from(integers.toArray()).select(Th::is).write(Integer.class); 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/schema/JoinSchema.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.schema; 2 | 3 | import xyz.erupt.linq.consts.JoinExchange; 4 | import xyz.erupt.linq.consts.JoinMethod; 5 | import xyz.erupt.linq.lambda.LambdaSee; 6 | import xyz.erupt.linq.lambda.SFunction; 7 | 8 | import java.util.List; 9 | import java.util.function.BiFunction; 10 | 11 | public class JoinSchema { 12 | 13 | private final JoinMethod joinMethod; 14 | 15 | private final JoinExchange joinExchange; 16 | 17 | private final List target; 18 | 19 | private final Class clazz; 20 | 21 | private BiFunction on; 22 | 23 | private SFunction lon; 24 | 25 | 26 | private SFunction ron; 27 | 28 | 29 | public JoinSchema(JoinMethod joinMethod, List target, SFunction lon, SFunction ron) { 30 | this.joinMethod = joinMethod; 31 | this.target = target; 32 | this.lon = lon; 33 | this.ron = ron; 34 | this.clazz = (Class) LambdaSee.info(lon).getClazz(); 35 | this.joinExchange = JoinExchange.HASH; 36 | } 37 | 38 | public JoinSchema(JoinMethod joinMethod, List target, BiFunction on) { 39 | this.joinMethod = joinMethod; 40 | this.target = target; 41 | this.on = on; 42 | this.clazz = (Class) target.getClass().getGenericSuperclass().getClass(); 43 | this.joinExchange = JoinExchange.NESTED_LOOP; 44 | } 45 | 46 | public JoinMethod getJoinMethod() { 47 | return joinMethod; 48 | } 49 | 50 | public JoinExchange getJoinExchange() { 51 | return joinExchange; 52 | } 53 | 54 | public List getTarget() { 55 | return target; 56 | } 57 | 58 | public Class getClazz() { 59 | return clazz; 60 | } 61 | 62 | public SFunction getLon() { 63 | return lon; 64 | } 65 | 66 | public SFunction getRon() { 67 | return ron; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/DatasetCustomerTest.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.reflect.TypeToken; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import xyz.erupt.linq.data.customer.CustomerChurnModel; 8 | import xyz.erupt.linq.data.customer.CustomerInfo; 9 | import xyz.erupt.linq.util.Columns; 10 | 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.InputStreamReader; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | public class DatasetCustomerTest { 19 | 20 | private List dataset; 21 | 22 | private final List customerInfos = new ArrayList<>(); 23 | 24 | @Before 25 | public void before() throws IOException { 26 | try (InputStream stream = DatasetCustomerTest.class.getResourceAsStream("/CustomerChurnModelDataSet.json")) { 27 | if (stream != null) { 28 | dataset = new Gson().fromJson(new InputStreamReader(stream), new TypeToken>() { 29 | }.getType()); 30 | } 31 | } 32 | customerInfos.add(new CustomerInfo(15634602L, "MM")); 33 | customerInfos.add(new CustomerInfo(15634602L, "KK")); 34 | System.out.println("dataset length:" + dataset.size()); 35 | } 36 | 37 | @Test 38 | public void allData() { 39 | List> result = Linq.from(dataset) 40 | .select(CustomerChurnModel::getAge, CustomerChurnModel::getGender) 41 | .writeMap(); 42 | System.out.println(result.size()); 43 | } 44 | 45 | @Test 46 | public void query() { 47 | List> result = Linq.from(dataset) 48 | .distinct() 49 | .leftJoin(customerInfos, CustomerInfo::getCustomerId, CustomerChurnModel::getCustomerId) 50 | .select(CustomerChurnModel::getAge, CustomerChurnModel::getGender) 51 | .select(CustomerInfo::getNickName) 52 | .like(CustomerChurnModel::getGender, "Male") 53 | .between(CustomerChurnModel::getAge, 10, 20) 54 | .eq(CustomerChurnModel::getExited, true) 55 | .groupBy(CustomerChurnModel::getAge, CustomerChurnModel::getGender) 56 | .orderBy(CustomerChurnModel::getAge) 57 | .writeMap(); 58 | System.out.println("result size " + result.size()); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/schema/Dql.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.schema; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Function; 6 | 7 | public class Dql { 8 | 9 | private boolean distinct = false; 10 | 11 | private List from; 12 | 13 | // columns definition 14 | private final List columns = new ArrayList() { 15 | @Override 16 | public boolean add(Column column) { 17 | super.remove(column); 18 | return super.add(column); 19 | } 20 | }; 21 | 22 | // json definition 23 | private final List> joinSchemas = new ArrayList<>(); 24 | 25 | // where definition 26 | private final List> wheres = new ArrayList<>(); 27 | 28 | // group by definition 29 | private final List groupBys = new ArrayList<>(); 30 | 31 | // having definition 32 | private final List> having = new ArrayList<>(); 33 | 34 | // order by definition 35 | private final List orderBys = new ArrayList<>(); 36 | 37 | 38 | private Integer limit = null; 39 | 40 | private Integer offset = null; 41 | 42 | 43 | public boolean isDistinct() { 44 | return distinct; 45 | } 46 | 47 | public List getFrom() { 48 | return from; 49 | } 50 | 51 | public void setFrom(List from) { 52 | this.from = from; 53 | } 54 | 55 | public void setDistinct(boolean distinct) { 56 | this.distinct = distinct; 57 | } 58 | 59 | public List getColumns() { 60 | return columns; 61 | } 62 | 63 | public List> getWheres() { 64 | return wheres; 65 | } 66 | 67 | public List> getJoinSchemas() { 68 | return joinSchemas; 69 | } 70 | 71 | public List getGroupBys() { 72 | return groupBys; 73 | } 74 | 75 | public List> getHaving() { 76 | return having; 77 | } 78 | 79 | public Integer getLimit() { 80 | return limit; 81 | } 82 | 83 | public void setLimit(Integer limit) { 84 | this.limit = limit; 85 | } 86 | 87 | public Integer getOffset() { 88 | return offset; 89 | } 90 | 91 | public void setOffset(Integer offset) { 92 | this.offset = offset; 93 | } 94 | 95 | public List getOrderBys() { 96 | return orderBys; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/schema/Column.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.schema; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | import java.util.function.Function; 6 | 7 | public class Column { 8 | 9 | private Class table; 10 | 11 | private String field; 12 | 13 | private String alias; 14 | 15 | // Column value convert handler 16 | private Function rowConvert; 17 | 18 | // The handler functions listed in the grouping scenario 19 | private Function, Object> groupByFun; 20 | 21 | public Column() { 22 | } 23 | 24 | public Column(Class table, String field, String alias) { 25 | this.table = table; 26 | this.field = field; 27 | this.alias = alias; 28 | } 29 | 30 | // Get the original column information 31 | public Column getRawColumn() { 32 | Column column = new Column(this.table, this.field, this.field); 33 | column.setGroupByFun(this.getGroupByFun()); 34 | column.setRowConvert(this.getRowConvert()); 35 | return column; 36 | } 37 | 38 | public Class getTable() { 39 | return table; 40 | } 41 | 42 | public void setTable(Class table) { 43 | this.table = table; 44 | } 45 | 46 | public String getField() { 47 | return field; 48 | } 49 | 50 | public void setField(String field) { 51 | this.field = field; 52 | } 53 | 54 | public String getAlias() { 55 | return alias; 56 | } 57 | 58 | public void setAlias(String alias) { 59 | this.alias = alias; 60 | } 61 | 62 | public Function, Object> getGroupByFun() { 63 | return groupByFun; 64 | } 65 | 66 | public void setGroupByFun(Function, Object> groupByFun) { 67 | this.groupByFun = groupByFun; 68 | } 69 | 70 | public Function getRowConvert() { 71 | return rowConvert; 72 | } 73 | 74 | public void setRowConvert(Function rowConvert) { 75 | this.rowConvert = rowConvert; 76 | } 77 | 78 | @Override 79 | public boolean equals(Object o) { 80 | if (this == o) return true; 81 | if (o == null || getClass() != o.getClass()) return false; 82 | Column column = (Column) o; 83 | return Objects.equals(table.getName(), column.table.getName()) && Objects.equals(field, column.field) && Objects.equals(alias, column.alias); 84 | } 85 | 86 | @Override 87 | public int hashCode() { 88 | return Objects.hash(table.getName(), field, alias); 89 | } 90 | 91 | @Override 92 | public String toString() { 93 | return table.getName() + "." + field + " as " + alias; 94 | } 95 | 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/lambda/LambdaSee.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.lambda; 2 | 3 | import xyz.erupt.linq.exception.LinqException; 4 | 5 | import java.beans.Introspector; 6 | import java.lang.invoke.SerializedLambda; 7 | import java.lang.reflect.Method; 8 | import java.util.Map; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | public class LambdaSee { 14 | 15 | private static final String GET = "get", IS = "is", WRITE_REPLACE = "writeReplace"; 16 | 17 | private static final Pattern CLASS_TYPE_PATTERN = Pattern.compile("\\(L(.*);\\).*"); 18 | 19 | private static final Map, LambdaInfo> S_FUNCTION_CACHE = new ConcurrentHashMap<>(); 20 | 21 | public static String field(SFunction func) { 22 | return info(func).getField(); 23 | } 24 | 25 | public static String method(SFunction func) { 26 | return info(func).getMethod(); 27 | } 28 | 29 | public static LambdaInfo info(SFunction func) { 30 | try { 31 | if (S_FUNCTION_CACHE.containsKey(func)) { 32 | return S_FUNCTION_CACHE.get(func); 33 | } else synchronized (LambdaSee.class) { 34 | if (S_FUNCTION_CACHE.containsKey(func)) return S_FUNCTION_CACHE.get(func); 35 | } 36 | if (!func.getClass().isSynthetic()) 37 | throw new LinqException("Synthetic classes produced by non-lambda expressions"); 38 | Method method = func.getClass().getDeclaredMethod(WRITE_REPLACE); 39 | method.setAccessible(true); 40 | SerializedLambda serializedLambda = (SerializedLambda) method.invoke(func); 41 | Matcher matcher = CLASS_TYPE_PATTERN.matcher(serializedLambda.getInstantiatedMethodType()); 42 | if (!matcher.find() || matcher.groupCount() != 1) 43 | throw new RuntimeException("Failed to get Lambda information"); 44 | Class clazz = Class.forName(matcher.group(1).replace("/", ".")); 45 | LambdaInfo lambdaInfo = getserializedLambdaInfo(serializedLambda, clazz); 46 | S_FUNCTION_CACHE.put(func, lambdaInfo); 47 | return lambdaInfo; 48 | } catch (ReflectiveOperationException e) { 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | 53 | private static LambdaInfo getserializedLambdaInfo(SerializedLambda serializedLambda, Class clazz) { 54 | String methodName = serializedLambda.getImplMethodName(); 55 | if (clazz.isInterface()) { 56 | return new LambdaInfo(clazz, methodName, null); 57 | } else { 58 | String field = methodName; 59 | if (methodName.startsWith(GET) && methodName.length() != GET.length()) 60 | field = methodName.substring(GET.length()); 61 | if (methodName.startsWith(IS) && methodName.length() != IS.length()) 62 | field = methodName.substring(IS.length()); 63 | return new LambdaInfo(clazz, methodName, Introspector.decapitalize(field)); 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/data/customer/CustomerChurnModel.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.data.customer; 2 | 3 | public class CustomerChurnModel { 4 | 5 | private Integer rowNumber; 6 | 7 | private Long customerId; 8 | 9 | private String surname; 10 | 11 | private Integer creditScore; 12 | 13 | private String geography; 14 | 15 | private String gender; 16 | 17 | private Integer age; 18 | 19 | private Integer tenure; 20 | 21 | private Float balance; 22 | 23 | private Integer numOfProducts; 24 | 25 | private Boolean hasCrCard; 26 | 27 | private Boolean isActiveMember; 28 | 29 | private Float estimatedSalary; 30 | 31 | private Boolean exited; 32 | 33 | 34 | public Integer getRowNumber() { 35 | return rowNumber; 36 | } 37 | 38 | public void setRowNumber(Integer rowNumber) { 39 | this.rowNumber = rowNumber; 40 | } 41 | 42 | public Long getCustomerId() { 43 | return customerId; 44 | } 45 | 46 | public void setCustomerId(Long customerId) { 47 | this.customerId = customerId; 48 | } 49 | 50 | public String getSurname() { 51 | return surname; 52 | } 53 | 54 | public void setSurname(String surname) { 55 | this.surname = surname; 56 | } 57 | 58 | public Integer getCreditScore() { 59 | return creditScore; 60 | } 61 | 62 | public void setCreditScore(Integer creditScore) { 63 | this.creditScore = creditScore; 64 | } 65 | 66 | public String getGeography() { 67 | return geography; 68 | } 69 | 70 | public void setGeography(String geography) { 71 | this.geography = geography; 72 | } 73 | 74 | public String getGender() { 75 | return gender; 76 | } 77 | 78 | public void setGender(String gender) { 79 | this.gender = gender; 80 | } 81 | 82 | public Integer getAge() { 83 | return age; 84 | } 85 | 86 | public void setAge(Integer age) { 87 | this.age = age; 88 | } 89 | 90 | public Integer getTenure() { 91 | return tenure; 92 | } 93 | 94 | public void setTenure(Integer tenure) { 95 | this.tenure = tenure; 96 | } 97 | 98 | public Float getBalance() { 99 | return balance; 100 | } 101 | 102 | public void setBalance(Float balance) { 103 | this.balance = balance; 104 | } 105 | 106 | public Integer getNumOfProducts() { 107 | return numOfProducts; 108 | } 109 | 110 | public void setNumOfProducts(Integer numOfProducts) { 111 | this.numOfProducts = numOfProducts; 112 | } 113 | 114 | public Boolean getHasCrCard() { 115 | return hasCrCard; 116 | } 117 | 118 | public void setHasCrCard(Boolean hasCrCard) { 119 | this.hasCrCard = hasCrCard; 120 | } 121 | 122 | public Boolean getActiveMember() { 123 | return isActiveMember; 124 | } 125 | 126 | public void setActiveMember(Boolean activeMember) { 127 | isActiveMember = activeMember; 128 | } 129 | 130 | public Float getEstimatedSalary() { 131 | return estimatedSalary; 132 | } 133 | 134 | public void setEstimatedSalary(Float estimatedSalary) { 135 | this.estimatedSalary = estimatedSalary; 136 | } 137 | 138 | public Boolean getExited() { 139 | return exited; 140 | } 141 | 142 | public void setExited(Boolean exited) { 143 | this.exited = exited; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/grammar/Where.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.grammar; 2 | 3 | import xyz.erupt.linq.Linq; 4 | import xyz.erupt.linq.consts.CompareSymbol; 5 | import xyz.erupt.linq.lambda.SFunction; 6 | import xyz.erupt.linq.schema.Column; 7 | import xyz.erupt.linq.schema.Row; 8 | import xyz.erupt.linq.util.Columns; 9 | import xyz.erupt.linq.util.CompareUtil; 10 | 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.function.Function; 15 | 16 | public interface Where { 17 | 18 | // equals 19 | default Linq eq(SFunction column, Object value) { 20 | return where(column, f -> null != value && value.equals(f)); 21 | } 22 | 23 | // not equals 24 | default Linq ne(SFunction column, Object value) { 25 | return where(column, f -> null != value && !value.equals(f)); 26 | } 27 | 28 | 29 | // :val >= start and :val <= end 30 | default Linq between(SFunction column, Object start, Object end) { 31 | return where(column, row -> CompareUtil.compare(row, start, CompareSymbol.GTE) && 32 | CompareUtil.compare(row, end, CompareSymbol.LTE)); 33 | } 34 | 35 | // > 36 | default Linq gt(SFunction column, Object value) { 37 | return where(column, f -> CompareUtil.compare(f, value, CompareSymbol.GT)); 38 | } 39 | 40 | // < 41 | default Linq lt(SFunction column, Object value) { 42 | return where(column, f -> CompareUtil.compare(f, value, CompareSymbol.LT)); 43 | } 44 | 45 | // >= 46 | default Linq gte(SFunction column, Object value) { 47 | return where(column, f -> CompareUtil.compare(f, value, CompareSymbol.GTE)); 48 | } 49 | 50 | // <= 51 | default Linq lte(SFunction column, Object value) { 52 | return where(column, f -> CompareUtil.compare(f, value, CompareSymbol.LTE)); 53 | } 54 | 55 | default Linq like(SFunction column, Object value) { 56 | return where(column, f -> f != null && value != null && f.toString().contains(value.toString())); 57 | } 58 | 59 | default Linq in(SFunction column, Object... value) { 60 | return where(column, f -> f != null && Arrays.stream(value).anyMatch(it -> null != it && it.equals(f))); 61 | } 62 | 63 | default Linq in(SFunction column, List value) { 64 | return in(column, value.toArray()); 65 | } 66 | 67 | default Linq notIn(SFunction column, Object... value) { 68 | return where(column, f -> f != null && Arrays.stream(value).noneMatch(it -> null != it && it.equals(f))); 69 | } 70 | 71 | default Linq notIn(SFunction column, List value) { 72 | return notIn(column, value.toArray()); 73 | } 74 | 75 | 76 | default Linq isNull(SFunction column) { 77 | return where(column, Objects::isNull); 78 | } 79 | 80 | default Linq isNotNull(SFunction column) { 81 | return where(column, Objects::nonNull); 82 | } 83 | 84 | default Linq isBlank(SFunction column) { 85 | return where(column, f -> f == null || f.toString().trim().isEmpty()); 86 | } 87 | 88 | default Linq isNotBlank(SFunction column) { 89 | return where(column, f -> f != null && !f.toString().trim().isEmpty()); 90 | } 91 | 92 | default Linq where(SFunction column, Function process) { 93 | return where(f -> process.apply(f.get(column))); 94 | } 95 | 96 | Linq where(Function fun); 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/grammar/Write.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.grammar; 2 | 3 | import xyz.erupt.linq.engine.Engine; 4 | import xyz.erupt.linq.exception.LinqException; 5 | import xyz.erupt.linq.schema.Column; 6 | import xyz.erupt.linq.schema.Dql; 7 | import xyz.erupt.linq.schema.Row; 8 | import xyz.erupt.linq.util.RowUtil; 9 | 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | public interface Write { 16 | 17 | String MULTI_VAL_ERROR = "Expected one result (or null) to be returned by writeOne(), but found: "; 18 | 19 | Engine wEngine(); 20 | 21 | Dql wDQL(); 22 | 23 | default List write(Class clazz) { 24 | wEngine().preprocessor(this.wDQL()); 25 | List table = wEngine().query(this.wDQL()); 26 | if (table.isEmpty()) { 27 | return new ArrayList<>(); 28 | } 29 | 30 | // Optimization: for simple types with single column, directly extract values 31 | Row firstRow = table.get(0); 32 | int firstRowSize = firstRow.size(); 33 | if (firstRowSize == 1) { 34 | // Cache the first column to avoid repeated iterator creation 35 | Column firstColumn = null; 36 | Object firstValue = null; 37 | for (Map.Entry entry : firstRow.entrySet()) { 38 | firstColumn = entry.getKey(); 39 | firstValue = entry.getValue(); 40 | break; // Only need first entry 41 | } 42 | 43 | // Check if it's a simple type that can be directly cast 44 | if (firstValue != null && clazz.isAssignableFrom(firstValue.getClass())) { 45 | List result = new ArrayList<>(Math.min(table.size(), 10000)); 46 | for (Row row : table) { 47 | result.add((T) row.get(firstColumn)); 48 | } 49 | return result; 50 | } 51 | // Check if target is a simple type - use array access for better performance 52 | Class[] simpleClasses = RowUtil.SIMPLE_CLASS; 53 | for (Class simpleClass : simpleClasses) { 54 | if (simpleClass.isAssignableFrom(clazz)) { 55 | List result = new ArrayList<>(Math.min(table.size(), 10000)); 56 | for (Row row : table) { 57 | result.add(RowUtil.rowToObject(row, clazz)); 58 | } 59 | return result; 60 | } 61 | } 62 | } 63 | List result = new ArrayList<>(Math.min(table.size(), 10000)); 64 | for (Row row : table) { 65 | result.add(RowUtil.rowToObject(row, clazz)); 66 | } 67 | return result; 68 | } 69 | 70 | default T writeOne(Class clazz) { 71 | wEngine().preprocessor(this.wDQL()); 72 | List result = wEngine().query(this.wDQL()); 73 | if (result.isEmpty()) { 74 | return null; 75 | } else if (result.size() == 1) { 76 | return RowUtil.rowToObject(result.get(0), clazz); 77 | } else { 78 | throw new LinqException(MULTI_VAL_ERROR + result.size()); 79 | } 80 | } 81 | 82 | default List> writeMap() { 83 | wEngine().preprocessor(this.wDQL()); 84 | List table = wEngine().query(this.wDQL()); 85 | List> result = new ArrayList<>(Math.min(table.size(), 10000)); 86 | int columnNum = this.wDQL().getColumns().size(); 87 | for (Row row : table) { 88 | Map $map = new HashMap<>(columnNum); 89 | result.add($map); 90 | row.forEach((k, v) -> $map.put(k.getAlias(), v)); 91 | } 92 | return result; 93 | } 94 | 95 | default Map writeMapOne() { 96 | List> result = writeMap(); 97 | if (result.isEmpty()) { 98 | return null; 99 | } else if (result.size() == 1) { 100 | return result.get(0); 101 | } else { 102 | throw new LinqException(MULTI_VAL_ERROR + result.size()); 103 | } 104 | } 105 | 106 | } 107 | 108 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/DatasetStudentTest.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import xyz.erupt.linq.consts.OrderByDirection; 6 | import xyz.erupt.linq.data.student.*; 7 | import xyz.erupt.linq.util.Columns; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class DatasetStudentTest { 14 | 15 | private final List students = new ArrayList<>(); 16 | 17 | private final List studentScores = new ArrayList<>(); 18 | 19 | private final List studentSubjects = new ArrayList<>(); 20 | 21 | 22 | @Before 23 | public void before() { 24 | students.add(new Student(1L, "Genevieve", 18, LocalDateTime.now().minusYears(3))); 25 | students.add(new Student(2L, "Liz", 18, LocalDateTime.now().minusYears(5))); 26 | students.add(new Student(3L, "Berg", 33, LocalDateTime.now().minusYears(9))); 27 | students.add(new Student(4L, "Milo", 21, LocalDateTime.now().minusYears(2))); 28 | students.add(new Student(5L, "Thanos", 19, LocalDateTime.now().minusYears(7))); 29 | 30 | studentSubjects.add(new StudentSubject(1L, "math")); 31 | studentSubjects.add(new StudentSubject(2L, "physics")); 32 | studentSubjects.add(new StudentSubject(3L, "computer")); 33 | 34 | studentScores.add(new StudentScore(1L, 1L, 50, LocalDateTime.now().minusDays(90))); 35 | studentScores.add(new StudentScore(1L, 2L, 70, LocalDateTime.now().minusDays(232))); 36 | studentScores.add(new StudentScore(1L, 3L, 76, LocalDateTime.now().minusDays(21))); 37 | studentScores.add(new StudentScore(2L, 1L, 11, LocalDateTime.now().minusDays(90))); 38 | studentScores.add(new StudentScore(2L, 2L, 54, LocalDateTime.now().minusDays(33))); 39 | studentScores.add(new StudentScore(2L, 3L, 76, LocalDateTime.now().minusDays(123))); 40 | studentScores.add(new StudentScore(3L, 1L, 43, LocalDateTime.now().minusDays(4))); 41 | studentScores.add(new StudentScore(3L, 2L, 90, LocalDateTime.now().minusDays(34))); 42 | studentScores.add(new StudentScore(3L, 3L, 54, LocalDateTime.now().minusDays(55))); 43 | studentScores.add(new StudentScore(4L, 1L, 50, LocalDateTime.now().minusDays(655))); 44 | studentScores.add(new StudentScore(4L, 2L, 102, LocalDateTime.now().minusDays(12))); 45 | studentScores.add(new StudentScore(4L, 3L, 121, LocalDateTime.now().minusDays(22))); 46 | 47 | studentScores.add(new StudentScore(5L, 3L, 8, LocalDateTime.now().minusDays(22))); 48 | } 49 | 50 | @Test 51 | public void joinTest() { 52 | List studentVos = Linq.from(students) 53 | .leftJoin(studentScores, StudentScore::getStudentId, Student::getId) 54 | .leftJoin(studentSubjects, StudentSubject::getId, StudentScore::getSubjectId) 55 | .selectAs(Student::getName, (row, name) -> name + row.get(Student::getAge), StudentVo::getName) 56 | .selectAs(StudentSubject::getName, StudentVo::getSubjectName) 57 | .selectAs(StudentScore::getScore, StudentVo::getScore) 58 | .write(StudentVo.class); 59 | assert studentScores.size() == studentVos.size(); 60 | } 61 | 62 | @Test 63 | public void groupByTest() { 64 | Linq.from(students) 65 | .innerJoin(studentScores, StudentScore::getStudentId, Student::getId) 66 | .innerJoin(studentSubjects, StudentSubject::getId, StudentScore::getSubjectId) 67 | .groupBy(Student::getName, Student::getAge) 68 | .select( 69 | Columns.of(Student::getName, StudentScoreAnalysis::getName), 70 | Columns.avg(StudentScore::getScore, StudentScoreAnalysis::getAvgScore), 71 | Columns.max(StudentScore::getScore, StudentScoreAnalysis::getMaxScore), 72 | Columns.min(StudentScore::getScore, StudentScoreAnalysis::getMinScore), 73 | Columns.sum(StudentScore::getScore, StudentScoreAnalysis::getTotalScore), 74 | Columns.count(StudentScoreAnalysis::getSubjectCount) 75 | ) 76 | .write(StudentScoreAnalysis.class); 77 | } 78 | 79 | @Test 80 | public void allAnalysisTest() { 81 | Integer r = Linq.from(students) 82 | .innerJoin(studentScores, StudentScore::getStudentId, Student::getId) 83 | .select(Columns.count(StudentScoreAnalysis::getSubjectCount)) 84 | .writeOne(Integer.class); 85 | System.out.println(r); 86 | } 87 | 88 | @Test 89 | public void orderByTest() { 90 | Object s = Linq.from(students) 91 | .orderBy(Student::getAge, OrderByDirection.DESC) 92 | .orderBy(Student::getId, OrderByDirection.DESC) 93 | .select(Student.class) 94 | .writeMap(); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | xyz.erupt 8 | linq.j 9 | 0.1.1 10 | Linq.j 11 | 12 | Linq.J in Java , Lambda Language Integrated Query library 13 | https://github.com/erupts/Linq.J 14 | 15 | 16 | 8 17 | UTF-8 18 | 19 | 20 | 21 | 22 | junit 23 | junit 24 | 4.13.2 25 | test 26 | 27 | 28 | com.google.code.gson 29 | gson 30 | 2.9.1 31 | test 32 | 33 | 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-compiler-plugin 40 | 3.1 41 | 42 | ${java.version} 43 | ${java.version} 44 | utf-8 45 | true 46 | true 47 | 48 | -parameters 49 | 50 | 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-scm-plugin 55 | 1.8.1 56 | 57 | 58 | org.sonatype.central 59 | central-publishing-maven-plugin 60 | 0.7.0 61 | true 62 | 63 | central 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-source-plugin 69 | 2.2.1 70 | 71 | 72 | attach-sources 73 | package 74 | 75 | jar-no-fork 76 | 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-javadoc-plugin 83 | 3.0.0 84 | 85 | 86 | -Xdoclint:none 87 | 88 | 89 | 90 | 91 | package 92 | 93 | jar 94 | 95 | 96 | 97 | 98 | 99 | 100 | org.apache.maven.plugins 101 | maven-gpg-plugin 102 | 1.6 103 | 104 | 105 | sign-artifacts 106 | verify 107 | 108 | sign 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | The MIT License (MIT) 119 | https://github.com/erupts/Linq.J/blob/main/LICENSE 120 | 121 | 122 | 123 | master 124 | git@github.com:erupts/Linq.J.git 125 | scm:git:git@github.com:erupts/Linq.J.git 126 | scm:git:git@github.com:erupts/Linq.J.git 127 | 128 | 129 | 130 | YuePeng 131 | erupts@126.com 132 | 133 | owner 134 | 135 | +8 136 | 137 | 138 | 139 | 140 | 141 | disable-javadoc-doclint 142 | 143 | [1.8,) 144 | 145 | 146 | -Xdoclint:none 147 | ${java.home}/bin/javadoc 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # Linq.J 基于内存的对象查询语言 2 | 中文 / [English](./README.md) 3 | 4 | `Java 中使用类似 C# 的 Linq 能力 ` [C# Linq](https://learn.microsoft.com/zh-cn/dotnet/csharp/linq/) 5 | 6 |

7 | Erupt Framework 8 | maven-central 9 | jdk 8+ 10 | license Apache 2.0 11 | GitCode star 12 | GitEE star 13 | GitHub stars 14 |

15 | 16 | 17 | ### Linq 是面向对象的 sql,linq实际上是对内存中数据的查询,使开发人员能够更容易地编写查询。这些查询表达式看起来很像SQL 18 | 19 | > 可以通过最少的代码对数据源进行关联、筛选、排序和分组等操作。这些操作可以在单个查询中组合起来,以获得更复杂的结果,无需for循环与if分支操作数据,内置查询引擎性能卓越 20 | 21 | #### 允许编写Java代码以查询数据库相同的方式操作内存数据,例如 22 | - List 集合、Array 数组中的数据 23 | - CSV、XML、JSON 文档数据集 24 | - Stream、File 流 25 | - Redis、SQL、MongoDB数据库结果集 26 | 27 | #### 应用场景 28 | - 分布式开发时 Feign / Dubbo / gRPC / WebService 等 RPC 结果的关联/筛选/聚合/分页 29 | - 语义化对象转换与映射,团队协作代码更清晰敏捷 30 | - 异构系统数据的内存计算 31 | - 使用代码组织 SQL 结果数据 32 | - 跨数据源的联邦访问 33 | 34 | #### 操作语法 35 | `From` `Select` `Distinct`、`Join`、`Where`、`Group By`、`Order By`、`Limit`、`Offset`、`...` 36 | 37 | #### 使用提示 38 | ⚠️ 注意:操作的对象字段必须存在 get 方法便于 lambda 查找,建议配合 **Lombok** 的 @Getter 注解快速创建字段的 get 访问 39 | 40 | #### 使用方法 41 | ```xml 42 | 43 | 44 | xyz.erupt 45 | linq.j 46 | LATEST 47 | 48 | ``` 49 | 50 | #### Example 1 51 | ```javascript 52 | var strings = Linq.from("C", "A", "B", "B").gt(Th::is, "A").orderByDesc(Th::is).write(String.class); 53 | // [C, B, B] 54 | 55 | var integers = Linq.from(1, 2, 3, 7, 6, 5).orderBy(Th::is).write(Integer.class); 56 | // [1, 2, 3, 5, 6, 7] 57 | 58 | var name = Linq.from(data) 59 | // left join 60 | .innerJoin(target, Target::getId, Data::getId) 61 | // where like 62 | .like(Data::getName, "a") 63 | // select name 64 | .select(Data::getName) 65 | // distinct 66 | .distinct() 67 | // order by 68 | .orderBy(Data::getName) 69 | .write(String.class); 70 | 71 | ``` 72 | 73 | #### Example 2 74 | ```java 75 | public class ObjectQuery{ 76 | 77 | private final List source = http.get("https://gw.alipayobjects.com/os/antfincdn/v6MvZBUBsQ/column-data.json"); 78 | 79 | private final List target = mongodb.query("db.target.find()"); 80 | 81 | /** 82 | * select demo 83 | */ 84 | public void select(){ 85 | // select * 86 | Linq.from(source).select(TestSource.class); 87 | // select a, b, c 88 | Linq.from(source) 89 | .select(TestSource::getName, TestSource::getDate, TestSource::getTags) 90 | .select(TestSource::getTags, "tag2") // alias 91 | .select(Columns.ofx(TestSource::getId, id -> id + "xxx")); // value convert 92 | // select count(*), sum(id), max(id) 93 | Linq.from(source) 94 | .select(Columns.count("count")) 95 | .select(Columns.sum(TestSource::getId, "sum")) 96 | .select(Columns.max(TestSource::getId, "max")); 97 | } 98 | 99 | 100 | /** 101 | * join demo 102 | */ 103 | public void join(){ 104 | // left join 105 | Linq.from(source).leftJoin(target, TestSourceExt::getId, TestSource::getId) 106 | .select(TestSource.class) 107 | .select(TestSourceExt::getName) 108 | .select(TestSourceExt2::getValue); 109 | // right join 110 | Linq.from(source).rightJoin(target, TestSourceExt::getId, TestSource::getId); 111 | // inner join 112 | Linq.from(source).innerJoin(target, TestSourceExt::getId, TestSource::getId); 113 | // full join 114 | Linq.from(source).fullJoin(target, TestSourceExt::getId, TestSource::getId); 115 | } 116 | 117 | 118 | /** 119 | * where demo 120 | */ 121 | public void where() { 122 | // = 123 | Linq.from(source).eq(TestSource::getName, "Thanos").select(Columns.count(countAlias)).writeOne(Integer.class); 124 | // >=:lval and <=:rval 125 | Linq.from(source).between(TestSource::getId, 1, 3); 126 | // in (x,x,x) 127 | Linq.from(source).in(TestSource::getId, 1, 2, 3); 128 | // like '%x%' 129 | Linq.from(source).like(TestSource::getName, "a"); 130 | // is null 131 | Linq.from(source).isNull(TestSource::getId); 132 | 133 | // customer single field where 134 | Linq.from(source).where(TestSource::getId, id -> id >= 5); 135 | 136 | // customer condition or multi field 137 | Linq.from(source).where(data -> { 138 | String name = data.get(TestSource::getName); 139 | Integer age = (Integer)data.get(TestSource::getAge); 140 | // name = 'xxx' or age > 10 141 | return "xxx".equals(name) || age > 10; 142 | }); 143 | } 144 | 145 | 146 | /** 147 | * group by demo 148 | */ 149 | public void groupBy(){ 150 | Linq.from(source) 151 | .groupBy(TestSource::getName) 152 | .select( 153 | Columns.of(TestSource::getName, "name"), 154 | Columns.min(TestSource::getDate, "min"), 155 | Columns.avg(TestSource::getId, "avg"), 156 | Columns.count("count"), 157 | Columns.count(TestSource::getName, "countName"), 158 | Columns.countDistinct(TestSource::getName, "countDistinct") 159 | ) 160 | .having(row -> Integer.parseInt(row.get("avg").toString()) > 2) 161 | .orderBy(TestSource::getAge); 162 | } 163 | 164 | 165 | /** 166 | * result write demo 167 | */ 168 | public void write(){ 169 | // write List 170 | List list = Linq.from(source).orderByAsc(TestSource::getDate).write(TestSource.class); 171 | // write Object 172 | TestSource obj = Linq.from(source).limit(3).writeOne(TestSource.class); 173 | // write List 174 | List> map = Linq.from(source).writeMap(); 175 | // write Map 176 | Map mapOne = Linq.from(source).writeMapOne(); 177 | } 178 | 179 | } 180 | 181 | ``` 182 | 183 | #### 后续迭代计划 184 | 185 | - [ ] 支持多个查询结果集进行组合: UNION ALL、UNION、INTERSECT、EXCEPT、UNION BY NAME 186 | - [ ] 支持窗口函数 187 | - [ ] 支持 Nested loop join 188 | - [x] 支持 having 189 | - [x] 支持分组列格式化 group by date(created_at) 190 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/util/Columns.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.util; 2 | 3 | import xyz.erupt.linq.consts.CompareSymbol; 4 | import xyz.erupt.linq.lambda.LambdaInfo; 5 | import xyz.erupt.linq.lambda.LambdaSee; 6 | import xyz.erupt.linq.lambda.SFunction; 7 | import xyz.erupt.linq.schema.Column; 8 | import xyz.erupt.linq.schema.Row; 9 | 10 | import java.math.BigDecimal; 11 | import java.util.*; 12 | import java.util.function.BiFunction; 13 | import java.util.function.Function; 14 | 15 | public class Columns { 16 | 17 | public static Column of(SFunction fun) { 18 | return of(fun, LambdaSee.field(fun)); 19 | } 20 | 21 | public static Column of(SFunction fun, String alias) { 22 | LambdaInfo lambdaInfo = LambdaSee.info(fun); 23 | Column column = new Column(); 24 | column.setTable(lambdaInfo.getClazz()); 25 | column.setField(lambdaInfo.getField()); 26 | column.setAlias(alias); 27 | return column; 28 | } 29 | 30 | public static Column of(SFunction fun, SFunction alias) { 31 | return of(fun, LambdaSee.field(alias)); 32 | } 33 | 34 | @Deprecated 35 | public static Column ofx(SFunction fun, Function convert, String alias) { 36 | Column column = Columns.of(fun); 37 | column.setAlias(alias); 38 | column.setRowConvert(row -> convert.apply(row.get(fun))); 39 | return column; 40 | } 41 | 42 | @Deprecated 43 | public static Column ofx(SFunction fun, Function convert) { 44 | return ofx(fun, convert, LambdaSee.field(fun)); 45 | } 46 | 47 | @Deprecated 48 | public static Column ofs(Function fun, String alias) { 49 | Column column = new Column(); 50 | column.setTable(VirtualColumn.class); 51 | column.setField(VirtualColumn.lambdaColumn().getField()); 52 | column.setAlias(alias); 53 | column.setRowConvert(fun); 54 | return column; 55 | } 56 | 57 | @Deprecated 58 | public static Column ofs(Function fun, SFunction alias) { 59 | return ofs(fun, LambdaSee.field(alias)); 60 | } 61 | 62 | 63 | public static Column count(String alias) { 64 | Column column = new Column(VirtualColumn.class, VirtualColumn.lambdaColumn().getField(), alias); 65 | column.setGroupByFun(it -> BigDecimal.valueOf(it.size())); 66 | return column; 67 | } 68 | 69 | public static Column count(SFunction alias) { 70 | return count(LambdaSee.field(alias)); 71 | } 72 | 73 | public static Column count(SFunction fun, String alias) { 74 | return groupByProcess(fun, alias, (column, list) -> { 75 | int i = 0; 76 | for (Row row : list) { 77 | if (null != row.get(column)) i++; 78 | } 79 | return BigDecimal.valueOf(i); 80 | }); 81 | } 82 | 83 | public static Column count(SFunction fun, SFunction alias) { 84 | return count(fun, LambdaSee.field(alias)); 85 | } 86 | 87 | public static Column countDistinct(SFunction fun, String alias) { 88 | return groupByProcess(fun, alias, (column, list) -> { 89 | Map distinctMap = new HashMap<>(); 90 | for (Row row : list) { 91 | Optional.ofNullable(row.get(column)).ifPresent(it -> distinctMap.put(it, null)); 92 | } 93 | return BigDecimal.valueOf(distinctMap.size()); 94 | }); 95 | } 96 | 97 | public static Column countDistinct(SFunction fun, SFunction alias) { 98 | return countDistinct(fun, LambdaSee.field(alias)); 99 | } 100 | 101 | public static Column max(SFunction fun, String alias) { 102 | return groupByProcess(fun, alias, (column, list) -> { 103 | Object result = null; 104 | for (Row row : list) { 105 | Object val = row.get(column); 106 | if (null == result) result = val; 107 | if (CompareUtil.compare(val, result, CompareSymbol.GT)) result = val; 108 | } 109 | if (result instanceof Number) { 110 | return RowUtil.numberToBigDecimal((Number) result); 111 | } else { 112 | return result; 113 | } 114 | }); 115 | } 116 | 117 | 118 | public static Column max(SFunction fun, SFunction alias) { 119 | return max(fun, LambdaSee.field(alias)); 120 | } 121 | 122 | public static Column min(SFunction fun, String alias) { 123 | return groupByProcess(fun, alias, (column, list) -> { 124 | Object result = null; 125 | for (Row row : list) { 126 | Object val = row.get(column); 127 | if (null == result) result = val; 128 | if (CompareUtil.compare(val, result, CompareSymbol.LT)) result = val; 129 | } 130 | if (result instanceof Number) { 131 | return RowUtil.numberToBigDecimal((Number) result); 132 | } else { 133 | return result; 134 | } 135 | }); 136 | } 137 | 138 | public static Column min(SFunction fun, SFunction alias) { 139 | return min(fun, LambdaSee.field(alias)); 140 | } 141 | 142 | public static Column avg(SFunction fun, String alias) { 143 | return groupByProcess(fun, alias, (column, list) -> { 144 | BigDecimal bigDecimal = new BigDecimal(0); 145 | int count = 0; 146 | for (Row row : list) { 147 | Object val = row.get(column); 148 | if (val instanceof Number) { 149 | bigDecimal = bigDecimal.add(new BigDecimal(String.valueOf(val))); 150 | count++; 151 | } 152 | } 153 | return count > 0 ? BigDecimal.valueOf(bigDecimal.doubleValue() / count) : BigDecimal.valueOf(0); 154 | }); 155 | } 156 | 157 | public static Column avg(SFunction fun, SFunction alias) { 158 | return avg(fun, LambdaSee.field(alias)); 159 | } 160 | 161 | public static Column sum(SFunction fun, String alias) { 162 | return groupByProcess(fun, alias, (column, list) -> { 163 | BigDecimal bigDecimal = new BigDecimal(0); 164 | for (Row row : list) { 165 | Object val = row.get(column); 166 | if (val instanceof Number) { 167 | bigDecimal = bigDecimal.add(new BigDecimal(String.valueOf(val))); 168 | } 169 | } 170 | return bigDecimal; 171 | }); 172 | } 173 | 174 | public static Column sum(SFunction fun, SFunction alias) { 175 | return sum(fun, LambdaSee.field(alias)); 176 | } 177 | 178 | // select object[] 179 | public static Column groupArray(SFunction fun, String alias) { 180 | return groupByProcess(fun, alias, (column, list) -> { 181 | List result = new ArrayList<>(); 182 | for (Row row : list) { 183 | Optional.ofNullable(row.get(column)).ifPresent(result::add); 184 | } 185 | return result; 186 | }); 187 | } 188 | 189 | public static Column groupArray(SFunction fun, SFunction alias) { 190 | return groupArray(fun, LambdaSee.field(alias)); 191 | } 192 | 193 | // custom group by logic 194 | public static Column groupByProcess(SFunction fun, String alias, BiFunction, Object> groupByProcess) { 195 | Column column = Columns.of(fun, alias); 196 | column.setGroupByFun(it -> groupByProcess.apply(column.getRawColumn(), it)); 197 | return column; 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linq.J A Memory-based Object Query language 2 | [中文](./README-zh.md) / English 3 | 4 | `Java uses Linq capabilities similar to C#` [C# Linq](https://learn.microsoft.com/zh-cn/dotnet/csharp/linq/) 5 | 6 |

7 | Erupt Framework 8 | maven-central 9 | jdk 8+ 10 | license Apache 2.0 11 | GitCode star 12 | GitEE star 13 | GitHub stars 14 |

15 | 16 | ### Linq is Object oriented sql, linq is actually a query on the data in memory, enabling developers to write queries more easily. These query expressions look a lot like SQL 17 | 18 | > You can join, filter, sort, and group data sources with minimal code. These operations can be combined in a single query to obtain more complex results 19 | 20 | #### allows you to write Java code that manipulates in-memory data in the same way that you query a database, for example 21 | - List, Array 22 | - SQL result data 23 | - CSV, XML, JSON document datasets 24 | - Stream, File stream 25 | 26 | #### Application Scenarios 27 | - Result association for RPCS such as Feign/Dubbo during distributed development 28 | - In-memory computation of heterogeneous system data 29 | - Use code to organize SQL result data 30 | - Sorted aggregation of multiple result objects with in-memory paging 31 | - Semantic object transformation and mapping 32 | - Clean code, no need for loops and branches to manipulate data 33 | - Federated access across data sources 34 | 35 | #### Operation syntax 36 | `From` `Select` `Distinct`、`Join`、`Where`、`Group By`、`Order By`、`Limit`、`Offset`、`...` 37 | 38 | #### Tips 39 | ⚠️ Note: The object field must have a get method to facilitate lambda lookup. It is recommended to use the **Lombok** @Getter annotation to quickly create get access to the field 40 | 41 | #### How to Use 42 | It has zero external dependencies and is only 50kb in size 43 | ```xml 44 | 45 | xyz.erupt 46 | linq.j 47 | LATEST 48 | 49 | ``` 50 | 51 | #### Example 1 52 | ```javascript 53 | var strings = Linq.from("C", "A", "B", "B").gt(Th::is, "A").orderByDesc(Th::is).write(String.class); 54 | // [C, B, B] 55 | 56 | var integers = Linq.from(1, 2, 3, 7, 6, 5).orderBy(Th::is).write(Integer.class); 57 | // [1, 2, 3, 5, 6, 7] 58 | 59 | var name = Linq.from(data) 60 | // left join 61 | .innerJoin(target, Target::getId, Data::getId) 62 | // where like 63 | .like(Data::getName, "a") 64 | // select name 65 | .select(Data::getName) 66 | // distinct 67 | .distinct() 68 | // order by 69 | .orderBy(Data::getName) 70 | .write(String.class); 71 | 72 | ``` 73 | 74 | #### Example 2 75 | ```java 76 | public class ObjectQuery{ 77 | 78 | private final List source = http.get("https://gw.alipayobjects.com/os/antfincdn/v6MvZBUBsQ/column-data.json"); 79 | 80 | private final List target = mongodb.query("db.target.find()"); 81 | 82 | /** 83 | * select demo 84 | */ 85 | public void select(){ 86 | // select * 87 | Linq.from(source).select(TestSource.class); 88 | // select a, b, c 89 | Linq.from(source) 90 | .select(TestSource::getName, TestSource::getDate, TestSource::getTags) 91 | .select(TestSource::getTags, "tag2") // alias 92 | .select(Columns.ofx(TestSource::getId, id -> id + "xxx")); // value convert 93 | // select count(*), sum(id), max(id) 94 | Linq.from(source) 95 | .select(Columns.count("count")) 96 | .select(Columns.sum(TestSource::getId, "sum")) 97 | .select(Columns.max(TestSource::getId, "max")); 98 | } 99 | 100 | 101 | /** 102 | * join demo 103 | */ 104 | public void join(){ 105 | // left join 106 | Linq.from(source).leftJoin(target, TestSourceExt::getId, TestSource::getId) 107 | .select(TestSource.class) 108 | .select(TestSourceExt::getName) 109 | .select(TestSourceExt2::getValue); 110 | // right join 111 | Linq.from(source).rightJoin(target, TestSourceExt::getId, TestSource::getId); 112 | // inner join 113 | Linq.from(source).innerJoin(target, TestSourceExt::getId, TestSource::getId); 114 | // full join 115 | Linq.from(source).fullJoin(target, TestSourceExt::getId, TestSource::getId); 116 | } 117 | 118 | 119 | /** 120 | * where demo 121 | */ 122 | public void where() { 123 | // = 124 | Linq.from(source).eq(TestSource::getName, "Thanos").select(Columns.count(countAlias)).writeOne(Integer.class); 125 | // >=:lval and <=:rval 126 | Linq.from(source).between(TestSource::getId, 1, 3); 127 | // in (x,x,x) 128 | Linq.from(source).in(TestSource::getId, 1, 2, 3); 129 | // like '%x%' 130 | Linq.from(source).like(TestSource::getName, "a"); 131 | // is null 132 | Linq.from(source).isNull(TestSource::getId); 133 | 134 | // customer single field where 135 | Linq.from(source).where(TestSource::getId, id -> id >= 5); 136 | 137 | // customer condition or multi field 138 | Linq.from(source).where(data -> { 139 | String name = data.get(TestSource::getName); 140 | Integer age = (Integer)data.get(TestSource::getAge); 141 | // name = 'xxx' or age > 10 142 | return "xxx".equals(name) || age > 10; 143 | }); 144 | } 145 | 146 | 147 | /** 148 | * group by demo 149 | */ 150 | public void groupBy(){ 151 | Linq.from(source) 152 | .groupBy(TestSource::getName) 153 | .select( 154 | Columns.of(TestSource::getName, "name"), 155 | Columns.min(TestSource::getDate, "min"), 156 | Columns.avg(TestSource::getId, "avg"), 157 | Columns.count("count"), 158 | Columns.count(TestSource::getName, "countName"), 159 | Columns.countDistinct(TestSource::getName, "countDistinct") 160 | ) 161 | .having(row -> Integer.parseInt(row.get("avg").toString()) > 2) 162 | .orderBy(TestSource::getAge); 163 | } 164 | 165 | 166 | /** 167 | * result write demo 168 | */ 169 | public void write(){ 170 | // write List 171 | List list = Linq.from(source).orderByAsc(TestSource::getDate).write(TestSource.class); 172 | // write Object 173 | TestSource obj = Linq.from(source).limit(3).writeOne(TestSource.class); 174 | // write List 175 | List> map = Linq.from(source).writeMap(); 176 | // write Map 177 | Map mapOne = Linq.from(source).writeMapOne(); 178 | } 179 | 180 | } 181 | 182 | ``` 183 | 184 | #### Next iteration plan 185 | 186 | - [ ] Supports combining multiple query result sets: UNION ALL, UNION, INTERSECT, EXCEPT, UNION BY NAME 187 | - [ ] Supports window functions 188 | - [ ] Support Nested loop join 189 | - [x] supports having 190 | - [x] Support group column format group by date(created_at) -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/Linq.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq; 2 | 3 | import xyz.erupt.linq.consts.JoinMethod; 4 | import xyz.erupt.linq.engine.Engine; 5 | import xyz.erupt.linq.engine.EruptEngine; 6 | import xyz.erupt.linq.grammar.*; 7 | import xyz.erupt.linq.lambda.LambdaSee; 8 | import xyz.erupt.linq.lambda.SFunction; 9 | import xyz.erupt.linq.lambda.Th; 10 | import xyz.erupt.linq.schema.*; 11 | import xyz.erupt.linq.util.Columns; 12 | import xyz.erupt.linq.util.ReflectField; 13 | import xyz.erupt.linq.util.VirtualColumn; 14 | 15 | import java.lang.reflect.Field; 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | import java.util.function.BiFunction; 20 | import java.util.function.Function; 21 | import java.util.stream.Collectors; 22 | 23 | public class Linq implements Select, Join, Where, GroupBy, OrderBy, Write { 24 | 25 | private Engine engine; 26 | 27 | public Linq() { 28 | } 29 | 30 | private final Dql dql = new Dql(); 31 | 32 | public static Linq from(List data) { 33 | Linq linq = new Linq(); 34 | linq.dql.setFrom(data); 35 | return linq; 36 | } 37 | 38 | @SafeVarargs 39 | public static Linq from(T... data) { 40 | return Linq.from(Arrays.stream(data).collect(Collectors.toList())); 41 | } 42 | 43 | public static Linq from(Boolean... data) { 44 | return Linq.from(Arrays.stream(data).collect(Collectors.toList())).select(Th::is); 45 | } 46 | 47 | public static Linq from(Byte... data) { 48 | return Linq.from(Arrays.stream(data).collect(Collectors.toList())).select(Th::is); 49 | } 50 | 51 | public static Linq from(Character... table) { 52 | return Linq.from(Arrays.stream(table).collect(Collectors.toList())).select(Th::is); 53 | } 54 | 55 | public static Linq from(String... data) { 56 | return Linq.from(Arrays.stream(data).collect(Collectors.toList())).select(Th::is); 57 | } 58 | 59 | public static Linq from(Short... data) { 60 | return Linq.from(Arrays.stream(data).collect(Collectors.toList())).select(Th::is); 61 | } 62 | 63 | public static Linq from(Integer... data) { 64 | return Linq.from(Arrays.stream(data).collect(Collectors.toList())).select(Th::is); 65 | } 66 | 67 | public static Linq from(Long... data) { 68 | return Linq.from(Arrays.stream(data).collect(Collectors.toList())).select(Th::is); 69 | } 70 | 71 | public static Linq from(Float... data) { 72 | return Linq.from(Arrays.stream(data).collect(Collectors.toList())).select(Th::is); 73 | } 74 | 75 | 76 | public static Linq from(Double... data) { 77 | return Linq.from(Arrays.stream(data).collect(Collectors.toList())).select(Th::is); 78 | } 79 | 80 | @Override 81 | public Linq distinct() { 82 | this.dql.setDistinct(true); 83 | return this; 84 | } 85 | 86 | @Override 87 | public Linq select(Column... columns) { 88 | for (Column column : columns) { 89 | this.dql.getColumns().add(column); 90 | } 91 | return this; 92 | } 93 | 94 | @Override 95 | public Linq select(Class table) { 96 | List columns = new ArrayList<>(); 97 | for (Field field : ReflectField.getFields(table)) { 98 | columns.add(new Column(table, field.getName(), field.getName())); 99 | } 100 | this.dql.getColumns().addAll(columns); 101 | return this; 102 | } 103 | 104 | @SafeVarargs 105 | @Override 106 | public final Linq select(SFunction... columns) { 107 | for (SFunction column : columns) { 108 | this.dql.getColumns().add(Columns.of(column)); 109 | } 110 | return this; 111 | } 112 | 113 | @Override 114 | public Linq select(SFunction column, BiFunction convert) { 115 | Column col = Columns.of(column); 116 | col.setRowConvert(row -> convert.apply(row, row.get(column))); 117 | this.dql.getColumns().add(col); 118 | return this; 119 | } 120 | 121 | @SafeVarargs 122 | @Override 123 | public final Linq selectExclude(SFunction... columns) { 124 | this.dql.getColumns().removeIf(it -> { 125 | for (SFunction column : columns) { 126 | if (Columns.of(column).equals(it)) { 127 | return true; 128 | } 129 | } 130 | return false; 131 | }); 132 | return this; 133 | } 134 | 135 | @Override 136 | public Linq selectAs(SFunction column, String alias) { 137 | this.dql.getColumns().add(Columns.of(column, alias)); 138 | return this; 139 | } 140 | 141 | @Override 142 | public Linq selectAs(SFunction column, SFunction alias) { 143 | this.dql.getColumns().add(Columns.of(column, LambdaSee.field(alias))); 144 | return this; 145 | } 146 | 147 | @Override 148 | public Linq selectAs(SFunction column, BiFunction convert, String alias) { 149 | Column col = Columns.of(column, alias); 150 | col.setRowConvert(row -> convert.apply(row, row.get(column))); 151 | this.dql.getColumns().add(col); 152 | return this; 153 | } 154 | 155 | @Override 156 | public Linq selectAs(SFunction column, BiFunction convert, SFunction alias) { 157 | return selectAs(column, convert, LambdaSee.field(alias)); 158 | } 159 | 160 | @Override 161 | public Linq selectRowAs(Function convert, String alias) { 162 | Column column = Columns.of(VirtualColumn::col, alias); 163 | column.setRowConvert(convert); 164 | this.dql.getColumns().add(column); 165 | return this; 166 | } 167 | 168 | @Override 169 | public Linq selectRowAs(Function convert, SFunction alias) { 170 | return selectRowAs(convert, LambdaSee.field(alias)); 171 | } 172 | 173 | @Override 174 | public Linq join(JoinSchema joinSchema) { 175 | this.dql.getJoinSchemas().add(joinSchema); 176 | return this; 177 | } 178 | 179 | @Override 180 | public Linq join(JoinMethod joinMethod, List target, SFunction onL, SFunction onR) { 181 | this.dql.getJoinSchemas().add(new JoinSchema<>(joinMethod, target, onL, onR)); 182 | return this; 183 | } 184 | 185 | @Override 186 | public Linq orderBy(List orderBySchemas) { 187 | this.dql.getOrderBys().addAll(orderBySchemas); 188 | return this; 189 | } 190 | 191 | @Override 192 | public Linq where(Function fun) { 193 | this.dql.getWheres().add(fun); 194 | return this; 195 | } 196 | 197 | @Override 198 | public Linq groupBy(Column... columns) { 199 | for (Column col : columns) { 200 | this.dql.getGroupBys().add(col); 201 | } 202 | return this; 203 | } 204 | 205 | @SafeVarargs 206 | @Override 207 | public final Linq groupBy(SFunction... columns) { 208 | Column[] cols = new Column[columns.length]; 209 | for (int i = 0; i < columns.length; i++) { 210 | cols[i] = Columns.of(columns[i]); 211 | } 212 | return groupBy(cols); 213 | } 214 | 215 | @Override 216 | public Linq groupBy(SFunction column, BiFunction convert) { 217 | Column col = Columns.of(column); 218 | col.setRowConvert(row -> convert.apply(row, row.get(column))); 219 | return groupBy(col); 220 | } 221 | 222 | @Override 223 | public Linq having(Function condition) { 224 | this.dql.getHaving().add(condition); 225 | return this; 226 | } 227 | 228 | public Linq limit(int size) { 229 | this.dql.setLimit(size); 230 | return this; 231 | } 232 | 233 | public Linq offset(int size) { 234 | this.dql.setOffset(size); 235 | return this; 236 | } 237 | 238 | public void setEngine(Engine engine) { 239 | this.engine = engine; 240 | } 241 | 242 | @Override 243 | public Engine wEngine() { 244 | if (null == this.engine) return new EruptEngine(); 245 | return this.engine; 246 | } 247 | 248 | @Override 249 | public Dql wDQL() { 250 | return this.dql; 251 | } 252 | 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/util/RowUtil.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.util; 2 | 3 | import xyz.erupt.linq.exception.LinqException; 4 | import xyz.erupt.linq.lambda.Th; 5 | import xyz.erupt.linq.schema.Column; 6 | import xyz.erupt.linq.schema.Row; 7 | 8 | import java.lang.reflect.Field; 9 | import java.math.BigDecimal; 10 | import java.time.temporal.Temporal; 11 | import java.util.ArrayList; 12 | import java.util.Date; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.stream.Collectors; 16 | 17 | public class RowUtil { 18 | 19 | public static final Class[] SIMPLE_CLASS = { 20 | CharSequence.class, Character.class, Number.class, Date.class, Temporal.class, Boolean.class, 21 | CharSequence[].class, Character[].class, Number[].class, Date[].class, Temporal[].class, Boolean[].class 22 | }; 23 | 24 | public static List listToTable(List objects) { 25 | int size = objects.size(); 26 | List list = new ArrayList<>(Math.min(size, 10000)); 27 | if (size == 0) { 28 | return list; 29 | } 30 | 31 | // Find first non-null object to determine class - optimize by checking common case first 32 | Object firstObj; 33 | firstObj = objects.get(0); 34 | if (firstObj == null && size > 1) { 35 | // Only search if first is null 36 | for (int i = 1; i < size; i++) { 37 | firstObj = objects.get(i); 38 | if (firstObj != null) { 39 | break; 40 | } 41 | } 42 | } 43 | if (firstObj == null) { 44 | return list; 45 | } 46 | 47 | Class clazz = firstObj.getClass(); 48 | List fields = ReflectField.getFields(clazz); 49 | // Optimize: check simple class using faster method 50 | boolean simpleClass = false; 51 | for (Class aClass : SIMPLE_CLASS) { 52 | if (aClass.isAssignableFrom(clazz)) { 53 | simpleClass = true; 54 | break; 55 | } 56 | } 57 | 58 | // Pre-create Column objects and set fields accessible 59 | Map columnCache = null; 60 | Column simpleColumn = null; 61 | if (simpleClass) { 62 | simpleColumn = Columns.of(Th::is); 63 | } else { 64 | columnCache = new java.util.HashMap<>(fields.size()); 65 | for (Field field : fields) { 66 | if (!field.isAccessible()) field.setAccessible(true); 67 | columnCache.put(field.getName(), new Column(clazz, field.getName(), field.getName())); 68 | } 69 | } 70 | 71 | // Process all objects 72 | if (simpleClass) { 73 | // Simple class path - optimized 74 | for (int i = 0; i < size; i++) { 75 | Object obj = objects.get(i); 76 | if (obj != null) { 77 | Row row = new Row(1); 78 | row.put(simpleColumn, obj); 79 | list.add(row); 80 | } 81 | } 82 | } else { 83 | // Optimization: for large datasets, use a more memory-efficient approach 84 | // Instead of creating HashMap for each Row, we can delay Row creation 85 | // or use a more compact data structure 86 | int fieldCount = fields.size(); 87 | Column[] columnArray = new Column[fieldCount]; 88 | Field[] fieldArray = fields.toArray(new Field[fieldCount]); 89 | for (int i = 0; i < fieldCount; i++) { 90 | columnArray[i] = columnCache.get(fieldArray[i].getName()); 91 | } 92 | 93 | // For very large datasets, create Row objects but optimize HashMap initialization 94 | // Use smaller initial capacity to reduce memory overhead 95 | try { 96 | for (int i = 0; i < size; i++) { 97 | Object obj = objects.get(i); 98 | if (obj != null) { 99 | // Create Row with exact capacity to avoid HashMap resizing 100 | Row row = new Row(fieldCount); 101 | // Batch put operations - use putDirect for performance (no duplicate check needed) 102 | for (int j = 0; j < fieldCount; j++) { 103 | Field field = fieldArray[j]; 104 | Object value = field.get(obj); 105 | if (null != value) { 106 | row.putDirect(columnArray[j], value); 107 | } 108 | } 109 | list.add(row); 110 | } 111 | } 112 | } catch (IllegalAccessException e) { 113 | throw new LinqException(e); 114 | } 115 | } 116 | return list; 117 | } 118 | 119 | // Cache for field maps to avoid repeated creation 120 | private static final Map, Map> FIELD_MAP_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); 121 | 122 | public static T rowToObject(Row row, Class clazz) { 123 | int rowSize = row.size(); 124 | if (rowSize == 1) { 125 | // Optimize: get first entry directly - avoid iterator creation 126 | Object firstVal = null; 127 | for (Map.Entry entry : row.entrySet()) { 128 | firstVal = entry.getValue(); 129 | break; // Only need first entry 130 | } 131 | if (firstVal != null && clazz == firstVal.getClass()) return (T) firstVal; 132 | // Use array access for better performance 133 | for (Class simpleClass : SIMPLE_CLASS) { 134 | if (simpleClass.isAssignableFrom(clazz)) { 135 | return (T) (firstVal instanceof BigDecimal ? bigDecimalConvert((BigDecimal) firstVal, clazz) : firstVal); 136 | } 137 | } 138 | } 139 | try { 140 | T instance = clazz.getDeclaredConstructor().newInstance(); 141 | // Cache field map to avoid repeated creation 142 | Map fieldMap = FIELD_MAP_CACHE.computeIfAbsent(clazz, k -> ReflectField.getFields(k).stream() 143 | .peek(field -> { 144 | if (!field.isAccessible()) field.setAccessible(true); 145 | }) 146 | .collect(Collectors.toMap(Field::getName, it -> it))); 147 | // Optimize: iterate once and check both conditions 148 | // Pre-check BigDecimal to avoid repeated instanceof 149 | for (Map.Entry entry : row.entrySet()) { 150 | Field field = fieldMap.get(entry.getKey().getAlias()); 151 | if (field != null) { 152 | Object value = entry.getValue(); 153 | try { 154 | if (value instanceof BigDecimal) { 155 | field.set(instance, bigDecimalConvert((BigDecimal) value, field.getType())); 156 | } else { 157 | field.set(instance, value); 158 | } 159 | } catch (IllegalAccessException e) { 160 | throw new LinqException(e); 161 | } 162 | } 163 | } 164 | return instance; 165 | } catch (Exception e) { 166 | throw new LinqException(e); 167 | } 168 | } 169 | 170 | public static Object bigDecimalConvert(BigDecimal bigDecimal, Class target) { 171 | if (Integer.class == target) { 172 | return bigDecimal.intValue(); 173 | } else if (Short.class == target) { 174 | return bigDecimal.shortValue(); 175 | } else if (Float.class == target) { 176 | return bigDecimal.floatValue(); 177 | } else if (Double.class == target) { 178 | return bigDecimal.doubleValue(); 179 | } else if (Byte.class == target) { 180 | return bigDecimal.byteValue(); 181 | } else if (Long.class == target) { 182 | return bigDecimal.longValue(); 183 | } else if (BigDecimal.class == target) { 184 | return bigDecimal; 185 | } else if (target.isAssignableFrom(String.class)) { 186 | return bigDecimal.toString(); 187 | } 188 | throw new LinqException("unknown 'bigDecimal' target type: " + target.getName()); 189 | } 190 | 191 | public static BigDecimal numberToBigDecimal(Number number) { 192 | if (number instanceof Integer) { 193 | return BigDecimal.valueOf((Integer) number); 194 | } else if (number instanceof Short) { 195 | return BigDecimal.valueOf((Short) number); 196 | } else if (number instanceof Float) { 197 | return BigDecimal.valueOf((Float) number); 198 | } else if (number instanceof Double) { 199 | return BigDecimal.valueOf((Double) number); 200 | } else if (number instanceof Byte) { 201 | return BigDecimal.valueOf((Byte) number); 202 | } else if (number instanceof BigDecimal) { 203 | return (BigDecimal) number; 204 | } else { 205 | return BigDecimal.valueOf(number.doubleValue()); 206 | } 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/schema/Row.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.schema; 2 | 3 | import xyz.erupt.linq.exception.LinqException; 4 | import xyz.erupt.linq.lambda.LambdaInfo; 5 | import xyz.erupt.linq.lambda.LambdaSee; 6 | import xyz.erupt.linq.lambda.SFunction; 7 | import xyz.erupt.linq.util.RowUtil; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.*; 11 | 12 | /** 13 | * Optimized Row implementation using pure array storage - no HashMap overhead. 14 | * This significantly reduces memory usage for large datasets. 15 | */ 16 | public class Row extends AbstractMap { 17 | 18 | // Array-based storage - no HashMap overhead 19 | private Column[] columns; 20 | private Object[] values; 21 | private int size; 22 | 23 | // Alias cache using arrays for compact storage (only created when needed) 24 | private String[] aliasKeys; 25 | private Column[] aliasValues; 26 | private int aliasCacheSize; 27 | 28 | public Row(int initialCapacity) { 29 | // Use exact capacity to avoid waste 30 | this.columns = new Column[initialCapacity]; 31 | this.values = new Object[initialCapacity]; 32 | this.size = 0; 33 | } 34 | 35 | public Row(Row row) { 36 | // Copy arrays directly - very efficient 37 | this.size = row.size; 38 | this.columns = Arrays.copyOf(row.columns, row.size); 39 | this.values = Arrays.copyOf(row.values, row.size); 40 | // Copy alias cache if exists 41 | if (row.aliasKeys != null) { 42 | this.aliasKeys = Arrays.copyOf(row.aliasKeys, row.aliasCacheSize); 43 | this.aliasValues = Arrays.copyOf(row.aliasValues, row.aliasCacheSize); 44 | this.aliasCacheSize = row.aliasCacheSize; 45 | } 46 | } 47 | 48 | public Row() { 49 | this(8); // Default small capacity 50 | } 51 | 52 | @Override 53 | public Object put(Column column, Object value) { 54 | // Check if column already exists (identity check first for performance, then equals) 55 | for (int i = 0; i < size; i++) { 56 | if (columns[i] == column || columns[i].equals(column)) { 57 | Object oldValue = values[i]; 58 | values[i] = value; 59 | return oldValue; 60 | } 61 | } 62 | 63 | // Add new entry 64 | putDirect(column, value); 65 | return null; 66 | } 67 | 68 | /** 69 | * Direct put without duplicate check - for performance optimization. 70 | * Use this when you know the column doesn't exist (e.g., in listToTable). 71 | * This avoids O(n) linear search overhead. 72 | */ 73 | public void putDirect(Column column, Object value) { 74 | // Grow array if needed 75 | if (size >= columns.length) { 76 | int newCapacity = size + (size >> 1) + 1; // 1.5x growth 77 | columns = Arrays.copyOf(columns, newCapacity); 78 | values = Arrays.copyOf(values, newCapacity); 79 | } 80 | 81 | columns[size] = column; 82 | values[size] = value; 83 | size++; 84 | 85 | // Update alias cache if exists 86 | if (aliasKeys != null) { 87 | updateAliasCache(column); 88 | } 89 | } 90 | 91 | @Override 92 | public Object get(Object key) { 93 | if (key instanceof Column) { 94 | return get((Column) key); 95 | } 96 | return null; 97 | } 98 | 99 | public Object get(Column column) { 100 | // Linear search - fast for small arrays, acceptable for larger ones 101 | // Use identity check first for performance, then equals for compatibility 102 | for (int i = 0; i < size; i++) { 103 | if (columns[i] == column || columns[i].equals(column)) { 104 | return values[i]; 105 | } 106 | } 107 | return null; 108 | } 109 | 110 | public Object get(String alias) { 111 | // Build alias cache on first access if not exists 112 | if (aliasKeys == null) { 113 | buildAliasCache(); 114 | } 115 | 116 | // Linear search in alias cache (usually small) 117 | for (int i = 0; i < aliasCacheSize; i++) { 118 | if (aliasKeys[i].equals(alias)) { 119 | return get(aliasValues[i]); 120 | } 121 | } 122 | return null; 123 | } 124 | 125 | private void buildAliasCache() { 126 | aliasKeys = new String[size]; 127 | aliasValues = new Column[size]; 128 | aliasCacheSize = 0; 129 | for (int i = 0; i < size; i++) { 130 | aliasKeys[aliasCacheSize] = columns[i].getAlias(); 131 | aliasValues[aliasCacheSize] = columns[i]; 132 | aliasCacheSize++; 133 | } 134 | } 135 | 136 | private void updateAliasCache(Column column) { 137 | // Grow alias cache if needed 138 | if (aliasCacheSize >= aliasKeys.length) { 139 | int newCapacity = aliasCacheSize + (aliasCacheSize >> 1) + 1; 140 | aliasKeys = Arrays.copyOf(aliasKeys, newCapacity); 141 | aliasValues = Arrays.copyOf(aliasValues, newCapacity); 142 | } 143 | aliasKeys[aliasCacheSize] = column.getAlias(); 144 | aliasValues[aliasCacheSize] = column; 145 | aliasCacheSize++; 146 | } 147 | 148 | public R get(SFunction alias) { 149 | LambdaInfo lambdaInfo = LambdaSee.info(alias); 150 | Object val = this.get(lambdaInfo.getField()); 151 | try { 152 | if (val instanceof BigDecimal) { 153 | return (R) RowUtil.bigDecimalConvert((BigDecimal) val, lambdaInfo.getClazz().getDeclaredField(lambdaInfo.getField()).getType()); 154 | } else { 155 | return (R) val; 156 | } 157 | } catch (Exception e) { 158 | throw new LinqException(e); 159 | } 160 | } 161 | 162 | @Override 163 | public int size() { 164 | return size; 165 | } 166 | 167 | @Override 168 | public boolean containsKey(Object key) { 169 | if (key instanceof Column) { 170 | Column column = (Column) key; 171 | for (int i = 0; i < size; i++) { 172 | // Use equals for compatibility, but prefer identity check for performance 173 | if (columns[i] == column || columns[i].equals(column)) { 174 | return true; 175 | } 176 | } 177 | } 178 | return false; 179 | } 180 | 181 | @Override 182 | public boolean equals(Object o) { 183 | if (this == o) return true; 184 | if (!(o instanceof Map)) return false; 185 | Map other = (Map) o; 186 | if (size != other.size()) return false; 187 | 188 | // Compare all entries - for each key in this map, check if other has same key-value 189 | for (int i = 0; i < size; i++) { 190 | Column key = columns[i]; 191 | Object value = values[i]; 192 | 193 | // Check if other map contains this key 194 | if (!other.containsKey(key)) { 195 | return false; 196 | } 197 | 198 | // Compare values 199 | Object otherValue = other.get(key); 200 | if (value == null) { 201 | if (otherValue != null) { 202 | return false; 203 | } 204 | } else { 205 | if (!value.equals(otherValue)) { 206 | return false; 207 | } 208 | } 209 | } 210 | return true; 211 | } 212 | 213 | @Override 214 | public int hashCode() { 215 | // Standard Map hashCode: sum of (key.hashCode() ^ value.hashCode()) for all entries 216 | int result = 0; 217 | for (int i = 0; i < size; i++) { 218 | Column key = columns[i]; 219 | Object value = values[i]; 220 | result += (key == null ? 0 : key.hashCode()) ^ 221 | (value == null ? 0 : value.hashCode()); 222 | } 223 | return result; 224 | } 225 | 226 | @Override 227 | public Set> entrySet() { 228 | return new AbstractSet>() { 229 | @Override 230 | public Iterator> iterator() { 231 | return new Iterator>() { 232 | private int index = 0; 233 | 234 | @Override 235 | public boolean hasNext() { 236 | return index < size; 237 | } 238 | 239 | @Override 240 | public Entry next() { 241 | if (index >= size) { 242 | throw new NoSuchElementException(); 243 | } 244 | final int i = index++; 245 | return new Entry() { 246 | @Override 247 | public Column getKey() { 248 | return columns[i]; 249 | } 250 | 251 | @Override 252 | public Object getValue() { 253 | return values[i]; 254 | } 255 | 256 | @Override 257 | public Object setValue(Object value) { 258 | Object oldValue = values[i]; 259 | values[i] = value; 260 | return oldValue; 261 | } 262 | }; 263 | } 264 | }; 265 | } 266 | 267 | @Override 268 | public int size() { 269 | return Row.this.size; 270 | } 271 | }; 272 | } 273 | 274 | @Override 275 | public void clear() { 276 | // Clear arrays to help GC 277 | Arrays.fill(columns, 0, size, null); 278 | Arrays.fill(values, 0, size, null); 279 | size = 0; 280 | aliasKeys = null; 281 | aliasValues = null; 282 | aliasCacheSize = 0; 283 | } 284 | 285 | @Override 286 | public void putAll(Map m) { 287 | if (m instanceof Row) { 288 | Row otherRow = (Row) m; 289 | // Direct array copy for efficiency - avoid put() overhead 290 | int otherSize = otherRow.size; 291 | int newSize = size + otherSize; 292 | 293 | // Grow arrays if needed 294 | if (newSize > columns.length) { 295 | int newCapacity = Math.max(newSize, columns.length + (columns.length >> 1) + 1); 296 | columns = Arrays.copyOf(columns, newCapacity); 297 | values = Arrays.copyOf(values, newCapacity); 298 | } 299 | 300 | // Copy all entries directly 301 | for (int i = 0; i < otherSize; i++) { 302 | Column col = otherRow.columns[i]; 303 | // Check if already exists (using identity first, then equals) 304 | boolean exists = false; 305 | for (int j = 0; j < size; j++) { 306 | if (columns[j] == col || columns[j].equals(col)) { 307 | values[j] = otherRow.values[i]; 308 | exists = true; 309 | break; 310 | } 311 | } 312 | if (!exists) { 313 | columns[size] = col; 314 | values[size] = otherRow.values[i]; 315 | size++; 316 | } 317 | } 318 | 319 | // Rebuild alias cache if exists 320 | if (aliasKeys != null) { 321 | buildAliasCache(); 322 | } 323 | } else { 324 | // Fallback for other Map types 325 | for (Map.Entry entry : m.entrySet()) { 326 | put(entry.getKey(), entry.getValue()); 327 | } 328 | } 329 | } 330 | 331 | @Override 332 | public Object remove(Object key) { 333 | if (key instanceof Column) { 334 | Column column = (Column) key; 335 | for (int i = 0; i < size; i++) { 336 | if (columns[i] == column || columns[i].equals(column)) { 337 | Object oldValue = values[i]; 338 | // Shift remaining elements 339 | System.arraycopy(columns, i + 1, columns, i, size - i - 1); 340 | System.arraycopy(values, i + 1, values, i, size - i - 1); 341 | columns[size - 1] = null; 342 | values[size - 1] = null; 343 | size--; 344 | 345 | // Rebuild alias cache if exists 346 | if (aliasKeys != null) { 347 | buildAliasCache(); 348 | } 349 | return oldValue; 350 | } 351 | } 352 | } 353 | return null; 354 | } 355 | } 356 | 357 | -------------------------------------------------------------------------------- /src/main/java/xyz/erupt/linq/engine/EruptEngine.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq.engine; 2 | 3 | import xyz.erupt.linq.consts.JoinExchange; 4 | import xyz.erupt.linq.consts.OrderByDirection; 5 | import xyz.erupt.linq.exception.LinqException; 6 | import xyz.erupt.linq.schema.*; 7 | import xyz.erupt.linq.util.Columns; 8 | import xyz.erupt.linq.util.RowUtil; 9 | 10 | import java.util.*; 11 | import java.util.function.Function; 12 | import java.util.stream.Collectors; 13 | 14 | public class EruptEngine extends Engine { 15 | 16 | @Override 17 | public List query(Dql dql) { 18 | this.removeAmbiguousColumn(dql); 19 | List dataset = RowUtil.listToTable(dql.getFrom()); 20 | // join process 21 | if (!dql.getJoinSchemas().isEmpty()) { 22 | this.join(dql, dataset); 23 | } 24 | // where process - optimized to use iterator for better performance 25 | if (!dql.getWheres().isEmpty()) { 26 | List> wheres = dql.getWheres(); 27 | int whereCount = wheres.size(); 28 | // Use array for faster access 29 | Function[] whereArray = wheres.toArray(new Function[whereCount]); 30 | dataset.removeIf(row -> { 31 | for (int i = 0; i < whereCount; i++) { 32 | if (!whereArray[i].apply(row)) return true; 33 | } 34 | return false; 35 | }); 36 | } 37 | // group by process 38 | if (null != dql.getGroupBys() && !dql.getGroupBys().isEmpty()) { 39 | dataset = this.groupBy(dql, dataset); 40 | } else { 41 | // simple select process - optimized to reduce object creation 42 | List columns = dql.getColumns(); 43 | int columnCount = columns.size(); 44 | 45 | // Check if we can reuse existing rows (no rowConvert, no groupByFun, columns match) 46 | boolean canReuseRows = true; 47 | boolean existGroupFun = false; 48 | Column[] rawColumns = new Column[columnCount]; 49 | boolean[] hasRowConvert = new boolean[columnCount]; 50 | boolean[] hasGroupByFun = new boolean[columnCount]; 51 | 52 | for (int i = 0; i < columnCount; i++) { 53 | Column col = columns.get(i); 54 | rawColumns[i] = col.getRawColumn(); 55 | hasRowConvert[i] = col.getRowConvert() != null; 56 | hasGroupByFun[i] = col.getGroupByFun() != null; 57 | if (hasRowConvert[i] || hasGroupByFun[i]) { 58 | canReuseRows = false; 59 | } 60 | if (hasGroupByFun[i]) { 61 | existGroupFun = true; 62 | } 63 | } 64 | 65 | // Optimization: if columns match exactly and no conversion needed, reuse rows 66 | if (canReuseRows && !dataset.isEmpty()) { 67 | Row firstRow = dataset.get(0); 68 | int firstRowSize = firstRow.size(); 69 | if (firstRowSize == columnCount) { 70 | // Check if all selected columns exist in original row 71 | boolean allMatch = true; 72 | for (int i = 0; i < columnCount; i++) { 73 | Column rawCol = rawColumns[i]; 74 | if (!firstRow.containsKey(rawCol)) { 75 | allMatch = false; 76 | break; 77 | } 78 | } 79 | if (allMatch) { 80 | // Just update column aliases if needed, no need to create new rows 81 | // Check if aliases need to be updated 82 | boolean needUpdateAlias = false; 83 | for (int i = 0; i < columnCount; i++) { 84 | Column rawCol = rawColumns[i]; 85 | Column targetCol = columns.get(i); 86 | if (!rawCol.getAlias().equals(targetCol.getAlias())) { 87 | needUpdateAlias = true; 88 | break; 89 | } 90 | } 91 | if (needUpdateAlias) { 92 | // Update aliases in place 93 | for (Row row : dataset) { 94 | for (int i = 0; i < columnCount; i++) { 95 | Column rawCol = rawColumns[i]; 96 | Column targetCol = columns.get(i); 97 | if (rawCol != targetCol && row.containsKey(rawCol)) { 98 | Object value = row.get(rawCol); 99 | row.put(targetCol, value); 100 | row.remove(rawCol); 101 | } 102 | } 103 | } 104 | } 105 | // Skip the rest of select processing - rows are already correct 106 | return dataset; 107 | } 108 | } 109 | } 110 | 111 | // Normal select process - create new rows 112 | // Don't pre-allocate full capacity for very large datasets to avoid OOM 113 | // ArrayList will grow dynamically, which is acceptable for performance 114 | int datasetSize = dataset.size(); 115 | List $table = new ArrayList<>(Math.min(datasetSize, 10000)); 116 | Column[] columnsArray = columns.toArray(new Column[columnCount]); 117 | 118 | // Optimize: batch process to reduce overhead 119 | for (int rowIdx = 0; rowIdx < datasetSize; rowIdx++) { 120 | Row row = dataset.get(rowIdx); 121 | Row newRow = new Row(columnCount); 122 | // Pre-check if we can optimize by avoiding null checks 123 | // Use putDirect for performance - columns are added in order without duplicates 124 | for (int i = 0; i < columnCount; i++) { 125 | Column column = columnsArray[i]; 126 | if (hasGroupByFun[i]) { 127 | existGroupFun = true; 128 | newRow.putDirect(column, column.getGroupByFun().apply(dataset)); 129 | } else { 130 | // Use pre-computed raw column - optimize branch prediction 131 | Object value = row.get(rawColumns[i]); 132 | if (hasRowConvert[i]) { 133 | newRow.putDirect(column, column.getRowConvert().apply(row)); 134 | } else { 135 | newRow.putDirect(column, value); 136 | } 137 | } 138 | } 139 | $table.add(newRow); 140 | if (existGroupFun) break; 141 | } 142 | dataset.clear(); 143 | dataset.addAll($table); 144 | } 145 | // having process - optimized to use iterator for better performance 146 | if (!dql.getHaving().isEmpty()) { 147 | List> having = dql.getHaving(); 148 | int havingCount = having.size(); 149 | // Use array for faster access 150 | Function[] havingArray = having.toArray(new Function[havingCount]); 151 | dataset.removeIf(row -> { 152 | for (int i = 0; i < havingCount; i++) { 153 | if (!havingArray[i].apply(row)) return true; 154 | } 155 | return false; 156 | }); 157 | } 158 | // order by process 159 | if (!dql.getOrderBys().isEmpty()) { 160 | this.orderBy(dql, dataset); 161 | } 162 | // limit 163 | if (null != dql.getOffset()) { 164 | dataset = dql.getOffset() > dataset.size() ? new ArrayList<>(0) : dataset.subList(dql.getOffset(), dataset.size()); 165 | } 166 | if (null != dql.getLimit()) { 167 | dataset = dataset.subList(0, dql.getLimit() > dataset.size() ? dataset.size() : dql.getLimit()); 168 | } 169 | // distinct process 170 | if (dql.isDistinct()) { 171 | dataset = dataset.stream().distinct().collect(Collectors.toList()); 172 | } 173 | return dataset; 174 | } 175 | 176 | // remove ambiguous column 177 | private void removeAmbiguousColumn(Dql dql) { 178 | Map uniqueColumns = new HashMap<>(); 179 | for (Column column : dql.getColumns()) { 180 | uniqueColumns.put(column.getAlias(), column); 181 | } 182 | dql.getColumns().clear(); 183 | dql.getColumns().addAll(uniqueColumns.values()); 184 | } 185 | 186 | // Note: The original code used removeAll which would remove all duplicate entries. 187 | // This modified code uses a HashMap to keep only the first occurrence of each alias, 188 | // and then sets the columns of the Dql object to the values of this HashMap. 189 | public void join(Dql dql, List dataset) { 190 | for (JoinSchema joinSchema : dql.getJoinSchemas()) { 191 | Column lon = Columns.of(joinSchema.getLon()); 192 | Column ron = Columns.of(joinSchema.getRon()); 193 | if (joinSchema.getJoinExchange() == JoinExchange.HASH) { 194 | List targetData = RowUtil.listToTable(joinSchema.getTarget()); 195 | switch (joinSchema.getJoinMethod()) { 196 | case LEFT: 197 | this.crossHashJoin(dataset, ron, targetData, lon); 198 | break; 199 | case RIGHT: 200 | this.crossHashJoin(targetData, lon, dataset, ron); 201 | dataset.clear(); 202 | dataset.addAll(targetData); 203 | break; 204 | case INNER: 205 | this.crossHashJoin(dataset, ron, targetData, lon); 206 | dataset.removeIf(it -> !it.containsKey(lon)); 207 | break; 208 | case FULL: 209 | this.crossHashJoin(dataset, ron, targetData, lon); 210 | this.crossHashJoin(targetData, lon, dataset, ron); 211 | targetData.removeIf(it -> it.containsKey(ron)); 212 | dataset.addAll(targetData); 213 | break; 214 | } 215 | } else { 216 | throw new LinqException(joinSchema.getJoinExchange().name() + " is not supported yet"); 217 | } 218 | } 219 | } 220 | 221 | //Cartesian product case 222 | private void crossHashJoin(List source, Column sourceColumn, 223 | List target, Column targetColumn) { 224 | Map> rightMap = new HashMap<>(); 225 | for (Row row : target) { 226 | if (!rightMap.containsKey(row.get(targetColumn))) { 227 | rightMap.put(row.get(targetColumn), new LinkedList<>()); 228 | } 229 | rightMap.get(row.get(targetColumn)).add(row); 230 | } 231 | ListIterator iterator = source.listIterator(); 232 | while (iterator.hasNext()) { 233 | Row row = iterator.next(); 234 | if (rightMap.containsKey(row.get(sourceColumn))) { 235 | for (int i = rightMap.get(row.get(sourceColumn)).size() - 1; i >= 0; i--) { 236 | if (i == 0) { 237 | row.putAll(rightMap.get(row.get(sourceColumn)).get(i)); 238 | } else { 239 | Row cartesianRow = new Row(row); 240 | cartesianRow.putAll(rightMap.get(row.get(sourceColumn)).get(i)); 241 | iterator.add(cartesianRow); 242 | } 243 | } 244 | } 245 | } 246 | } 247 | 248 | public List groupBy(Dql dql, List dataset) { 249 | Map> groupMap = new HashMap<>(); 250 | for (Row row : dataset) { 251 | StringBuilder key = new StringBuilder(); 252 | for (Column groupBy : dql.getGroupBys()) { 253 | if (null != groupBy.getRowConvert()) { 254 | key.append(groupBy.getRawColumn().getRowConvert().apply(row)); 255 | } else { 256 | key.append(row.get(groupBy.getRawColumn())); 257 | } 258 | } 259 | if (!groupMap.containsKey(key.toString())) { 260 | groupMap.put(key.toString(), new ArrayList<>()); 261 | } 262 | groupMap.get(key.toString()).add(row); 263 | } 264 | List result = new ArrayList<>(groupMap.size()); 265 | // group by select process 266 | for (Map.Entry> entry : groupMap.entrySet()) { 267 | Row values = new Row(dql.getColumns().size()); 268 | result.add(values); 269 | // Use putDirect for performance - columns are added in order without duplicates 270 | for (Column column : dql.getColumns()) { 271 | Object val = null; 272 | if (null != column.getGroupByFun()) { 273 | val = column.getRawColumn().getGroupByFun().apply(entry.getValue()); 274 | } else { 275 | if (!entry.getValue().isEmpty()) { 276 | val = entry.getValue().get(0).get(column.getRawColumn()); 277 | } 278 | } 279 | values.putDirect(column, val); 280 | } 281 | } 282 | return result; 283 | } 284 | 285 | public void orderBy(Dql dql, List dataset) { 286 | dataset.sort((a, b) -> { 287 | int i = 0; 288 | for (OrderBySchema orderBy : dql.getOrderBys()) { 289 | if (null == a.get(orderBy.getColumn()) || null == b.get(orderBy.getColumn())) return 0; 290 | if (a.get(orderBy.getColumn()) instanceof Comparable) { 291 | Comparable comparable = (Comparable) a.get(orderBy.getColumn()); 292 | i = comparable.compareTo(b.get(orderBy.getColumn())); 293 | if (orderBy.getDirection() == OrderByDirection.DESC) i = ~i + 1; 294 | if (i != 0) return i; 295 | } else { 296 | throw new LinqException(orderBy.getColumn().getTable() + "." + orderBy.getColumn().getField() + " sort does not implement the Comparable interface"); 297 | } 298 | } 299 | return i; 300 | }); 301 | } 302 | 303 | } 304 | -------------------------------------------------------------------------------- /src/test/java/xyz/erupt/linq/LinqTest.java: -------------------------------------------------------------------------------- 1 | package xyz.erupt.linq; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import xyz.erupt.linq.consts.OrderByDirection; 6 | import xyz.erupt.linq.data.source.TestSource; 7 | import xyz.erupt.linq.data.source.TestSourceExt; 8 | import xyz.erupt.linq.data.source.TestSourceExt2; 9 | import xyz.erupt.linq.data.source.TestSourceGroupByVo; 10 | import xyz.erupt.linq.lambda.Th; 11 | import xyz.erupt.linq.util.Columns; 12 | 13 | import java.util.*; 14 | 15 | public class LinqTest { 16 | 17 | private final List source = new ArrayList<>(); 18 | 19 | private final List target = new ArrayList<>(); 20 | 21 | private final List target2 = new ArrayList<>(); 22 | 23 | 24 | @Before 25 | public void before() { 26 | source.add(new TestSource(1, "Thanos", new Date(), new String[]{"a", "b"})); 27 | source.add(new TestSource(2, "Cube", new Date(), new String[]{"c", "d"})); 28 | source.add(new TestSource(3, "Berg", new Date(), new String[]{"f"})); 29 | source.add(new TestSource(4, "Liz", new Date(), new String[]{"a", "s", "x", "y", "z"})); 30 | source.add(new TestSource(5, "Thanos", new Date(), new String[]{"k"})); 31 | source.add(new TestSource(null, "", new Date(), new String[]{"k"})); 32 | 33 | target.add(new TestSourceExt(1, "Aa")); 34 | target.add(new TestSourceExt(2, "Bb")); 35 | target.add(new TestSourceExt(3, "Cc")); 36 | 37 | target2.add(new TestSourceExt2(1, "A")); 38 | target2.add(new TestSourceExt2(1, 1)); 39 | target2.add(new TestSourceExt2(1, true)); 40 | target2.add(new TestSourceExt2(8, "Berg")); 41 | } 42 | 43 | @Test 44 | public void selectId() { 45 | List longs = Linq.from(source).select(TestSource::getId).write(Long.class); 46 | System.out.println(longs); 47 | } 48 | 49 | @Test 50 | public void fromSingletonObject() { 51 | TestSource source = new TestSource(1, "Thanos", new Date(), new String[]{"x", "y", "z"}); 52 | String name = Linq.from(source).select(TestSource::getName).writeOne(String.class); 53 | TestSource testSourceResult = Linq.from(source).select(TestSource.class).writeOne(TestSource.class); 54 | assert "Thanos".equals(name); 55 | assert source.getId().equals(testSourceResult.getId()); 56 | assert source.getName().equals(testSourceResult.getName()); 57 | assert source.getDate().equals(testSourceResult.getDate()); 58 | assert Arrays.equals(source.getTags(), testSourceResult.getTags()); 59 | } 60 | 61 | @Test 62 | public void fromSimpleClass() { 63 | List strings = Linq.from("C", "A", "B", "B").gt(Th::is, "A").orderByDesc(Th::is).write(String.class); 64 | List integers = Linq.from(1, 2, 3, 7, 6, 5).orderBy(Th::is).write(Integer.class); 65 | System.out.println(strings); 66 | System.out.println(integers); 67 | } 68 | 69 | @Test 70 | public void writeSimpleType() { 71 | List arrays = Linq.from(source).select(TestSource::getTags).write(String[].class); 72 | List dates = Linq.from(source).select(TestSource::getId).write(Date.class); 73 | List integers = Linq.from(source).select(TestSource::getId).write(Integer.class); 74 | List strings = Linq.from(source).select(TestSource::getName).write(String.class); 75 | assert !arrays.isEmpty() && !dates.isEmpty() && !integers.isEmpty() && !strings.isEmpty(); 76 | } 77 | 78 | @Test 79 | public void selectTest() { 80 | List> list = Linq.from(source) 81 | .select(TestSource::getName, TestSource::getDate, TestSource::getTags) 82 | .selectAs(TestSource::getTags, "tag2") 83 | .select(TestSource::getId, (row, id) -> id + "xxxx") 84 | .select(TestSource::getId, (row, id) -> id + "xxx2") 85 | .writeMap(); 86 | assert Objects.equals(source.get(0).getDate().toString(), list.get(0).get("date").toString()); 87 | assert Objects.equals(list.get(0).get("tags"), list.get(0).get("tag2")); 88 | } 89 | 90 | @Test 91 | public void selectProcessTest2() { 92 | List> list = Linq.from(source) 93 | .select(TestSource::getId, (row, id) -> id + "xxxx") 94 | .select(Columns.sum(TestSource::getId, "sum")) 95 | .groupBy(TestSource::getId, (row, id) -> id) 96 | .having(row -> Integer.parseInt(row.get("sum").toString()) > 4) 97 | .writeMap(); 98 | System.out.println(list); 99 | } 100 | 101 | @Test 102 | public void aggregationSelectTest() { 103 | Map map = Linq.from(source) 104 | .select(TestSource::getId, TestSource::getName, TestSource::getDate) 105 | .select(Columns.count("count")) 106 | .select(Columns.countDistinct(TestSource::getId, "countDistinct")) 107 | .select(Columns.sum(TestSource::getId, "sum")) 108 | .select(Columns.max(TestSource::getId, "max")) 109 | .select(Columns.min(TestSource::getId, "min")) 110 | .select(Columns.avg(TestSource::getId, "avg")) 111 | .writeMapOne(); 112 | assert Integer.parseInt(map.get("count").toString()) == source.size(); 113 | assert Integer.parseInt(map.get("countDistinct").toString()) == source.stream().map(TestSource::getName).distinct().count(); 114 | assert Integer.parseInt(map.get("sum").toString()) == source.stream().filter(it -> null != it.getId()).mapToInt(TestSource::getId).sum(); 115 | assert Integer.parseInt(map.get("max").toString()) == source.stream().filter(it -> null != it.getId()).mapToInt(TestSource::getId).max().orElse(0); 116 | assert Integer.parseInt(map.get("min").toString()) == source.stream().filter(it -> null != it.getId()).mapToInt(TestSource::getId).min().orElse(0); 117 | assert Double.parseDouble(map.get("avg").toString()) == source.stream().filter(it -> null != it.getId()).mapToInt(TestSource::getId).average().orElse(0); 118 | } 119 | 120 | @Test 121 | public void customerSelect() { 122 | List result = Linq.from(source).selectRowAs(it -> it.get(TestSource::getName) + " Borg", "Hello").write(String.class); 123 | for (int i = 0; i < result.size(); i++) { 124 | assert (source.get(i).getName() + " Borg").equals(result.get(i)); 125 | } 126 | } 127 | 128 | @Test 129 | public void strJoinTest() { 130 | List> join = Linq.from(source) 131 | .leftJoin(target2, TestSourceExt2::getValue, TestSource::getName) 132 | .select(TestSource.class) 133 | .select(TestSourceExt2::getValue) 134 | .writeMap(); 135 | System.out.println(join); 136 | } 137 | 138 | @Test 139 | public void joinTest() { 140 | List> leftJoinRes = Linq.from(source) 141 | .leftJoin(target, TestSourceExt::getId, TestSource::getId) 142 | .select(TestSource.class) 143 | .selectAs(TestSourceExt::getName, "name2") 144 | .writeMap(); 145 | assert source.size() == leftJoinRes.size(); 146 | 147 | List> rightJoinRes = Linq.from(source) 148 | .rightJoin(target, TestSourceExt::getId, TestSource::getId) 149 | .select(TestSource.class) 150 | .selectAs(TestSourceExt::getName, "name2") 151 | .writeMap(); 152 | assert target.size() == rightJoinRes.size(); 153 | 154 | List> innerJoin = Linq.from(source) 155 | .innerJoin(target, TestSourceExt::getId, TestSource::getTags) 156 | .select(TestSource.class) 157 | .selectAs(TestSourceExt::getName, "name2") 158 | .writeMap(); 159 | assert innerJoin.isEmpty(); 160 | } 161 | 162 | @Test 163 | public void complexJoinTest() { 164 | List> testSourceInfo = Linq.from(source) 165 | .leftJoin(target, TestSourceExt::getId, TestSource::getId) 166 | .leftJoin(target2, TestSourceExt2::getId, TestSource::getId) 167 | .select(TestSource.class) 168 | .select(TestSourceExt2::getValue) 169 | .select(TestSourceExt::getName) 170 | .select(TestSource::getName) 171 | .writeMap(); 172 | assert testSourceInfo.size() > source.size(); 173 | } 174 | 175 | @Test 176 | public void whereTest() { 177 | String countAlias = "count"; 178 | Integer eq = Linq.from(source).eq(TestSource::getName, "Thanos").select(Columns.count(countAlias)).writeOne(Integer.class); 179 | Integer ne = Linq.from(source).ne(TestSource::getName, "Thanos").select(Columns.count(countAlias)).writeOne(Integer.class); 180 | Integer gt = Linq.from(source).gt(TestSource::getId, 2).select(Columns.count(countAlias)).writeOne(Integer.class); 181 | Integer lt = Linq.from(source).lt(TestSource::getId, 2).select(Columns.count(countAlias)).writeOne(Integer.class); 182 | Integer gte = Linq.from(source).gte(TestSource::getId, 2).select(Columns.count(countAlias)).writeOne(Integer.class); 183 | Integer lte = Linq.from(source).lte(TestSource::getId, 2).select(Columns.count(countAlias)).writeOne(Integer.class); 184 | Integer between = Linq.from(source).between(TestSource::getId, 1, 3).select(Columns.count(countAlias)).writeOne(Integer.class); 185 | Integer in = Linq.from(source).in(TestSource::getId, 1, 2, null).select(Columns.count(countAlias)).writeOne(Integer.class); 186 | Integer notIn = Linq.from(source).notIn(TestSource::getId, 1, 2, null).select(Columns.count(countAlias)).writeOne(Integer.class); 187 | Integer like = Linq.from(source).like(TestSource::getName, "a").select(Columns.count(countAlias)).writeOne(Integer.class); 188 | Integer isNull = Linq.from(source).isNull(TestSource::getId).select(Columns.count(countAlias)).writeOne(Integer.class); 189 | Integer isNotNull = Linq.from(source).isNotNull(TestSource::getId).select(Columns.count(countAlias)).writeOne(Integer.class); 190 | Integer blank = Linq.from(source).isBlank(TestSource::getName).select(Columns.count(countAlias)).writeOne(Integer.class); 191 | Integer notBlank = Linq.from(source).isNotBlank(TestSource::getName).select(Columns.count(countAlias)).writeOne(Integer.class); 192 | Integer fieldWhere = Linq.from(source).where(TestSource::getId, id -> null != id && id >= 5).select(Columns.count(countAlias)).writeOne(Integer.class); 193 | assert eq == source.stream().filter(it -> it.getName().equals("Thanos")).count(); 194 | assert ne == source.stream().filter(it -> !it.getName().equals("Thanos")).count(); 195 | assert gt == source.stream().filter(it -> null != it.getId() && it.getId() > 2).count(); 196 | assert lt == source.stream().filter(it -> null != it.getId() && it.getId() < 2).count(); 197 | assert gte == source.stream().filter(it -> null != it.getId() && it.getId() >= 2).count(); 198 | assert lte == source.stream().filter(it -> null != it.getId() && it.getId() <= 2).count(); 199 | assert between == source.stream().filter(it -> null != it.getId() && it.getId() >= 1 && it.getId() <= 3).count(); 200 | assert in == source.stream().filter(it -> null != it.getId() && (it.getId() == 1 || it.getId() == 2)).count(); 201 | assert notIn == source.stream().filter(it -> null != it.getId() && it.getId() != 1 && it.getId() != 2).count(); 202 | assert like == source.stream().filter(it -> it.getName().contains("a")).count(); 203 | assert isNull == source.stream().filter(it -> null == it.getId()).count(); 204 | assert isNotNull == source.stream().filter(it -> null != it.getId()).count(); 205 | assert blank == source.stream().filter(it -> null == it.getName() || it.getName().trim().isEmpty()).count(); 206 | assert notBlank == source.stream().filter(it -> null != it.getName() && !it.getName().trim().isEmpty()).count(); 207 | assert fieldWhere == source.stream().filter(it -> null != it.getId() && it.getId() >= 5).count(); 208 | } 209 | 210 | @Test 211 | public void orConditionTest() { 212 | // name = 'Thanos' or id = 4 213 | List> res = Linq.from(source) 214 | .leftJoin(target, TestSourceExt::getId, TestSource::getId) 215 | .select(TestSource.class) 216 | .where(data -> { 217 | String name = data.get(TestSource::getName); 218 | Integer id = data.get(TestSource::getId); 219 | if (null != name && null != id) { 220 | return name.equals("Thanos") || id == 4; 221 | } 222 | return false; 223 | }).writeMap(); 224 | assert res.size() == 3; 225 | } 226 | 227 | 228 | @Test 229 | public void groupBy() { 230 | List result = Linq.from(source) 231 | .groupBy(TestSource::getName) 232 | .select( 233 | Columns.of(TestSource::getName, TestSourceGroupByVo::getName), 234 | Columns.groupArray(TestSource::getId, TestSourceGroupByVo::getIds), 235 | Columns.min(TestSource::getDate, TestSourceGroupByVo::getMin), 236 | Columns.max(TestSource::getDate, TestSourceGroupByVo::getMax), 237 | Columns.avg(TestSource::getId, TestSourceGroupByVo::getAvg), 238 | Columns.count(TestSource::getName, TestSourceGroupByVo::getNameCount), 239 | Columns.count(TestSourceGroupByVo::getCount) 240 | ) 241 | .having(TestSourceGroupByVo::getAvg, avg -> avg >= 1) 242 | .orderBy(TestSource::getName).write(TestSourceGroupByVo.class); 243 | System.out.println(result); 244 | } 245 | 246 | @Test 247 | public void orderBy() { 248 | List result = Linq.from(source) 249 | .select(TestSource.class) 250 | .selectExclude(TestSource::getTags, TestSource::getName) 251 | .orderBy(TestSource::getId, OrderByDirection.DESC) 252 | .orderBy(TestSource::getName) 253 | .write(TestSource.class); 254 | source.sort((a, b) -> a.getId() == null || b.getId() == null ? 0 : b.getId() - a.getId()); 255 | assert Objects.equals(result.get(0).getId(), source.get(0).getId()); 256 | } 257 | 258 | @Test 259 | public void distinctTest() { 260 | List names = Linq.from(source) 261 | .select(TestSource::getName) 262 | .distinct() 263 | .write(String.class); 264 | assert names.size() == source.stream().map(TestSource::getName).distinct().count(); 265 | } 266 | 267 | @Test 268 | public void limit() { 269 | int offset = 3; 270 | String name = Linq.from(source) 271 | .select(TestSource::getName) 272 | .distinct() 273 | .limit(1).offset(offset) 274 | .writeOne(String.class); 275 | for (int i = 0; i < source.size(); i++) { 276 | assert i != offset || name.equals(source.get(i).getName()); 277 | } 278 | } 279 | 280 | } 281 | --------------------------------------------------------------------------------