Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ settings.xml

# Temp files
pom.xml.versionsBackup
.code-graph/
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.526-RELEASE] - 2025-07-25

### Added
- Core TeaQL Java implementation
- Providers and runtime services
- Dialect mapColumnType for DDL type mapping
- LARGE_TEXT semantic type support with correct max attribute interpolation

### Fixed
- Revert reflection fallback for EntityDescriptor
- Entity reflection instantiation and postgres max token issues

### Tests
- saveGraph merge verification for to-one and collection relations
- BaseEntity equals/hashCode contract enforcement
- BaseEntity root-backed property change tracking coverage
34 changes: 34 additions & 0 deletions docs/issues/issue-entity-constructor-suppliers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Bug: Entity constructor suppliers not registered in default SimpleEntityMetaFactory

## Description
When executing database queries (e.g. `Q.outboxEvents()...executeForList(ctx)`), the mapped rows are converted to Java entity instances. The mapping repository calls `descriptor.createEntity()`, which relies on `entitySupplier` to instantiate the classes:

```java
public Entity createEntity() {
if (entitySupplier == null) {
throw new IllegalStateException("No entity supplier registered for " + getType());
}
return entitySupplier.get();
}
```

The default `SimpleEntityMetaFactory` resolved by the registry contains metadata configurations but does not associate or register these constructor suppliers. As a result, query execution throws:
`java.lang.IllegalStateException: No entity supplier registered for OutboxEvent`

## Expected Behavior
Entity descriptors should automatically resolve their class constructors using reflection or have registrations automatically generated without requiring users to register constructor suppliers manually on boot.

## Suggested Solution
1. **Generator-side registration**: Update the code generator to emit constructor supplier registration statements inside the generated `EntityMetaRegistry.java` file (e.g., `entityDescriptor.setEntitySupplier(OutboxEvent::new)`).
2. **Reflection-based Fallback**: Update `EntityDescriptor` or `SimpleEntityMetaFactory` to automatically construct a default constructor supplier using reflection if `entitySupplier` is null:
```java
if (entitySupplier == null && targetType != null) {
entitySupplier = () -> {
try {
return targetType.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to instantiate " + targetType, e);
}
};
}
```
22 changes: 22 additions & 0 deletions docs/issues/issue-postgres-max-token-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Bug: Schema auto-migration fails on Postgres due to `<max>` varchar token mismatch

## Description
When running automatic database schema migration via `PostgresDataServiceExecutor.ensureSchema(ctx)`, the framework generates SQL DDL strings for varchar fields with a placeholder `<max>` (e.g. `VARCHAR(<max>)`).

Unlike SQL Server or other dialects, PostgreSQL does not recognize `VARCHAR(<max>)` as a valid type definition, throwing a `BadSqlGrammarException` during execution.

## Expected Behavior
The Postgres database dialect executor should automatically replace any `<max>` placeholder string with a valid PostgreSQL type definition (such as `VARCHAR(255)` or `TEXT`).

## Suggested Solution
Update `PostgresDataServiceExecutor` or its SQL formatter to automatically replace `<max>` varchar placeholders prior to DDL execution:

```java
@Override
public void execute(String sql) {
if (sql != null) {
sql = sql.replace("<max>", "255");
}
super.execute(sql);
}
```
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.teaql</groupId>
<artifactId>teaql-java-parent</artifactId>
<version>1.525-RELEASE</version>
<version>1.526-RELEASE</version>
<packaging>pom</packaging>

<name>teaql-java-parent</name>
Expand Down Expand Up @@ -44,6 +44,7 @@

<modules>
<module>teaql-utils</module>
<module>teaql-redis</module>
<module>teaql-utils-reflection</module>
<module>teaql-utils-json</module>
<module>teaql-utils-spring</module>
Expand Down
2 changes: 1 addition & 1 deletion teaql-android/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>io.teaql</groupId>
<artifactId>teaql-java-parent</artifactId>
<version>1.525-RELEASE</version>
<version>1.526-RELEASE</version>
</parent>

<artifactId>teaql-android</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion teaql-business-id-jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>io.teaql</groupId>
<artifactId>teaql-java-parent</artifactId>
<version>1.525-RELEASE</version>
<version>1.526-RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion teaql-context-runtime-tools/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>io.teaql</groupId>
<artifactId>teaql-java-parent</artifactId>
<version>1.525-RELEASE</version>
<version>1.526-RELEASE</version>
</parent>

<artifactId>teaql-context-runtime-tools</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion teaql-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>io.teaql</groupId>
<artifactId>teaql-java-parent</artifactId>
<version>1.525-RELEASE</version>
<version>1.526-RELEASE</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
21 changes: 21 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/UserContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,25 @@ default String generateBusinessId(Entity entity, io.teaql.core.meta.EntityDescri
void put(String key, Object value);

<T> T evaluate(String expression, Object... args);

// ==========================================
// Context Attribute (上下文级属性)
// ==========================================
default void putAttribute(String key, Object value) {}
default <T> T getAttribute(String key, Class<T> clazz) { return null; }
default Object getAttribute(String key) { return null; }

// ==========================================
// Remote Cache (分布式级,跨节点共享)
// ==========================================
default void putToRemoteCache(String key, Object value) {}
default void putToRemoteCache(String key, Object value, int timeToLiveInSeconds) {}
default <T> T getFromRemoteCache(String key, Class<T> clazz) { return null; }
default void removeFromRemoteCache(String key) {}

// ==========================================
// Remote Lock (分布式锁)
// ==========================================
default boolean tryRemoteLock(String key, long timeoutMillis, long expireMillis) { return true; }
default void unlockRemote(String key) {}
}
10 changes: 10 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/spi/CacheException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.teaql.core.spi;

public class CacheException extends RuntimeException {
public CacheException(String message) {
super(message);
}
public CacheException(String message, Throwable cause) {
super(message, cause);
}
}
13 changes: 13 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/spi/LocalCacheProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.teaql.core.spi;

import io.teaql.core.utils.Cache;

public interface LocalCacheProvider {
/**
* 获取指定命名空间的本地内存缓存实例
* @param namespace 命名空间,用于隔离不同的缓存数据
* @return 本地缓存实例
* @throws CacheException
*/
<K, V> Cache<K, V> getCache(String namespace) throws CacheException;
}
13 changes: 13 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/spi/LocalLockProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.teaql.core.spi;

import java.util.concurrent.locks.Lock;

public interface LocalLockProvider {
/**
* 获取指定的本地锁实例
* @param name 锁名称
* @return 本地锁实例
* @throws LockException
*/
Lock getLock(String name) throws LockException;
}
10 changes: 10 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/spi/LockException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.teaql.core.spi;

public class LockException extends RuntimeException {
public LockException(String message) {
super(message);
}
public LockException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.teaql.core.spi;

import io.teaql.core.utils.RemoteCache;

public interface RemoteCacheProvider {
/**
* 获取指定命名空间的远程分布式缓存实例
* @param namespace 命名空间,用于隔离不同的缓存数据
* @return 远程缓存实例
* @throws CacheException
*/
<K, V> RemoteCache<K, V> getCache(String namespace) throws CacheException;
}
13 changes: 13 additions & 0 deletions teaql-core/src/main/java/io/teaql/core/spi/RemoteLockProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.teaql.core.spi;

import io.teaql.core.utils.RemoteLock;

public interface RemoteLockProvider {
/**
* 获取指定的远程分布式锁实例
* @param name 锁名称
* @return 远程分布式锁
* @throws LockException
*/
RemoteLock getLock(String name) throws LockException;
}
4 changes: 4 additions & 0 deletions teaql-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
exports io.teaql.core.spi;

uses io.teaql.core.spi.ContextAssembler;
uses io.teaql.core.spi.RemoteCacheProvider;
uses io.teaql.core.spi.LocalCacheProvider;
uses io.teaql.core.spi.RemoteLockProvider;
uses io.teaql.core.spi.LocalLockProvider;
}
143 changes: 143 additions & 0 deletions teaql-core/src/test/java/io/teaql/core/AggregationResultTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package io.teaql.core;

import org.junit.Test;
import java.util.*;

import static org.junit.Assert.*;

public class AggregationResultTest {

@Test
public void testGetPropagateDimensionValues() {
AggregationResult result = new AggregationResult();
result.setName("Test");
assertEquals("Test", result.getName());

AggregationItem item1 = new AggregationItem();
Map<SimpleNamedExpression, Object> dim1 = new HashMap<>();
dim1.put(new SimpleNamedExpression("propA"), "valA1");
dim1.put(new SimpleNamedExpression("propB"), "valB1");
item1.setDimensions(dim1);

AggregationItem item2 = new AggregationItem();
Map<SimpleNamedExpression, Object> dim2 = new HashMap<>();
dim2.put(new SimpleNamedExpression("propA"), "valA2");
item2.setDimensions(dim2);

result.setData(Arrays.asList(item1, item2));
assertEquals(2, result.getData().size());

List<Object> propAValues = result.getPropagateDimensionValues("propA");
assertEquals(2, propAValues.size());
assertTrue(propAValues.contains("valA1"));
assertTrue(propAValues.contains("valA2"));

List<Object> propBValues = result.getPropagateDimensionValues("propB");
assertEquals(1, propBValues.size());
assertEquals("valB1", propBValues.get(0));

List<Object> propCValues = result.getPropagateDimensionValues("propC");
assertTrue(propCValues.isEmpty());
}

@Test
public void testToNumber() {
AggregationResult result = new AggregationResult();

// Null data
assertEquals(Integer.valueOf(10), result.toNumber(10));

// Empty data
result.setData(new ArrayList<>());
assertEquals(Integer.valueOf(10), result.toNumber(10));

AggregationItem item = new AggregationItem();
result.setData(Arrays.asList(item));

// Empty values
assertEquals(Integer.valueOf(10), result.toNumber(10));

Map<SimpleNamedExpression, Object> values = new LinkedHashMap<>(); // Use LinkedHashMap for predictable first item
item.setValues(values);

// Null first value
values.put(new SimpleNamedExpression("val"), null);
assertEquals(Integer.valueOf(10), result.toNumber(10));

// Number value
values.clear();
values.put(new SimpleNamedExpression("val"), 25.5);
assertEquals(25.5, result.toNumber(10).doubleValue(), 0.001);

// String value (Convert)
values.clear();
values.put(new SimpleNamedExpression("val"), "42");
assertEquals(42L, result.toNumber(10).longValue());
}

@Test
public void testToInt() {
AggregationResult result = new AggregationResult();
AggregationItem item = new AggregationItem();
Map<SimpleNamedExpression, Object> values = new HashMap<>();
values.put(new SimpleNamedExpression("val"), "100");
item.setValues(values);
result.setData(Arrays.asList(item));

assertEquals(100, result.toInt());
}

@Test
public void testToSimpleMap() {
AggregationResult result = new AggregationResult();
AggregationItem item1 = new AggregationItem();

// Empty dimensions / values
result.setData(Arrays.asList(item1));
assertTrue(result.toSimpleMap().isEmpty());

Map<SimpleNamedExpression, Object> dims = new LinkedHashMap<>();
dims.put(new SimpleNamedExpression("dim"), "dimVal");
item1.setDimensions(dims);
assertTrue(result.toSimpleMap().isEmpty());

Map<SimpleNamedExpression, Object> vals = new LinkedHashMap<>();
vals.put(new SimpleNamedExpression("val"), 42);
item1.setValues(vals);

Map<Object, Number> map = result.toSimpleMap();
assertEquals(1, map.size());
assertEquals(42, map.get("dimVal").intValue());

// Null dimension value
dims.clear();
dims.put(new SimpleNamedExpression("dim2"), null);
assertTrue(result.toSimpleMap().isEmpty());
}

@Test
public void testToLists() {
AggregationResult result = new AggregationResult();
AggregationItem item = new AggregationItem();

Map<SimpleNamedExpression, Object> dims = new HashMap<>();
dims.put(new SimpleNamedExpression("dim1"), "dimVal1");
item.setDimensions(dims);

Map<SimpleNamedExpression, Object> vals = new HashMap<>();
vals.put(new SimpleNamedExpression("val1"), 100);
item.setValues(vals);

result.setData(Arrays.asList(item));

List<Map<String, Object>> valueList = result.valueList();
assertEquals(1, valueList.size());
assertEquals(100, valueList.get(0).get("val1"));
assertNull(valueList.get(0).get("dim1"));

List<Map<String, Object>> list = result.toList();
assertEquals(1, list.size());
assertEquals(100, list.get(0).get("val1"));
assertEquals("dimVal1", list.get(0).get("dim1"));
}
}
Loading
Loading