diff --git a/annotations/src/main/java/net/staticstudios/data/Identifier.java b/annotations/src/main/java/net/staticstudios/data/Identifier.java index 86fb3772..963e0144 100644 --- a/annotations/src/main/java/net/staticstudios/data/Identifier.java +++ b/annotations/src/main/java/net/staticstudios/data/Identifier.java @@ -12,4 +12,6 @@ @Target(ElementType.FIELD) public @interface Identifier { String value(); + + boolean index() default false; } diff --git a/build.gradle b/build.gradle index 671a2375..04117996 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { allprojects { group = 'net.staticstudios' - version = '3.1.6-SNAPSHOT' + version = '3.2.0-SNAPSHOT' repositories { mavenCentral() diff --git a/core/build.gradle b/core/build.gradle index cbb5254c..0e63b167 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -15,6 +15,8 @@ dependencies { implementation 'com.h2database:h2:2.3.232' implementation 'org.jetbrains:annotations:24.0.1' + implementation("com.github.ben-manes.caffeine:caffeine:3.2.3") + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testRuntimeOnly "org.junit.platform:junit-platform-launcher" diff --git a/core/src/main/java/net/staticstudios/data/CachedValue.java b/core/src/main/java/net/staticstudios/data/CachedValue.java index f0f338cb..aa65ae4a 100644 --- a/core/src/main/java/net/staticstudios/data/CachedValue.java +++ b/core/src/main/java/net/staticstudios/data/CachedValue.java @@ -52,6 +52,24 @@ public ProxyCachedValue(UniqueData holder, Class dataType) { public void setDelegate(CachedValueMetadata metadata, AbstractCachedValue delegate) { Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); + + if (fallback != null && !metadata.hasValidatedFallbackSupplier()) { + LambdaUtils.assertLambdaDoesntCapture(fallback, List.of(UniqueData.class), null); + metadata.setValidatedFallbackSupplier(true); + } + + if (refresher != null && !metadata.hasValidatedRefresher()) { + LambdaUtils.assertLambdaDoesntCapture(refresher, List.of(UniqueData.class), null); + metadata.setValidatedRefresher(true); + } + + if (!metadata.hasValidatedUpdateHandlers()) { + for (ValueUpdateHandlerWrapper handler : updateHandlers) { + LambdaUtils.assertLambdaDoesntCapture(handler.getHandler(), List.of(UniqueData.class), null); + } + metadata.setValidatedUpdateHandlers(true); + } + delegate.setFallback(this.fallback); delegate.setRefresher(refresher); this.delegate = delegate; @@ -89,7 +107,6 @@ public CachedValue supplyFallback(Supplier fallback) { throw new UnsupportedOperationException("Cannot set fallback after initialization"); } Preconditions.checkNotNull(fallback, "Fallback supplier cannot be null"); - LambdaUtils.assertLambdaDoesntCapture(fallback, List.of(UniqueData.class), null); this.fallback = fallback; return this; } @@ -98,7 +115,6 @@ public CachedValue supplyFallback(Supplier fallback) { @Override public CachedValue refresher(Class clazz, CachedValueRefresher refresher) { Preconditions.checkArgument(delegate == null, "Cannot dynamically add a refresher after the holder has been initialized!"); - LambdaUtils.assertLambdaDoesntCapture(refresher, List.of(UniqueData.class), null); this.refresher = (CachedValueRefresher) refresher; return this; } diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 9cef61c6..92592b82 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -1,5 +1,8 @@ package net.staticstudios.data; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; import com.google.common.base.Preconditions; import com.google.common.collect.MapMaker; import net.staticstudios.data.impl.DataAccessor; @@ -17,6 +20,7 @@ import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.data.util.redis.RedisUtils; import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; @@ -34,13 +38,16 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @ApiStatus.Internal public class DataManager { + private static final Map DATA_MANAGER_INSTANCES = new ConcurrentHashMap<>(); private static Boolean useGlobal = null; private static DataManager instance; private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private final UUID applicationId; private final String applicationName; private final DataAccessor dataAccessor; private final SQLBuilder sqlBuilder; @@ -57,6 +64,10 @@ public class DataManager { private final Set registeredUpdateHandlersForRedis = ConcurrentHashMap.newKeySet(); private final Set registeredChangeHandlersForCollection = ConcurrentHashMap.newKeySet(); private final Set registeredUpdateHandlersForReference = ConcurrentHashMap.newKeySet(); + private final Cache relationCache; + private final Map> dependencyToRelationCacheMapping = new ConcurrentHashMap<>(); + private final Cache cellCache; + private final Map> dependencyToCellCacheMapping = new ConcurrentHashMap<>(); private final List> valueSerializers = new CopyOnWriteArrayList<>(); private final Consumer updateHandlerExecutor; @@ -84,13 +95,27 @@ public DataManager(StaticDataConfig config, boolean setGlobal) { instance = this; } DataManager.useGlobal = setGlobal; - applicationName = "static_data_manager_v3-" + UUID.randomUUID(); + + this.applicationId = UUID.randomUUID(); + DATA_MANAGER_INSTANCES.put(applicationId, this); + applicationName = "static_data_manager_v3-" + applicationId; postgresListener = new PostgresListener(this, dataSourceConfig); this.taskQueue = new TaskQueue(dataSourceConfig, applicationName); redisListener = new RedisListener(dataSourceConfig, this.taskQueue); sqlBuilder = new SQLBuilder(this); dataAccessor = new H2DataAccessor(this, postgresListener, redisListener, taskQueue); + this.relationCache = Caffeine.newBuilder() + .maximumSize(10_000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .removalListener((SelectQuery selectQuery, ReadCacheResult result, RemovalCause cause) -> cleanupRelationCacheEntry(selectQuery, result)) + .build(); + this.cellCache = Caffeine.newBuilder() + .maximumSize(20_000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .removalListener((SelectQuery selectQuery, ReadCacheResult result, RemovalCause cause) -> cleanupCellCacheEntry(selectQuery, result)) + .build(); + //todo: when we reconnect to postgres, refresh the internal cache from the source } @@ -99,10 +124,20 @@ public static DataManager getInstance() { return DataManager.instance; } + public static DataManager getInstance(UUID applicationId) { + DataManager manager = DATA_MANAGER_INSTANCES.get(applicationId); + Preconditions.checkArgument(manager != null, "No DataManager instance found for application ID " + applicationId); + return manager; + } + public String getApplicationName() { return applicationName; } + public UUID getApplicationId() { + return applicationId; + } + public DataAccessor getDataAccessor() { return dataAccessor; } @@ -1028,6 +1063,8 @@ public T getInstance(Class clazz, @NotNull ColumnValue String schema = metadata.schema(); String table = metadata.table(); + boolean exists; + StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT 1 FROM \"").append(schema).append("\".\"").append(table).append("\" WHERE "); for (ColumnValuePair columnValuePair : idColumns) { @@ -1035,12 +1072,36 @@ public T getInstance(Class clazz, @NotNull ColumnValue } sqlBuilder.setLength(sqlBuilder.length() - 5); @Language("SQL") String sql = sqlBuilder.toString(); - try (ResultSet rs = dataAccessor.executeQuery(sql, idColumns.stream().map(ColumnValuePair::value).toList())) { - if (!rs.next()) { - return null; + + List values = new ArrayList<>(); + for (ColumnValuePair columnValuePair : idColumns) { + values.add(columnValuePair.value()); + } + SelectQuery selectQuery = new SelectQuery(sql, values); + + ReadCacheResult cacheResult = getRelationCacheResult(selectQuery); + if (cacheResult != null) { + exists = true; + } else { + try (ResultSet rs = dataAccessor.executeQuery(sql, idColumns.stream().map(ColumnValuePair::value).toList())) { + exists = rs.next(); + + if (exists) { + Set dependencies = new HashSet<>(); + for (ColumnValuePair columnValuePair : idColumns) { + dependencies.add(new Cell(schema, table, columnValuePair.column(), idColumns)); + } + ReadCacheResult result = new ReadCacheResult(ColumnValuePairs.EMPTY, dependencies); + putRelationCacheResult(selectQuery, result); + } + + } catch (SQLException e) { + throw new RuntimeException(e); } - } catch (SQLException e) { - throw new RuntimeException(e); + } + + if (!exists) { + return null; } PersistentValueImpl.delegate(instance); @@ -1221,7 +1282,6 @@ private List generateInsertStatements(InsertContext insertConte } public T get(String schema, String table, String column, ColumnValuePairs idColumns, List idColumnLinks, Class dataType) { - //todo: caffeine cache for these as well. StringBuilder sqlBuilder = new StringBuilder().append("SELECT \"").append(column).append("\" FROM \"").append(schema).append("\".\"").append(table).append("\" WHERE "); for (ColumnValuePair columnValuePair : idColumns) { String name = columnValuePair.column(); @@ -1235,13 +1295,43 @@ public T get(String schema, String table, String column, ColumnValuePairs id } sqlBuilder.setLength(sqlBuilder.length() - 5); @Language("SQL") String sql = sqlBuilder.toString(); - try (ResultSet rs = dataAccessor.executeQuery(sql, idColumns.stream().map(ColumnValuePair::value).toList())) { + + List values = new ArrayList<>(); + for (ColumnValuePair columnValuePair : idColumns) { + values.add(columnValuePair.value()); + } + + SelectQuery selectQuery = new SelectQuery(sql, values); + if (idColumnLinks.isEmpty()) { + ReadCacheResult cacheResult = getCellCacheResult(selectQuery); + if (cacheResult != null) { + return (T) cacheResult.getValue(); + } + } + + try (ResultSet rs = dataAccessor.executeQuery(sql, values)) { Object serializedValue = null; if (rs.next()) { serializedValue = rs.getObject(column, getSerializedType(dataType)); } //todo: do some type validation here, either on the serialized or un serialized type. this method will be exposed so we need to be careful - return deserialize(dataType, serializedValue); + T deserialized = deserialize(dataType, serializedValue); + + if (!idColumnLinks.isEmpty()) { + return deserialized; // it is less trivial to invalidate columns in another table/with links. + } + + Set dependencies = new HashSet<>(); + + for (ColumnValuePair columnValuePair : idColumns) { + String name = columnValuePair.column(); + dependencies.add(new Cell(schema, table, name, idColumns)); + } + dependencies.add(new Cell(schema, table, column, idColumns)); + ReadCacheResult result = new ReadCacheResult(deserialized, dependencies); + putCellCacheResult(selectQuery, result); + return deserialized; + } catch (SQLException e) { throw new RuntimeException(e); } @@ -1358,9 +1448,8 @@ public void set(String schema, String table, String column, ColumnValuePairs idC } - public @Nullable T getRedis(String holderSchema, String holderTable, String identifier, ColumnValuePairs icColumns, Class type) { - String key = RedisUtils.buildRedisKey(holderSchema, holderTable, identifier, icColumns); - String encoded = dataAccessor.getRedisValue(key); + public @Nullable T getRedis(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns, Class type) { + String encoded = dataAccessor.getRedisValue(holderSchema, holderTable, identifier, idColumns); if (encoded == null) { return null; } @@ -1368,11 +1457,10 @@ public void set(String schema, String table, String column, ColumnValuePairs idC return deserialize(type, serialized); } - public void setRedis(String holderSchema, String holderTable, String identifier, ColumnValuePairs icColumns, int expireAfterSeconds, @Nullable Object value) { - String key = RedisUtils.buildRedisKey(holderSchema, holderTable, identifier, icColumns); + public void setRedis(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns, int expireAfterSeconds, @Nullable Object value) { Object serialized = serialize(value); String encoded = Primitives.encode(serialized); - dataAccessor.setRedisValue(key, encoded, expireAfterSeconds); + dataAccessor.setRedisValue(holderSchema, holderTable, identifier, idColumns, encoded, expireAfterSeconds); } private boolean hasCycle(SQLTable table, Map> dependencyGraph, Set visited, Set stack) { @@ -1529,4 +1617,126 @@ public void flushTaskQueue() { //Ignore }).join(); } + + public StaticDataStatistics getStatistics() { + StaticDataStatistics stats = new StaticDataStatistics(); + dataAccessor.populateStatistics(stats); + stats.setRelationCacheSize((int) relationCache.estimatedSize()); + stats.setDependenciesToRelationsCacheMappingSize(dependencyToRelationCacheMapping.size()); + stats.setCellCacheSize((int) cellCache.estimatedSize()); + stats.setDependenciesToCellCacheMappingSize(dependencyToCellCacheMapping.size()); + return stats; + } + + public @Nullable ReadCacheResult getRelationCacheResult(SelectQuery query) { + return relationCache.getIfPresent(query); + } + + public void putRelationCacheResult(SelectQuery query, @NotNull ReadCacheResult result) { + logger.trace("Putting result in relation cache for query {} with result {}", query, result); + relationCache.put(query, result); + for (Cell cell : result.getDependencies()) { + dependencyToRelationCacheMapping.computeIfAbsent(cell, k -> ConcurrentHashMap.newKeySet()) + .add(query); + } + } + + public void invalidateRelationCache(List columnNames, String schema, String table, List changedColumns, Object[] oldValues) { + for (UniqueDataMetadata metadata : uniqueDataMetadataMap.values()) { + if (metadata.schema().equals(schema) && metadata.table().equals(table)) { + ColumnValuePair[] idColumns = new ColumnValuePair[metadata.idColumns().size()]; + for (ColumnMetadata idColumn : metadata.idColumns()) { + boolean found = false; + for (int i = 0; i < columnNames.size(); i++) { + if (idColumn.name().equals(columnNames.get(i))) { + idColumns[metadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), oldValues[i]); + found = true; + break; + } + } + Preconditions.checkArgument(found, "Not all ID columnsInReferringTable were provided for UniqueData class %s. Required: %s, Provided: %s", metadata.clazz().getName(), metadata.idColumns(), Arrays.toString(oldValues)); + } + + ColumnValuePairs idCols = new ColumnValuePairs(idColumns); + for (String changedColumn : changedColumns) { + Cell cell = new Cell(schema, table, changedColumn, idCols); + Set queries = dependencyToRelationCacheMapping.remove(cell); + if (queries != null) { + for (SelectQuery query : queries) { + relationCache.invalidate(query); + logger.trace("Invalidated relation cache for query {} due to change in cell {}", query, cell); + } + } + } + } + } + } + + private void cleanupRelationCacheEntry(@NotNull SelectQuery query, @NotNull ReadCacheResult res) { + for (Cell dependency : res.getDependencies()) { + Set dependentQueries = dependencyToRelationCacheMapping.get(dependency); + if (dependentQueries != null) { + dependentQueries.remove(query); + if (dependentQueries.isEmpty()) { + dependencyToRelationCacheMapping.remove(dependency); + } + } + } + } + + public @Nullable ReadCacheResult getCellCacheResult(SelectQuery query) { + return cellCache.getIfPresent(query); + } + + public void putCellCacheResult(SelectQuery query, @NotNull ReadCacheResult result) { + logger.trace("Putting result in cell cache for query {} with result {}", query, result); + cellCache.put(query, result); + for (Cell cell : result.getDependencies()) { + dependencyToCellCacheMapping.computeIfAbsent(cell, k -> ConcurrentHashMap.newKeySet()) + .add(query); + } + } + + public void invalidateCellCache(List columnNames, String schema, String table, List changedColumns, Object[] oldValues) { + for (UniqueDataMetadata metadata : uniqueDataMetadataMap.values()) { + if (metadata.schema().equals(schema) && metadata.table().equals(table)) { + ColumnValuePair[] idColumns = new ColumnValuePair[metadata.idColumns().size()]; + for (ColumnMetadata idColumn : metadata.idColumns()) { + boolean found = false; + for (int i = 0; i < columnNames.size(); i++) { + if (idColumn.name().equals(columnNames.get(i))) { + idColumns[metadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), oldValues[i]); + found = true; + break; + } + } + Preconditions.checkArgument(found, "Not all ID columnsInReferringTable were provided for UniqueData class %s. Required: %s, Provided: %s", metadata.clazz().getName(), metadata.idColumns(), Arrays.toString(oldValues)); + } + + ColumnValuePairs idCols = new ColumnValuePairs(idColumns); + for (String changedColumn : changedColumns) { + Cell cell = new Cell(schema, table, changedColumn, idCols); + Set queries = dependencyToCellCacheMapping.remove(cell); + if (queries != null) { + for (SelectQuery query : queries) { + cellCache.invalidate(query); + logger.trace("Invalidated cell cache for query {} due to change in cell {}", query, cell); + } + } + } + } + } + } + + private void cleanupCellCacheEntry(@NotNull SelectQuery query, @NotNull ReadCacheResult res) { + for (Cell dependency : res.getDependencies()) { + Set dependentQueries = dependencyToCellCacheMapping.get(dependency); + if (dependentQueries != null) { + dependentQueries.remove(query); + if (dependentQueries.isEmpty()) { + dependencyToCellCacheMapping.remove(dependency); + } + } + } + } } diff --git a/core/src/main/java/net/staticstudios/data/PersistentCollection.java b/core/src/main/java/net/staticstudios/data/PersistentCollection.java index 66f489c2..ac6bc0df 100644 --- a/core/src/main/java/net/staticstudios/data/PersistentCollection.java +++ b/core/src/main/java/net/staticstudios/data/PersistentCollection.java @@ -1,10 +1,7 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.util.CollectionChangeHandler; -import net.staticstudios.data.util.CollectionChangeHandlerWrapper; -import net.staticstudios.data.util.PersistentCollectionMetadata; -import net.staticstudios.data.util.Relation; +import net.staticstudios.data.util.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -65,6 +62,14 @@ public Class getDataType() { public void setDelegate(PersistentCollectionMetadata metadata, PersistentCollection delegate) { Preconditions.checkState(this.delegate == null, "Delegate has already been set"); + + if (!metadata.hasValidatedChangeHandlers()) { + for (CollectionChangeHandlerWrapper wrapper : changeHandlers) { + LambdaUtils.assertLambdaDoesntCapture(wrapper.getHandler(), List.of(UniqueData.class), null); + } + metadata.setValidatedChangeHandlers(true); + } + this.delegate = delegate; holder.getDataManager().registerCollectionChangeHandlers(metadata, changeHandlers); } diff --git a/core/src/main/java/net/staticstudios/data/PersistentValue.java b/core/src/main/java/net/staticstudios/data/PersistentValue.java index a56328c6..afc50b02 100644 --- a/core/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/core/src/main/java/net/staticstudios/data/PersistentValue.java @@ -1,10 +1,7 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.util.PersistentValueMetadata; -import net.staticstudios.data.util.Value; -import net.staticstudios.data.util.ValueUpdateHandler; -import net.staticstudios.data.util.ValueUpdateHandlerWrapper; +import net.staticstudios.data.util.*; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -16,7 +13,6 @@ * @param */ public interface PersistentValue extends Value { - //todo: use caffeine to further cache pvs, provided we are using the H2 data accessor. allow us to toggle this on and off when setting up the data manager static PersistentValue of(UniqueData holder, Class dataType) { return new ProxyPersistentValue<>(holder, dataType); @@ -42,6 +38,14 @@ public ProxyPersistentValue(UniqueData holder, Class dataType) { public void setDelegate(PersistentValueMetadata metadata, PersistentValue delegate) { Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); + + if (!metadata.hasValidatedUpdateHandlers()) { + for (ValueUpdateHandlerWrapper wrapper : updateHandlers) { + LambdaUtils.assertLambdaDoesntCapture(wrapper.getHandler(), List.of(UniqueData.class), null); + } + metadata.setValidatedUpdateHandlers(true); + } + this.delegate = delegate; holder.getDataManager().registerPersistentValueUpdateHandlers(metadata, updateHandlers); } diff --git a/core/src/main/java/net/staticstudios/data/Reference.java b/core/src/main/java/net/staticstudios/data/Reference.java index d426560a..dbe5d761 100644 --- a/core/src/main/java/net/staticstudios/data/Reference.java +++ b/core/src/main/java/net/staticstudios/data/Reference.java @@ -1,10 +1,7 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.util.ReferenceMetadata; -import net.staticstudios.data.util.ReferenceUpdateHandler; -import net.staticstudios.data.util.ReferenceUpdateHandlerWrapper; -import net.staticstudios.data.util.Relation; +import net.staticstudios.data.util.*; import org.jetbrains.annotations.Nullable; import java.lang.reflect.AccessFlag; @@ -71,6 +68,14 @@ public void set(T value) { public void setDelegate(ReferenceMetadata metadata, Reference delegate) { Preconditions.checkState(this.delegate == null, "Delegate has already been set"); + + if (!metadata.hasValidatedUpdateHandlers()) { + for (ReferenceUpdateHandlerWrapper wrapper : updateHandlers) { + LambdaUtils.assertLambdaDoesntCapture(wrapper.getHandler(), List.of(UniqueData.class), null); + } + metadata.setValidatedUpdateHandlers(true); + } + this.delegate = delegate; holder.getDataManager().registerReferenceUpdateHandlers(metadata, updateHandlers); } diff --git a/core/src/main/java/net/staticstudios/data/StaticData.java b/core/src/main/java/net/staticstudios/data/StaticData.java index d5ce8f02..6755f0b9 100644 --- a/core/src/main/java/net/staticstudios/data/StaticData.java +++ b/core/src/main/java/net/staticstudios/data/StaticData.java @@ -90,4 +90,15 @@ public static void flushTaskQueue() { assertInit(); DataManager.getInstance().flushTaskQueue(); } + + + /** + * Retrieves the current performance statistics of the StaticData system, including metrics such as queries per second and updates per second. + * + * @return a StaticDataStatistics object containing the current performance metrics of the StaticData system + */ + public static StaticDataStatistics getStatistics() { + assertInit(); + return DataManager.getInstance().getStatistics(); + } } diff --git a/core/src/main/java/net/staticstudios/data/StaticDataStatistics.java b/core/src/main/java/net/staticstudios/data/StaticDataStatistics.java new file mode 100644 index 00000000..723b5690 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/StaticDataStatistics.java @@ -0,0 +1,63 @@ +package net.staticstudios.data; + +public class StaticDataStatistics { + private long queriesPerSecond = -1; + private long updatesPerSecond = -1; + private int relationCacheSize = -1; + private int dependenciesToRelationsCacheMappingSize = -1; + private int cellCacheSize = -1; + private int dependenciesToCellCacheMappingSize = -1; + + public void setQueriesPerSecond(long queriesPerSecond) { + this.queriesPerSecond = queriesPerSecond; + } + + public void setUpdatesPerSecond(long updatesPerSecond) { + this.updatesPerSecond = updatesPerSecond; + } + + public void setRelationCacheSize(int relationCacheSideSize) { + this.relationCacheSize = relationCacheSideSize; + } + + public void setDependenciesToRelationsCacheMappingSize(int dependenciesToRelationsCacheMappingSize) { + this.dependenciesToRelationsCacheMappingSize = dependenciesToRelationsCacheMappingSize; + } + + public void setCellCacheSize(int cellCacheSize) { + this.cellCacheSize = cellCacheSize; + } + + public void setDependenciesToCellCacheMappingSize(int dependenciesToCellCacheMappingSize) { + this.dependenciesToCellCacheMappingSize = dependenciesToCellCacheMappingSize; + } + + public long getQueriesPerSecond() { + return queriesPerSecond; + } + + public long getUpdatesPerSecond() { + return updatesPerSecond; + } + + public long getOperationsPerSecond() { + return queriesPerSecond + updatesPerSecond; + } + + public int getRelationCacheSize() { + return relationCacheSize; + } + + public int getDependenciesToRelationsCacheMappingSize() { + return dependenciesToRelationsCacheMappingSize; + } + + public int getCellCacheSize() { + return cellCacheSize; + } + + public int getDependenciesToCellCacheMappingSize() { + return dependenciesToCellCacheMappingSize; + } + +} diff --git a/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java index 93a4c6af..df352280 100644 --- a/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java +++ b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java @@ -1,7 +1,9 @@ package net.staticstudios.data.impl; import net.staticstudios.data.InsertMode; +import net.staticstudios.data.StaticDataStatistics; import net.staticstudios.data.parse.DDLStatement; +import net.staticstudios.data.util.ColumnValuePairs; import net.staticstudios.data.util.SQLTransaction; import net.staticstudios.data.util.SQlStatement; import org.intellij.lang.annotations.Language; @@ -27,11 +29,13 @@ default void executeUpdate(SQLTransaction.Statement statement, List valu void postDDL() throws SQLException; - @Nullable String getRedisValue(String key); + @Nullable String getRedisValue(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns); - void setRedisValue(String key, String value, int expirationSeconds); + void setRedisValue(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns, String value, int expirationSeconds); void discoverRedisKeys(List partialRedisKeys); void resync(); + + void populateStatistics(StaticDataStatistics stats); } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java index 7069a70e..5d78742e 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java @@ -41,24 +41,27 @@ public static CachedValueImpl create(UniqueData holder, Class dataType public static void delegate(T instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable CachedValue> pair : ReflectionUtils.getFieldInstancePairs(instance, CachedValue.class)) { - CachedValueMetadata pvMetadata = metadata.cachedValueMetadata().get(pair.field()); - if (pair.instance() instanceof CachedValue.ProxyCachedValue proxyCv) { - CachedValueImpl.createAndDelegate(proxyCv, pvMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, CachedValueImpl.create(instance, ReflectionUtils.getGenericType(pair.field()), pvMetadata)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.cachedValueMetadata().entrySet()) { + Field field = entry.getKey(); + CachedValueMetadata cvMetadata = entry.getValue(); + + Object value = field.get(instance); + if (value instanceof CachedValue.ProxyCachedValue proxyCv) { + CachedValueImpl.createAndDelegate(proxyCv, cvMetadata); + } else { + field.set(instance, CachedValueImpl.create(instance, cvMetadata.type(), cvMetadata)); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } public static Map extractMetadata(String holderSchema, String holderTable, Class clazz) { Map metadataMap = new HashMap<>(); for (Field field : ReflectionUtils.getFields(clazz, CachedValue.class)) { + field.setAccessible(true); metadataMap.put(field, extractMetadata(holderSchema, holderTable, clazz, field)); } return metadataMap; @@ -76,7 +79,8 @@ public static CachedValueMetadata extractMetadata(String expireAfterSeconds = expireAfterAnnotation.value(); } - return new CachedValueMetadata(clazz, holderSchema, holderTable, ValueUtils.parseValue(identifierAnnotation.value()), expireAfterSeconds); + + return new CachedValueMetadata(clazz, holderSchema, holderTable, ValueUtils.parseValue(identifierAnnotation.value()), ReflectionUtils.getGenericType(field), expireAfterSeconds); } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java index af972ed9..afd8f216 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java @@ -11,7 +11,6 @@ import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.lang.reflect.Array; import java.lang.reflect.Field; @@ -42,20 +41,21 @@ public static PersistentManyToManyCollectionImpl creat public static void delegate(T instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable PersistentCollection> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentCollection.class)) { - PersistentCollectionMetadata collectionMetadata = metadata.persistentCollectionMetadata().get(pair.field()); - if (!(collectionMetadata instanceof PersistentManyToManyCollectionMetadata oneToManyMetadata)) continue; - - if (pair.instance() instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { - createAndDelegate((PersistentCollection.ProxyPersistentCollection) proxyCollection, oneToManyMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, oneToManyMetadata)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.persistentCollectionMetadata().entrySet()) { + Field field = entry.getKey(); + PersistentCollectionMetadata pcMetadata = entry.getValue(); + + if (!(pcMetadata instanceof PersistentManyToManyCollectionMetadata manyToManyMetadata)) continue; + Object value = field.get(instance); + if (value instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { + PersistentManyToManyCollectionImpl.createAndDelegate((PersistentCollection.ProxyPersistentCollection) proxyCollection, manyToManyMetadata); + } else { + field.set(instance, PersistentManyToManyCollectionImpl.create(instance, manyToManyMetadata)); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } @@ -72,6 +72,7 @@ public static Map idsToRemove) { * @return set of id column value pairs for the referenced type */ public Set getIds() { - //todo: this method is slow, plan to cache this later. Preconditions.checkArgument(!holder.isDeleted(), "Cannot get entries on a deleted UniqueData instance"); Set ids = new HashSet<>(); UniqueDataMetadata holderMetadata = holder.getMetadata(); @@ -527,8 +527,23 @@ public Set getIds() { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); for (ColumnMetadata columnMetadata : target.idColumns()) { - sqlBuilder.append("_target.\"").append(columnMetadata.name()).append("\", "); + sqlBuilder.append("_target.\"").append(columnMetadata.name()).append("\" AS \"t_").append(columnMetadata.name()).append("\", "); + } + + for (Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinTableSchema).append("\".\"").append(joinTableName) + .append("\".\"").append(joinColumn).append("\" AS \"jd_") + .append(joinColumn).append("\", "); + } + + for (Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinTableSchema).append("\".\"").append(joinTableName) + .append("\".\"").append(joinColumn).append("\" AS \"jr_") + .append(joinColumn).append("\", "); } + sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(" FROM \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" "); sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" _data ON "); @@ -554,20 +569,82 @@ public Set getIds() { sqlBuilder.setLength(sqlBuilder.length() - 5); @Language("SQL") String sql = sqlBuilder.toString(); - try (ResultSet rs = dataAccessor.executeQuery(sql, holder.getIdColumns().stream().map(ColumnValuePair::value).toList())) { + + List values = new ArrayList<>(); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + values.add(columnValuePair.value()); + } + +// SelectQuery query = new SelectQuery(sql, values); +// +// ReadCacheResult result = holder.getDataManager().getRelationCacheResult(query); +// if (result != null) { +// return (Set) result.getValues(); +// } +// +// +// Set dependencies = new HashSet<>(); +// +// for (Link entry : joinTableToDataTableLinks) { +// String dataColumn = entry.columnInReferencedTable(); +// dependencies.add(new Cell(holderMetadata.schema(), holderMetadata.table(), dataColumn, holder.getIdColumns())); +// } + + try (ResultSet rs = dataAccessor.executeQuery(sql, values)) { while (rs.next()) { int i = 0; ColumnValuePair[] idColumns = new ColumnValuePair[target.idColumns().size()]; for (ColumnMetadata columnMetadata : target.idColumns()) { - Object value = rs.getObject(columnMetadata.name()); + Object value = rs.getObject("t_" + columnMetadata.name()); idColumns[i++] = new ColumnValuePair(columnMetadata.name(), value); } - ids.add(new ColumnValuePairs(idColumns)); + ColumnValuePairs entryIds = new ColumnValuePairs(idColumns); + ids.add(entryIds); + +// ColumnValuePair[] joinIds = new ColumnValuePair[joinTableToDataTableLinks.size() + joinTableToReferencedTableLinks.size()]; +// +// int j = 0; +// +// for (Link entry : joinTableToDataTableLinks) { +// String joinColumn = entry.columnInReferringTable(); +// Object value = rs.getObject("jd_" + joinColumn); +// joinIds[j++] = new ColumnValuePair(joinColumn, value); +// } +// +// for (Link entry : joinTableToReferencedTableLinks) { +// String joinColumn = entry.columnInReferringTable(); +// Object value = rs.getObject("jr_" + joinColumn); +// joinIds[j++] = new ColumnValuePair(joinColumn, value); +// } +// +// ColumnValuePairs joinIdColumns = new ColumnValuePairs(joinIds); +// +// for (Link link : joinTableToDataTableLinks) { +// String joinColumn = link.columnInReferringTable(); +// dependencies.add(new Cell(joinTableSchema, joinTableName, joinColumn, joinIdColumns)); +// } +// for (Link link : joinTableToReferencedTableLinks) { +// String joinColumn = link.columnInReferringTable(); +// String referencedColumn = link.columnInReferencedTable(); +// dependencies.add(new Cell(target.schema(), target.table(), referencedColumn, entryIds)); +// dependencies.add(new Cell(joinTableSchema, joinTableName, joinColumn, joinIdColumns)); +// } +// +// for (ColumnMetadata theirIdColumns : target.idColumns()) { +// String column = theirIdColumns.name(); +// dependencies.add(new Cell(target.schema(), target.table(), column, entryIds)); +// } + } } catch (SQLException e) { throw new RuntimeException(e); } +// ReadCacheResult newResult = new ReadCacheResult(ids, dependencies); +// holder.getDataManager().putRelationCacheResult(query, newResult); + + //note about caching: we need a way to invalidate entries when a new row is now a valid part of the collection. + return ids; } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java index 436223c5..352f78b7 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java @@ -11,7 +11,6 @@ import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.lang.reflect.Array; import java.lang.reflect.Field; @@ -45,20 +44,21 @@ public static PersistentOneToManyCollectionImpl create public static void delegate(T instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable PersistentCollection> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentCollection.class)) { - PersistentCollectionMetadata collectionMetadata = metadata.persistentCollectionMetadata().get(pair.field()); - if (!(collectionMetadata instanceof PersistentOneToManyCollectionMetadata oneToManyMetadata)) continue; - - if (pair.instance() instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { - createAndDelegate((PersistentCollection.ProxyPersistentCollection) proxyCollection, oneToManyMetadata.getLinks(), oneToManyMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, oneToManyMetadata.getReferencedType(), oneToManyMetadata.getLinks())); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.persistentCollectionMetadata().entrySet()) { + Field field = entry.getKey(); + PersistentCollectionMetadata pcMetadata = entry.getValue(); + + if (!(pcMetadata instanceof PersistentOneToManyCollectionMetadata oneToManyMetadata)) continue; + Object value = field.get(instance); + if (value instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { + PersistentOneToManyCollectionImpl.createAndDelegate((PersistentCollection.ProxyPersistentCollection) proxyCollection, oneToManyMetadata.getLinks(), oneToManyMetadata); + } else { + field.set(instance, PersistentOneToManyCollectionImpl.create(instance, oneToManyMetadata.getReferencedType(), oneToManyMetadata.getLinks())); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } @@ -70,6 +70,7 @@ public static Map genericType = ReflectionUtils.getGenericType(field); if (genericType == null || !UniqueData.class.isAssignableFrom(genericType)) continue; Class referencedClass = genericType.asSubclass(UniqueData.class); + field.setAccessible(true); metadataMap.put(field, new PersistentOneToManyCollectionMetadata(dataManager, clazz, referencedClass, SQLBuilder.parseLinks(oneToManyAnnotation.link()))); } @@ -404,7 +405,6 @@ private SQLTransaction.Statement buildClearStatement() { public Set getIds() { // note: we need the join since we support linking on non-id columnsInReferringTable Preconditions.checkArgument(!holder.isDeleted(), "Cannot get entries on a deleted UniqueData instance"); - Set ids = new HashSet<>(); UniqueDataMetadata holderMetadata = holder.getMetadata(); UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); @@ -428,17 +428,28 @@ public Set getIds() { sqlBuilder.setLength(sqlBuilder.length() - 5); sqlBuilder.append(" WHERE "); - for (Link entry : link) { - String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\".\"").append(theirColumn).append("\" = \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(entry.columnInReferringTable()).append("\" AND "); - } for (ColumnValuePair columnValuePair : holder.getIdColumns()) { sqlBuilder.append("\"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(columnValuePair.column()).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); @Language("SQL") String sql = sqlBuilder.toString(); - try (ResultSet rs = dataAccessor.executeQuery(sql, holder.getIdColumns().stream().map(ColumnValuePair::value).toList())) { + + List values = new ArrayList<>(); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + values.add(columnValuePair.value()); + } +// +// SelectQuery query = new SelectQuery(sql, values); +// ReadCacheResult result = holder.getDataManager().getRelationCacheResult(query); +// +// if (result != null) { +// Set cachedIds = (Set) result.getValues(); +// return cachedIds; +// } + + Set ids = new HashSet<>(); + try (ResultSet rs = dataAccessor.executeQuery(sql, values)) { while (rs.next()) { int i = 0; ColumnValuePair[] idColumns = new ColumnValuePair[typeMetadata.idColumns().size()]; @@ -452,6 +463,26 @@ public Set getIds() { throw new RuntimeException(e); } +// Set dependencies = new HashSet<>(); +// for (Link entry : link) { +// String myColumn = entry.columnInReferringTable(); +// String theirColumn = entry.columnInReferencedTable(); +// dependencies.add(new Cell(holderMetadata.schema(), holderMetadata.table(), myColumn, holder.getIdColumns())); +// for (ColumnValuePairs themIdColumns : ids) { +// dependencies.add(new Cell(typeMetadata.schema(), typeMetadata.table(), theirColumn, themIdColumns)); +// } +// } +// +// for (ColumnValuePairs themIdColumns : ids) { +// for (ColumnMetadata idColumn : typeMetadata.idColumns()) { +// dependencies.add(new Cell(typeMetadata.schema(), typeMetadata.table(), idColumn.name(), themIdColumns)); +// } +// } +// +// holder.getDataManager().putRelationCacheResult(query, new ReadCacheResult(ids, dependencies)); + + //note about caching: we need a way to invalidate entries when a new row is now a valid part of the collection. + return ids; } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyValueCollectionImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyValueCollectionImpl.java index 2d05e2d5..c0c85f10 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyValueCollectionImpl.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyValueCollectionImpl.java @@ -11,7 +11,6 @@ import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.lang.reflect.Field; import java.sql.ResultSet; @@ -53,33 +52,34 @@ public static PersistentOneToManyValueCollectionImpl create(UniqueData ho public static void delegate(T instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable PersistentCollection> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentCollection.class)) { - PersistentCollectionMetadata collectionMetadata = metadata.persistentCollectionMetadata().get(pair.field()); - if (!(collectionMetadata instanceof PersistentOneToManyValueCollectionMetadata oneToManyValueMetadata)) - continue; - - if (pair.instance() instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { - createAndDelegate((ProxyPersistentCollection) proxyCollection, - oneToManyValueMetadata.getDataSchema(), - oneToManyValueMetadata.getDataTable(), - oneToManyValueMetadata.getDataColumn(), - oneToManyValueMetadata.getLinks(), - oneToManyValueMetadata - ); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, + try { + for (var entry : metadata.persistentCollectionMetadata().entrySet()) { + Field field = entry.getKey(); + PersistentCollectionMetadata pcMetadata = entry.getValue(); + + if (!(pcMetadata instanceof PersistentOneToManyValueCollectionMetadata oneToManyValueMetadata)) + continue; + Object value = field.get(instance); + if (value instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { + createAndDelegate((ProxyPersistentCollection) proxyCollection, + oneToManyValueMetadata.getDataSchema(), + oneToManyValueMetadata.getDataTable(), + oneToManyValueMetadata.getDataColumn(), + oneToManyValueMetadata.getLinks(), + oneToManyValueMetadata + ); + } else { + field.set(instance, create(instance, oneToManyValueMetadata.getDataType(), oneToManyValueMetadata.getDataSchema(), oneToManyValueMetadata.getDataTable(), oneToManyValueMetadata.getDataColumn(), oneToManyValueMetadata.getLinks() )); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } @@ -100,6 +100,7 @@ public static Map PersistentValueImpl create(UniqueData holder, Class data public static void delegate(T instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable PersistentValue> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentValue.class)) { - PersistentValueMetadata pvMetadata = metadata.persistentValueMetadata().get(pair.field()); - if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { - PersistentValueImpl.createAndDelegate(proxyPv, pvMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, PersistentValueImpl.create(instance, ReflectionUtils.getGenericType(pair.field()), pvMetadata)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.persistentValueMetadata().entrySet()) { + Field field = entry.getKey(); + PersistentValueMetadata pvMetadata = entry.getValue(); + + Object value = field.get(instance); + if (value instanceof PersistentValue.ProxyPersistentValue proxyPv) { + PersistentValueImpl.createAndDelegate(proxyPv, pvMetadata); + } else { + field.set(instance, PersistentValueImpl.create(instance, pvMetadata.getColumnMetadata().type(), pvMetadata)); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } public static Map extractMetadata(String schema, String table, Class clazz) { Map metadataMap = new HashMap<>(); for (Field field : ReflectionUtils.getFields(clazz, PersistentValue.class)) { + field.setAccessible(true); metadataMap.put(field, extractMetadata(schema, table, clazz, field)); } return metadataMap; diff --git a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java index a9edba76..bef979f2 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java @@ -5,6 +5,7 @@ import net.staticstudios.data.util.*; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Field; import java.util.function.Supplier; public class ReadOnlyCachedValue extends AbstractCachedValue { @@ -34,18 +35,20 @@ private static CachedValue create(UniqueData holder, Class dataType, C public static void delegate(U instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable CachedValue> pair : ReflectionUtils.getFieldInstancePairs(instance, CachedValue.class)) { - CachedValueMetadata cvMetadata = metadata.cachedValueMetadata().get(pair.field()); - if (pair.instance() instanceof CachedValue.ProxyCachedValue proxyPv) { - createAndDelegate(proxyPv, cvMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, ReflectionUtils.getGenericType(pair.field()), cvMetadata)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.cachedValueMetadata().entrySet()) { + Field field = entry.getKey(); + CachedValueMetadata cvMetadata = entry.getValue(); + + Object value = field.get(instance); + if (value instanceof CachedValue.ProxyCachedValue proxyCv) { + ReadOnlyCachedValue.createAndDelegate(proxyCv, cvMetadata); + } else { + field.set(instance, ReadOnlyCachedValue.create(instance, ReflectionUtils.getGenericType(field), cvMetadata)); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyPersistentValue.java b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyPersistentValue.java index c28946f1..541f120a 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyPersistentValue.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyPersistentValue.java @@ -2,8 +2,11 @@ import net.staticstudios.data.PersistentValue; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.util.*; -import org.jetbrains.annotations.Nullable; +import net.staticstudios.data.util.PersistentValueMetadata; +import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.ValueUpdateHandler; + +import java.lang.reflect.Field; public class ReadOnlyPersistentValue implements PersistentValue { private final T value; @@ -32,18 +35,20 @@ private static PersistentValue create(UniqueData holder, Class dataTyp public static void delegate(U instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable PersistentValue> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentValue.class)) { - PersistentValueMetadata pvMetadata = metadata.persistentValueMetadata().get(pair.field()); - if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { - createAndDelegate(proxyPv, pvMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, ReflectionUtils.getGenericType(pair.field()), pvMetadata)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.persistentValueMetadata().entrySet()) { + Field field = entry.getKey(); + PersistentValueMetadata pvMetadata = entry.getValue(); + + Object value = field.get(instance); + if (value instanceof PersistentValue.ProxyPersistentValue proxyPv) { + ReadOnlyPersistentValue.createAndDelegate(proxyPv, pvMetadata); + } else { + field.set(instance, ReadOnlyPersistentValue.create(instance, pvMetadata.getColumnMetadata().type(), pvMetadata)); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReference.java b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReference.java index b2eb3b49..4ba0f94d 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReference.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReference.java @@ -2,8 +2,12 @@ import net.staticstudios.data.Reference; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.util.*; -import org.jetbrains.annotations.Nullable; +import net.staticstudios.data.util.ColumnValuePairs; +import net.staticstudios.data.util.ReferenceMetadata; +import net.staticstudios.data.util.ReferenceUpdateHandler; +import net.staticstudios.data.util.UniqueDataMetadata; + +import java.lang.reflect.Field; public class ReadOnlyReference implements Reference { private final ColumnValuePairs referencedColumnValuePairs; @@ -20,30 +24,32 @@ private static void createAndDelegate(Reference.ProxyRefe ReadOnlyReference delegate = new ReadOnlyReference<>( proxy.getHolder(), proxy.getReferenceType(), - ReferenceImpl.create(proxy.getHolder(), proxy.getReferenceType(), metadata.links(), metadata.updateReferencedTable()).getReferencedColumnValuePairs() + ReferenceImpl.create(proxy.getHolder(), proxy.getReferenceType(), metadata).getReferencedColumnValuePairs() ); proxy.setDelegate(metadata, delegate); } private static Reference create(UniqueData holder, Class referenceType, ReferenceMetadata metadata) { - return new ReadOnlyReference<>(holder, referenceType, ReferenceImpl.create(holder, referenceType, metadata.links(), metadata.updateReferencedTable()).getReferencedColumnValuePairs()); + return new ReadOnlyReference<>(holder, referenceType, ReferenceImpl.create(holder, referenceType, metadata).getReferencedColumnValuePairs()); } public static void delegate(U instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable Reference> pair : ReflectionUtils.getFieldInstancePairs(instance, Reference.class)) { - ReferenceMetadata refMetadata = metadata.referenceMetadata().get(pair.field()); - if (pair.instance() instanceof Reference.ProxyReference proxyRef) { - createAndDelegate(proxyRef, refMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, refMetadata.referencedClass(), refMetadata)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.referenceMetadata().entrySet()) { + Field field = entry.getKey(); + ReferenceMetadata referenceMetadata = entry.getValue(); + + Object value = field.get(instance); + if (value instanceof Reference.ProxyReference proxyRef) { + ReadOnlyReference.createAndDelegate(proxyRef, referenceMetadata); + } else { + field.set(instance, ReadOnlyReference.create(instance, referenceMetadata.referencedClass(), referenceMetadata)); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReferenceCollection.java b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReferenceCollection.java index 6570ff81..7b53781f 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReferenceCollection.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReferenceCollection.java @@ -4,9 +4,9 @@ import net.staticstudios.data.UniqueData; import net.staticstudios.data.util.*; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.lang.reflect.Array; +import java.lang.reflect.Field; import java.util.Collection; import java.util.Iterator; @@ -51,31 +51,30 @@ private static ReadOnlyReferenceCollection create(Uniq public static void delegate(U instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable PersistentCollection> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentCollection.class)) { - PersistentCollectionMetadata collectionMetadata = metadata.persistentCollectionMetadata().get(pair.field()); - if (collectionMetadata instanceof PersistentOneToManyCollectionMetadata oneToManyMetadata) { - if (pair.instance() instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { - createAndDelegate(proxyCollection, oneToManyMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, oneToManyMetadata.getReferencedType(), oneToManyMetadata)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.persistentCollectionMetadata().entrySet()) { + Field field = entry.getKey(); + PersistentCollectionMetadata pcMetadata = entry.getValue(); + + if (pcMetadata instanceof PersistentOneToManyCollectionMetadata oneToManyMetadata) { + Object value = field.get(instance); + if (value instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { + createAndDelegate(proxyCollection, oneToManyMetadata); + } else { + field.set(instance, create(instance, oneToManyMetadata.getReferencedType(), oneToManyMetadata)); } - } - } else if (collectionMetadata instanceof PersistentManyToManyCollectionMetadata manyToManyMetadata) { - if (pair.instance() instanceof PersistentCollection.ProxyPersistentCollection proxyPv) { - createAndDelegate(proxyPv, manyToManyMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, manyToManyMetadata.getReferencedType(), manyToManyMetadata)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + + } else if (pcMetadata instanceof PersistentManyToManyCollectionMetadata manyToManyMetadata) { + Object value = field.get(instance); + if (value instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { + createAndDelegate(proxyCollection, manyToManyMetadata); + } else { + field.set(instance, create(instance, manyToManyMetadata.getReferencedType(), manyToManyMetadata)); } } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyValuedCollection.java b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyValuedCollection.java index 32845228..1f6fff9a 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyValuedCollection.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyValuedCollection.java @@ -4,8 +4,8 @@ import net.staticstudios.data.UniqueData; import net.staticstudios.data.util.*; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Field; import java.util.*; public class ReadOnlyValuedCollection implements PersistentCollection { @@ -37,21 +37,22 @@ private static ReadOnlyValuedCollection create(UniqueData holder, Class void delegate(U instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable PersistentCollection> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentCollection.class)) { - PersistentCollectionMetadata collectionMetadata = metadata.persistentCollectionMetadata().get(pair.field()); - if (!(collectionMetadata instanceof PersistentOneToManyValueCollectionMetadata oneToManyValueMetadata)) - continue; - - if (pair.instance() instanceof PersistentCollection.ProxyPersistentCollection proxyPv) { - createAndDelegate(proxyPv, oneToManyValueMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, ReflectionUtils.getGenericType(pair.field()), oneToManyValueMetadata)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.persistentCollectionMetadata().entrySet()) { + Field field = entry.getKey(); + PersistentCollectionMetadata pcMetadata = entry.getValue(); + + if (!(pcMetadata instanceof PersistentOneToManyValueCollectionMetadata oneToManyValueMetadata)) + continue; + Object value = field.get(instance); + if (value instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { + createAndDelegate(proxyCollection, oneToManyValueMetadata); + } else { + field.set(instance, create(instance, ReflectionUtils.getGenericType(field), oneToManyValueMetadata)); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java index da00e2d9..862d544a 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -1,6 +1,7 @@ package net.staticstudios.data.impl.data; import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; import net.staticstudios.data.OneToOne; import net.staticstudios.data.Reference; import net.staticstudios.data.UniqueData; @@ -14,59 +15,58 @@ import java.lang.reflect.Field; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public class ReferenceImpl implements Reference { private final UniqueData holder; private final Class type; private final List link; private final boolean updateReferencedTable; + private final ReferenceMetadata metadata; - public ReferenceImpl(UniqueData holder, Class type, List link, boolean updateReferencedTable) { + public ReferenceImpl(UniqueData holder, ReferenceMetadata metadata) { this.holder = holder; - this.type = type; - this.link = link; - this.updateReferencedTable = updateReferencedTable; + this.type = (Class) metadata.referencedClass(); + this.link = metadata.links(); + this.updateReferencedTable = metadata.updateReferencedTable(); + this.metadata = metadata; } public static void createAndDelegate(Reference.ProxyReference proxy, ReferenceMetadata metadata) { ReferenceImpl delegate = new ReferenceImpl<>( proxy.getHolder(), - proxy.getReferenceType(), - metadata.links(), - metadata.updateReferencedTable() + metadata ); proxy.setDelegate(metadata, delegate); } - public static ReferenceImpl create(UniqueData holder, Class type, List link, boolean updateReferencedTable) { - return new ReferenceImpl<>(holder, type, link, updateReferencedTable); + public static ReferenceImpl create(UniqueData holder, Class type, ReferenceMetadata metadata) { + return new ReferenceImpl<>(holder, metadata); } public static void delegate(T instance) { UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); - for (FieldInstancePair<@Nullable Reference> pair : ReflectionUtils.getFieldInstancePairs(instance, Reference.class)) { - ReferenceMetadata refMetadata = metadata.referenceMetadata().get(pair.field()); - - if (pair.instance() instanceof Reference.ProxyReference proxyRef) { - createAndDelegate(proxyRef, refMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, create(instance, refMetadata.referencedClass(), refMetadata.links(), refMetadata.updateReferencedTable())); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + try { + for (var entry : metadata.referenceMetadata().entrySet()) { + Field field = entry.getKey(); + ReferenceMetadata referenceMetadata = entry.getValue(); + + Object value = field.get(instance); + if (value instanceof Reference.ProxyReference proxyRef) { + ReferenceImpl.createAndDelegate(proxyRef, referenceMetadata); + } else { + field.set(instance, ReferenceImpl.create(instance, referenceMetadata.referencedClass(), referenceMetadata)); } } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); } } public static Map extractMetadata(Class clazz) { Map metadataMap = new HashMap<>(); for (Field field : ReflectionUtils.getFields(clazz, Reference.class)) { + field.setAccessible(true); OneToOne oneToOneAnnotation = field.getAnnotation(OneToOne.class); Preconditions.checkNotNull(oneToOneAnnotation, "Field %s in class %s is missing @OneToOne annotation".formatted(field.getName(), clazz.getName())); Class referencedClass = ReflectionUtils.getGenericType(field); @@ -103,39 +103,37 @@ public Reference onUpdate(Class holderClass, Refere public ColumnValuePairs getReferencedColumnValuePairs() { Preconditions.checkArgument(!holder.isDeleted(), "Cannot get reference on a deleted UniqueData instance"); - UniqueDataMetadata holderMetadata = holder.getMetadata(); UniqueDataMetadata referencedMetadata = holder.getDataManager().getMetadata(type); DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); - StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append("SELECT "); - for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { - sqlBuilder.append("_referenced.\"").append(idColumn.name()).append("\", "); - } - for (Link entry : link) { - String myColumn = entry.columnInReferringTable(); - sqlBuilder.append("_referring.\"").append(myColumn).append("\", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(" FROM \"").append(referencedMetadata.schema()).append("\".\"").append(referencedMetadata.table()).append("\" _referenced"); - sqlBuilder.append(" INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" _referring ON "); - for (Link entry : link) { - String myColumn = entry.columnInReferringTable(); - String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("_referenced.\"").append(theirColumn).append("\" = "); - sqlBuilder.append("_referring.\"").append(myColumn).append("\" AND "); - } - sqlBuilder.setLength(sqlBuilder.length() - 5); + DataManager dataManager = holder.getDataManager(); - sqlBuilder.append(" WHERE "); + List values = new ArrayList<>(holder.getIdColumns().getPairs().length); for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - sqlBuilder.append("_referring.\"").append(columnValuePair.column()).append("\" = ? AND "); + values.add(columnValuePair.value()); } - sqlBuilder.setLength(sqlBuilder.length() - 5); - @Language("SQL") String sql = sqlBuilder.toString(); - try (ResultSet rs = dataAccessor.executeQuery(sql, holder.getIdColumns().stream().map(ColumnValuePair::value).toList())) { + SelectQuery query = metadata.buildSelectReferencedColumnValuePairsSelectQuery(holder.getDataManager(), values); + + ReadCacheResult cached = dataManager.getRelationCacheResult(query); + + if (cached != null) { + ColumnValuePairs columnValuePairs = (ColumnValuePairs) cached.getValue(); + List refIdColumns = referencedMetadata.idColumns(); + ColumnValuePair[] idColumns = new ColumnValuePair[refIdColumns.size()]; + for (int i = 0; i < refIdColumns.size(); i++) { + ColumnMetadata idColumn = refIdColumns.get(i); + Object val = ColumnValuePairs.getValue(idColumn.name(), columnValuePairs); + if (val == null) { + return null; + } + idColumns[i] = new ColumnValuePair(idColumn.name(), val); + } + return new ColumnValuePairs(idColumns); + } + + try (ResultSet rs = dataAccessor.executeQuery(query.getSql(), query.getValues())) { if (!rs.next()) { return null; } @@ -157,7 +155,28 @@ public ColumnValuePairs getReferencedColumnValuePairs() { } idColumns[i] = new ColumnValuePair(idColumn.name(), val); } - return new ColumnValuePairs(idColumns); + + ColumnValuePairs theirIdColumns = new ColumnValuePairs(idColumns); + + Set dependencies = new HashSet<>(); + for (Link entry : link) { + String myColumn = entry.columnInReferringTable(); + String theirColumn = entry.columnInReferencedTable(); + dependencies.add(new Cell(holder.getMetadata().schema(), holder.getMetadata().table(), myColumn, holder.getIdColumns())); + dependencies.add(new Cell(referencedMetadata.schema(), referencedMetadata.table(), theirColumn, theirIdColumns)); + } + + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + dependencies.add(new Cell(holder.getMetadata().schema(), holder.getMetadata().table(), columnValuePair.column(), holder.getIdColumns())); + } + for (ColumnValuePair columnValuePair : theirIdColumns) { + dependencies.add(new Cell(referencedMetadata.schema(), referencedMetadata.table(), columnValuePair.column(), theirIdColumns)); + } + + ReadCacheResult cacheResult = new ReadCacheResult(theirIdColumns, dependencies); + dataManager.putRelationCacheResult(query, cacheResult); + + return theirIdColumns; } catch (SQLException e) { throw new RuntimeException(e); } diff --git a/core/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/core/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index 491a420f..fa1749ab 100644 --- a/core/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/core/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -5,7 +5,9 @@ import com.impossibl.postgres.api.jdbc.PGConnection; import net.staticstudios.data.DataManager; import net.staticstudios.data.InsertMode; +import net.staticstudios.data.StaticDataStatistics; import net.staticstudios.data.impl.DataAccessor; +import net.staticstudios.data.impl.h2.trigger.H2ReadCacheInvalidatorTrigger; import net.staticstudios.data.impl.h2.trigger.H2UpdateHandlerTrigger; import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.impl.redis.RedisEncodedValue; @@ -18,6 +20,7 @@ import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.data.util.redis.RedisUtils; import net.staticstudios.utils.Pair; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; @@ -61,12 +64,14 @@ public class H2DataAccessor implements DataAccessor { return t; }); private final RedisListener redisListener; - private final Map redisCache = new ConcurrentHashMap<>(); private final Set knownRedisPartialKeys = ConcurrentHashMap.newKeySet(); private final ThreadLocal> commitCallbacks = ThreadLocal.withInitial(LinkedList::new); private final ThreadLocal> rollbackCallbacks = ThreadLocal.withInitial(LinkedList::new); + private final SlidingWindowCounter h2QueryCounter = new SlidingWindowCounter(10_000, 20); + private final SlidingWindowCounter h2UpdateCounter = new SlidingWindowCounter(10_000, 20); + public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener, RedisListener redisListener, TaskQueue taskQueue) { try { Class.forName("org.h2.Driver"); @@ -153,6 +158,7 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener } logger.debug("[H2] [HANDLE POSTGRES UPDATE] {}", sql); preparedStatement.executeUpdate(); + h2UpdateCounter.increment(); if (!connection.getAutoCommit()) { connection.commit(); } @@ -187,6 +193,7 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener } logger.debug("[H2] [HANDLE POSTGRES INSERT] {}", sql); preparedStatement.executeUpdate(); + h2UpdateCounter.increment(); if (!connection.getAutoCommit()) { connection.commit(); } @@ -215,6 +222,7 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener } logger.debug("[H2] [HANDLE POSTGRES DELETE] {}", sql); preparedStatement.executeUpdate(); + h2UpdateCounter.increment(); if (!connection.getAutoCommit()) { connection.commit(); } @@ -317,7 +325,12 @@ public synchronized void sync(List schemaTables, List redis cursor = scanResult.getCursor(); for (String key : scanResult.getResult()) { - redisCache.put(key, decodeRedis(jedis.get(key)).value()); + RedisUtils.FullyDeconstructedKey fullyDeconstructedKey = RedisUtils.fullyDeconstruct(key, dataManager); + if (fullyDeconstructedKey == null) { + continue; // we aren't tracking this table + } + + setRedisValueCache(fullyDeconstructedKey.holderSchema(), fullyDeconstructedKey.holderTable(), fullyDeconstructedKey.identifier(), fullyDeconstructedKey.idColumns(), decodeRedis(jedis.get(key)).value()); } } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); @@ -386,6 +399,7 @@ public void insert(List sqlStatements, InsertMode insertMode) thro } logger.trace("[H2] {}", sqlStatement.getH2Sql()); preparedStatement.executeUpdate(); + h2UpdateCounter.increment(); } } @@ -437,6 +451,7 @@ public ResultSet executeQuery(@Language("SQL") String sql, List values) cachePreparedStatement.setObject(i + 1, values.get(i)); } logger.trace("[H2] {}", sql); + h2QueryCounter.increment(); return cachePreparedStatement.executeQuery(); } @@ -459,7 +474,9 @@ public void executeTransaction(SQLTransaction transaction, int delay) throws SQL Consumer resultHandler = operation.getResultHandler(); if (resultHandler == null) { cachePreparedStatement.executeUpdate(); + h2UpdateCounter.increment(); } else { + h2QueryCounter.increment(); try (ResultSet rs = cachePreparedStatement.executeQuery()) { resultHandler.accept(rs); } @@ -509,20 +526,49 @@ public void postDDL() throws SQLException { } @Override - public @Nullable String getRedisValue(String key) { - return redisCache.get(key); + public @Nullable String getRedisValue(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns) { + String columnName = RedisUtils.getVirtualColumnName(identifier); + + StringBuilder sqlBuilder = new StringBuilder().append("SELECT \"").append(columnName).append("\" FROM \"").append(holderSchema).append("\".\"").append(holderTable).append("\" WHERE "); + for (ColumnValuePair columnValuePair : idColumns) { + String name = columnValuePair.column(); + sqlBuilder.append("\"").append(name).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + + try { + PreparedStatement preparedStatement = prepareStatement(sql); + int i = 1; + for (ColumnValuePair columnValuePair : idColumns) { + preparedStatement.setObject(i++, columnValuePair.value()); + } + logger.trace("[H2] {}", sql); + h2QueryCounter.increment(); + try (ResultSet rs = preparedStatement.executeQuery()) { + if (rs.next()) { + return rs.getString(1); + } else { + return null; + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } } @Override - public void setRedisValue(String key, String value, int expirationSeconds) { - String prev; + public void setRedisValue(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns, String value, int expirationSeconds) { + String prev = getAndSetRedisValueCache(holderSchema, holderTable, identifier, idColumns, value); + if (Objects.equals(prev, value)) { + return; // no change + } + String key = RedisUtils.buildRedisKey(holderSchema, holderTable, identifier, idColumns); if (value == null) { - prev = redisCache.remove(key); taskQueue.submitTask((connection, jedis) -> { jedis.del(key); }); } else { - prev = redisCache.put(key, value); taskQueue.submitTask((connection, jedis) -> { if (expirationSeconds > 0) { jedis.setex(key, expirationSeconds, encodeRedis(value)); @@ -536,6 +582,110 @@ public void setRedisValue(String key, String value, int expirationSeconds) { dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), prev, value); } + private void setRedisValueCache(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns, String value) { + String columnName = RedisUtils.getVirtualColumnName(identifier); + + StringBuilder sqlBuilder = new StringBuilder().append("UPDATE \"").append(holderSchema).append("\".\"").append(holderTable).append("\" SET \"").append(columnName).append("\" = ? WHERE "); + for (ColumnValuePair columnValuePair : idColumns) { + String name = columnValuePair.column(); + sqlBuilder.append("\"").append(name).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + + try { + PreparedStatement preparedStatement = prepareStatement(sql); + preparedStatement.setString(1, value); + int i = 2; + for (ColumnValuePair columnValuePair : idColumns) { + preparedStatement.setObject(i++, columnValuePair.value()); + } + logger.trace("[H2] {}", sql); + preparedStatement.executeUpdate(); + h2UpdateCounter.increment(); + + if (!getConnection().getAutoCommit()) { + getConnection().commit(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private @Nullable String getAndSetRedisValueCache(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns, @Nullable String value) { + String columnName = RedisUtils.getVirtualColumnName(identifier); + try { + String oldValue = null; + + Connection connection = getConnection(); + + boolean autoCommit = connection.getAutoCommit(); + connection.setAutoCommit(false); + + try { + StringBuilder selectSb = new StringBuilder() + .append("SELECT \"").append(columnName).append("\" FROM \"") + .append(holderSchema).append("\".\"").append(holderTable).append("\" WHERE "); + for (ColumnValuePair columnValuePair : idColumns) { + selectSb.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + } + selectSb.setLength(selectSb.length() - 5); + + PreparedStatement preparedStatement = prepareStatement(selectSb.toString()); + + int i = 1; + for (ColumnValuePair columnValuePair : idColumns) { + preparedStatement.setObject(i++, columnValuePair.value()); + } + + logger.trace("[H2] {}", selectSb); + h2QueryCounter.increment(); + try (ResultSet rs = preparedStatement.executeQuery()) { + if (rs.next()) { + oldValue = rs.getString(1); + } + } + + if (Objects.equals(oldValue, value)) { + return oldValue; + } + + StringBuilder updateSb = new StringBuilder() + .append("UPDATE \"").append(holderSchema).append("\".\"").append(holderTable) + .append("\" SET \"").append(columnName).append("\" = ? WHERE "); + for (ColumnValuePair columnValuePair : idColumns) { + updateSb.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + } + updateSb.setLength(updateSb.length() - 5); + + preparedStatement = prepareStatement(updateSb.toString()); + preparedStatement.setString(1, value); + + i = 2; + for (ColumnValuePair columnValuePair : idColumns) { + preparedStatement.setObject(i++, columnValuePair.value()); + } + + logger.trace("[H2] {}", updateSb); + preparedStatement.executeUpdate(); + h2UpdateCounter.increment(); + } catch (Exception e) { + connection.rollback(); + logger.error("Error updating Redis cache in H2", e); + } finally { + if (autoCommit) { + connection.setAutoCommit(true); + } else { + connection.commit(); + } + } + + return oldValue; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + @Override public void discoverRedisKeys(List partialRedisKeys) { knownRedisPartialKeys.addAll(partialRedisKeys); @@ -568,22 +718,35 @@ private synchronized void updateKnownTables() throws SQLException { if (!knownTables.contains(schemaTable)) { logger.debug("Discovered new referringTable {}.{}", schema, table); - UUID randomId = UUID.randomUUID(); - @Language("SQL") String sql = "CREATE TRIGGER IF NOT EXISTS \"insert_update_trg_%s_%s\" AFTER INSERT, UPDATE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; + @Language("SQL") String sql = "CREATE TRIGGER IF NOT EXISTS \"insert_update_handler_trg_%s_%s\" AFTER INSERT, UPDATE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; try (Statement createTrigger = connection.createStatement()) { - String formatted = sql.formatted(table, randomId.toString().replace('-', '_'), schema, table, H2UpdateHandlerTrigger.class.getName()); + String formatted = sql.formatted(table, dataManager.getApplicationId().toString().replace('-', '_'), schema, table, H2UpdateHandlerTrigger.class.getName()); logger.trace("[H2] {}", formatted); - H2UpdateHandlerTrigger.registerDataManager(randomId, dataManager); createTrigger.execute(formatted); } - sql = "CREATE TRIGGER IF NOT EXISTS \"delete_trg_%s_%s\" BEFORE DELETE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; + sql = "CREATE TRIGGER IF NOT EXISTS \"delete_handler_trg_%s_%s\" BEFORE DELETE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; try (Statement createTrigger = connection.createStatement()) { - String formatted = sql.formatted(table, randomId.toString().replace('-', '_'), schema, table, H2UpdateHandlerTrigger.class.getName()); + String formatted = sql.formatted(table, dataManager.getApplicationId().toString().replace('-', '_'), schema, table, H2UpdateHandlerTrigger.class.getName()); + logger.trace("[H2] {}", formatted); + createTrigger.execute(formatted); + } + + sql = "CREATE TRIGGER IF NOT EXISTS \"insert_update_cache_trg_%s_%s\" AFTER INSERT, UPDATE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; + + try (Statement createTrigger = connection.createStatement()) { + String formatted = sql.formatted(table, dataManager.getApplicationId().toString().replace('-', '_'), schema, table, H2ReadCacheInvalidatorTrigger.class.getName()); + logger.trace("[H2] {}", formatted); + createTrigger.execute(formatted); + } + + sql = "CREATE TRIGGER IF NOT EXISTS \"delete_cache_trg_%s_%s\" BEFORE DELETE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; + + try (Statement createTrigger = connection.createStatement()) { + String formatted = sql.formatted(table, dataManager.getApplicationId().toString().replace('-', '_'), schema, table, H2ReadCacheInvalidatorTrigger.class.getName()); logger.trace("[H2] {}", formatted); - H2UpdateHandlerTrigger.registerDataManager(randomId, dataManager); createTrigger.execute(formatted); } @@ -604,7 +767,13 @@ private List getColumnsInTable(String schema, String table) throws SQLEx ps.setString(2, table); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { - columns.add(rs.getString("COLUMN_NAME")); + String columnName = rs.getString("COLUMN_NAME"); + + if (columnName.startsWith("__virtual__")) { + continue; + } + + columns.add(columnName); } } } @@ -671,23 +840,34 @@ private void handleRedisEvent(RedisEvent event, String key, @Nullable String val return; // ignore events from ourselves } + RedisUtils.FullyDeconstructedKey fullyDeconstructedKey = RedisUtils.fullyDeconstruct(key, dataManager); + if (fullyDeconstructedKey == null) { + return; // we aren't tracking this key + } if (event == RedisEvent.SET) { - String entry = redisCache.get(key); - if (entry != null && Objects.equals(entry, redisValue)) { + String prev = getAndSetRedisValueCache(fullyDeconstructedKey.holderSchema(), fullyDeconstructedKey.holderTable(), fullyDeconstructedKey.identifier(), fullyDeconstructedKey.idColumns(), redisValue); + if (prev != null && Objects.equals(prev, redisValue)) { return; } - redisCache.put(key, redisValue); RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); - dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), entry, redisValue); + dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), prev, redisValue); } else if (event == RedisEvent.DEL || event == RedisEvent.EXPIRED) { - String entry = redisCache.remove(key); - if (entry != null) { + String prev = getAndSetRedisValueCache(fullyDeconstructedKey.holderSchema(), fullyDeconstructedKey.holderTable(), fullyDeconstructedKey.identifier(), fullyDeconstructedKey.idColumns(), null); + if (prev != null) { RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); - dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), entry, null); + dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), prev, null); } } } + public double getH2QueriesPerSecond() { + return h2QueryCounter.getPerSecond(); + } + + public double getH2UpdatesPerSecond() { + return h2UpdateCounter.getPerSecond(); + } + public void onCommit(Runnable callback) { commitCallbacks.get().add(callback); } @@ -733,4 +913,10 @@ private String encodeRedis(Object value) { private RedisEncodedValue decodeRedis(String encoded) { return GSON.fromJson(encoded, RedisEncodedValue.class); } + + @Override + public void populateStatistics(StaticDataStatistics stats) { + stats.setQueriesPerSecond((long) getH2QueriesPerSecond()); + stats.setUpdatesPerSecond((long) getH2UpdatesPerSecond()); + } } diff --git a/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2ReadCacheInvalidatorTrigger.java b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2ReadCacheInvalidatorTrigger.java new file mode 100644 index 00000000..00631f97 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2ReadCacheInvalidatorTrigger.java @@ -0,0 +1,91 @@ +package net.staticstudios.data.impl.h2.trigger; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.impl.h2.H2DataAccessor; +import org.h2.api.Trigger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +public class H2ReadCacheInvalidatorTrigger implements Trigger { + private final Logger logger = LoggerFactory.getLogger(H2ReadCacheInvalidatorTrigger.class); + private final List columnNames = new ArrayList<>(); + private DataManager dataManager; + private H2DataAccessor dataAccessor; + private String schema; + private String table; + + + @Override + public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException { + UUID dataManagerId = UUID.fromString(triggerName.substring(triggerName.length() - 36).replace('_', '-')); + this.table = triggerName.substring(triggerName.indexOf("_trg_") + 5, triggerName.length() - 37); //dont use referringTable name since it might be a copy for an internal referringTable (very odd behavior i must say h2) + this.dataManager = DataManager.getInstance(dataManagerId); + this.dataAccessor = (H2DataAccessor) dataManager.getDataAccessor(); + this.schema = schemaName; + } + + @Override + public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException { + //todo: when were syncing data, we should ignore all triggers. we should globally pause basically everything. + int dataLength = oldRow != null ? oldRow.length : (newRow != null ? newRow.length : 0); + if (columnNames.size() != dataLength) { + List columns = new ArrayList<>(dataLength); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION" + )) { + ps.setString(1, schema); + ps.setString(2, table); // H2 stores names in uppercase by default + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + columns.add(rs.getString("COLUMN_NAME")); + } + } + } + logger.trace("Schema change detected (or first run). Old name names: {}, new name names: {}", columnNames, columns); + columnNames.clear(); + columnNames.addAll(columns); + } + + + if (newRow == null && oldRow != null) { + logger.trace("Delete detected: oldRow={}", (Object) oldRow); + handleDelete(oldRow); + } else if (oldRow != null) { + logger.trace("Update detected: oldRow={}, newRow={}", oldRow, newRow); + handleUpdate(oldRow, newRow); + } + } + + + private void handleUpdate(Object[] oldRow, Object[] newRow) { + List changedColumns = new ArrayList<>(); + for (int i = 0; i < oldRow.length; i++) { + Object oldValue = oldRow[i]; + Object newValue = newRow[i]; + if (!Objects.equals(oldValue, newValue)) { + changedColumns.add(columnNames.get(i)); + } + } + + dataAccessor.onCommit(() -> { + dataManager.invalidateRelationCache(columnNames, schema, table, changedColumns, oldRow); + dataManager.invalidateCellCache(columnNames, schema, table, changedColumns, oldRow); + }); + } + + private void handleDelete(Object[] oldRow) { + dataAccessor.onCommit(() -> { + dataManager.invalidateRelationCache(columnNames, schema, table, columnNames, oldRow); + dataManager.invalidateCellCache(columnNames, schema, table, columnNames, oldRow); + }); + } +} diff --git a/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java index 63a6a186..6f36a745 100644 --- a/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java +++ b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java @@ -13,11 +13,12 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; public class H2UpdateHandlerTrigger implements Trigger { - private static final Map dataManagerMap = new ConcurrentHashMap<>(); private final Logger logger = LoggerFactory.getLogger(H2UpdateHandlerTrigger.class); private final List columnNames = new ArrayList<>(); private DataManager dataManager; @@ -25,15 +26,11 @@ public class H2UpdateHandlerTrigger implements Trigger { private String schema; private String table; - public static void registerDataManager(UUID id, DataManager dataManager) { - dataManagerMap.put(id, dataManager); - } - @Override public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException { UUID dataManagerId = UUID.fromString(triggerName.substring(triggerName.length() - 36).replace('_', '-')); this.table = triggerName.substring(triggerName.indexOf("_trg_") + 5, triggerName.length() - 37); //dont use referringTable name since it might be a copy for an internal referringTable (very odd behavior i must say h2) - this.dataManager = dataManagerMap.get(dataManagerId); + this.dataManager = DataManager.getInstance(dataManagerId); this.dataAccessor = (H2DataAccessor) dataManager.getDataAccessor(); this.schema = schemaName; } diff --git a/core/src/main/java/net/staticstudios/data/impl/redis/RedisListener.java b/core/src/main/java/net/staticstudios/data/impl/redis/RedisListener.java index 85f063d0..a0ba0062 100644 --- a/core/src/main/java/net/staticstudios/data/impl/redis/RedisListener.java +++ b/core/src/main/java/net/staticstudios/data/impl/redis/RedisListener.java @@ -1,8 +1,8 @@ package net.staticstudios.data.impl.redis; import net.staticstudios.data.util.DataSourceConfig; -import net.staticstudios.data.util.RedisUtils; import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.data.util.redis.RedisUtils; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; import org.slf4j.Logger; diff --git a/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index ff74e789..c84831df 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -4,6 +4,7 @@ import net.staticstudios.data.*; import net.staticstudios.data.impl.data.PersistentManyToManyCollectionImpl; import net.staticstudios.data.util.*; +import net.staticstudios.data.util.redis.RedisUtils; import net.staticstudios.data.utils.Link; import net.staticstudios.data.utils.StringUtils; import org.intellij.lang.annotations.Language; @@ -69,6 +70,9 @@ public List parse(Class clazz) { for (Class visitedClass : visited) { parseIndividualColumns(visitedClass, schemas); } + for (Class visitedClass : visited) { + parseIndividualCachedValues(visitedClass, schemas); + } for (Class visitedClass : visited) { parseIndividualRelations(visitedClass, schemas); } @@ -158,6 +162,11 @@ private List getDefs(Collection schemas) { h2Sb.append(";"); pgSb.append(";"); + + if (column.isVirtual()) { + pgSb.setLength(0); + } + statements.add(DDLStatement.of(h2Sb.toString(), pgSb.toString())); } } @@ -170,7 +179,11 @@ private List getDefs(Collection schemas) { if (column.isIndexed() && !column.isUnique()) { String indexName = "idx_" + schema.getName() + "_" + table.getName() + "_" + column.getName(); @Language("SQL") String h2 = "CREATE INDEX IF NOT EXISTS " + indexName + " ON \"" + schema.getName() + "\".\"" + table.getName() + "\" (\"" + column.getName() + "\");"; - statements.add(DDLStatement.both(h2)); + if (column.isVirtual()) { + statements.add(DDLStatement.of(h2, "")); + } else { + statements.add(DDLStatement.both(h2)); + } } } } @@ -291,6 +304,21 @@ private void parseIndividualColumns(Class clazz, Map clazz, Map schemas) { + logger.trace("Parsing cached values for class {}", clazz.getName()); + UniqueDataMetadata metadata = dataManager.getMetadata(clazz); + if (!clazz.isAnnotationPresent(Data.class)) { + throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @Data"); + } + + Data dataAnnotation = clazz.getAnnotation(Data.class); + Preconditions.checkNotNull(dataAnnotation, "Data annotation is null for class " + clazz.getName()); + + for (Field field : ReflectionUtils.getFields(clazz)) { + parseCachedValue(clazz, schemas, dataAnnotation, metadata, field); + } + } + private void parseIndividualRelations(Class clazz, Map schemas) { logger.trace("Parsing relations for class {}", clazz.getName()); UniqueDataMetadata metadata = dataManager.getMetadata(clazz); @@ -405,7 +433,7 @@ private void parseColumn(Class clazz, Map clazz, Map type = dataManager.getSerializedType(ReflectionUtils.getGenericType(field)); - SQLColumn sqlColumn = new SQLColumn(table, type, columnName, nullable, indexed, unique, defaultValue.isEmpty() ? null : SQLUtils.parseDefaultValue(type, defaultValue)); + SQLColumn sqlColumn = new SQLColumn(table, type, columnName, nullable, indexed, unique, defaultValue.isEmpty() ? null : SQLUtils.parseDefaultValue(type, defaultValue), false); SQLColumn existingColumn = table.getColumn(columnName); if (existingColumn != null) { @@ -467,6 +495,35 @@ private void parseColumn(Class clazz, Map clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { + if (!field.getType().equals(CachedValue.class)) { + return; + } + String dataSchema = ValueUtils.parseValue(dataAnnotation.schema()); + String dataTable = ValueUtils.parseValue(dataAnnotation.table()); + + Identifier identifier = field.getAnnotation(Identifier.class); + Preconditions.checkNotNull(identifier, "CachedValue field " + field.getName() + " in class " + clazz.getName() + " must be annotated with @Identifier"); + String identifierValue = ValueUtils.parseValue(identifier.value()); + + SQLSchema schema = schemas.computeIfAbsent(dataSchema, SQLSchema::new); + SQLTable table = schema.getTable(dataTable); + + if (table == null) { + table = new SQLTable(schema, dataTable, metadata.idColumns()); + schema.addTable(table); + } + + SQLColumn sqlColumn = new SQLColumn(table, String.class, RedisUtils.getVirtualColumnName(identifierValue), true, identifier.index(), false, null, true); + SQLColumn existingColumn = table.getColumn(sqlColumn.getName()); + if (existingColumn != null) { + Preconditions.checkState(existingColumn.equals(sqlColumn), "CachedValue column " + sqlColumn.getName() + " in referringTable " + table.getName() + " has conflicting definitions! Existing: " + existingColumn + ", New: " + sqlColumn); + return; + } + + table.addColumn(sqlColumn); + } + private void parseReference(Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { if (!field.getType().equals(Reference.class)) { return; @@ -560,11 +617,11 @@ private void parseOneToManyValuePersistentCollection(OneToMany oneToMany, Class< referencedTable = new SQLTable(referencedSchema, referencedTableName, idColumns); for (ColumnMetadata idCol : referencedTable.getIdColumns()) { Preconditions.checkState(referencedTable.getColumn(idCol.name()) == null, "ID column name " + idCol.name() + " in referringTable " + referencedTableName + " is duplicated!"); - SQLColumn sqlColumn = new SQLColumn(referencedTable, dataManager.getSerializedType(idCol.type()), idCol.name(), false, false, true, null); + SQLColumn sqlColumn = new SQLColumn(referencedTable, dataManager.getSerializedType(idCol.type()), idCol.name(), false, false, true, null, false); referencedTable.addColumn(sqlColumn); } referencedSchema.addTable(referencedTable); - referencedTable.addColumn(new SQLColumn(referencedTable, dataManager.getSerializedType(genericType), referencedColumnName, oneToMany.nullable(), oneToMany.indexed(), oneToMany.unique(), null)); + referencedTable.addColumn(new SQLColumn(referencedTable, dataManager.getSerializedType(genericType), referencedColumnName, oneToMany.nullable(), oneToMany.indexed(), oneToMany.unique(), null, false)); for (Link link : parseLinks(oneToMany.link())) { Class columnType = null; SQLColumn columnInReferringTable = table.getColumn(link.columnInReferringTable()); @@ -572,7 +629,7 @@ private void parseOneToManyValuePersistentCollection(OneToMany oneToMany, Class< columnType = columnInReferringTable.getType(); } Preconditions.checkNotNull(columnType, "Link name %s in OneToMany annotation on field %s in class %s is not an ID name", link.columnInReferringTable(), field.getName(), clazz.getName()); - SQLColumn linkingColumn = new SQLColumn(referencedTable, dataManager.getSerializedType(columnType), link.columnInReferencedTable(), false, false, false, null); + SQLColumn linkingColumn = new SQLColumn(referencedTable, dataManager.getSerializedType(columnType), link.columnInReferencedTable(), false, false, false, null, false); referencedTable.addColumn(linkingColumn); } @@ -683,7 +740,7 @@ private void parseManyToManyPersistentCollection(ManyToMany manyToMany, Class type, String name, boolean nullable, boolean indexed, boolean unique, @Nullable String defaultValue) { + public SQLColumn(SQLTable table, Class type, String name, boolean nullable, boolean indexed, boolean unique, @Nullable String defaultValue, boolean virtual) { this.table = table; this.type = type; this.name = name; @@ -21,6 +22,7 @@ public SQLColumn(SQLTable table, Class type, String name, boolean nullable, b this.indexed = indexed; this.unique = unique; this.defaultValue = defaultValue; + this.virtual = virtual; } public void setTable(SQLTable table) { @@ -55,6 +57,10 @@ public boolean isUnique() { return defaultValue; } + public boolean isVirtual() { + return virtual; + } + @Override public int hashCode() { return Objects.hash(table, type, name, nullable, indexed, unique, defaultValue); @@ -70,7 +76,8 @@ public boolean equals(Object obj) { unique == other.unique && Objects.equals(defaultValue, other.defaultValue) && Objects.equals(type, other.type) && - Objects.equals(name, other.name); + Objects.equals(name, other.name) && + Objects.equals(virtual, other.virtual); } @Override @@ -82,6 +89,7 @@ public String toString() { ", indexed=" + indexed + ", unique=" + unique + ", defaultValue='" + defaultValue + '\'' + + ", virtual=" + virtual + '}'; } } diff --git a/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java b/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java index 64b8bbd2..15adfacd 100644 --- a/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java @@ -3,6 +3,8 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.Order; import net.staticstudios.data.UniqueData; +import net.staticstudios.data.util.ColumnValuePairs; +import net.staticstudios.data.util.UniqueDataMetadata; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -43,6 +45,12 @@ protected void setOffset(int offset) { } public @Nullable T findOne() { + UniqueDataMetadata metadata = dataManager.getMetadata(type); + ColumnValuePairs specialCaseColumnValuePairs = where.isSpecialOnlyUseIdColumns(metadata); + if (specialCaseColumnValuePairs != null) { + return dataManager.getInstance(type, specialCaseColumnValuePairs); + } + ComputedClause computed = compute(); List result = dataManager.query(type, computed.sql(), computed.parameters()); if (result.isEmpty()) { @@ -56,7 +64,6 @@ protected void setOffset(int offset) { return dataManager.query(type, computed.sql(), computed.parameters()); } - private ComputedClause compute() { StringBuilder sb = new StringBuilder(); List parameters = new ArrayList<>(); @@ -74,7 +81,7 @@ private ComputedClause compute() { } sb.append("WHERE "); - where.buildWhereClause(sb, parameters); + where.buildWhereClause(dataManager, dataManager.getMetadata(type), sb, parameters); } if (limit > 0) { sb.append(" LIMIT ").append(limit); diff --git a/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java index 467c1030..02d932b4 100644 --- a/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java @@ -1,14 +1,17 @@ package net.staticstudios.data.query; import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; import net.staticstudios.data.query.clause.*; +import net.staticstudios.data.query.clause.cv.*; +import net.staticstudios.data.util.ColumnMetadata; +import net.staticstudios.data.util.ColumnValuePair; +import net.staticstudios.data.util.ColumnValuePairs; +import net.staticstudios.data.util.UniqueDataMetadata; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.Stack; +import java.util.*; @SuppressWarnings("unused") public abstract class BaseQueryWhere { @@ -16,7 +19,7 @@ public abstract class BaseQueryWhere { private final Stack nonGrouped = new Stack<>(); private Node root = null; - private static void buildWhereClauseRecursive(Node node, StringBuilder sb, List parameters) { + private static void buildWhereClauseRecursive(Node node, DataManager dataManager, UniqueDataMetadata holderMetadata, StringBuilder sb, List parameters) { if (node == null) { return; } @@ -25,10 +28,10 @@ private static void buildWhereClauseRecursive(Node node, StringBuilder sb, List< sb.append("("); } - buildWhereClauseRecursive(node.lhs, sb, parameters); - List clauseParams = node.clause.append(sb); + buildWhereClauseRecursive(node.lhs, dataManager, holderMetadata, sb, parameters); + List clauseParams = node.clause.append(sb, dataManager, holderMetadata); parameters.addAll(clauseParams); - buildWhereClauseRecursive(node.rhs, sb, parameters); + buildWhereClauseRecursive(node.rhs, dataManager, holderMetadata, sb, parameters); if (isConditional) { sb.append(")"); } @@ -152,6 +155,30 @@ protected void lessThanOrEqualToClause(String schema, String table, String colum setValueClause(new LessThanOrEqualToClause(schema, table, column, o)); } + protected void cachedValueEqualsClause(String schema, String table, String identifier, Object o) { + setValueClause(new CachedValueEqualsClause(schema, table, identifier, o)); + } + + protected void cachedValueNotEqualsClause(String schema, String table, String identifier, Object o) { + setValueClause(new CachedValueNotEqualsClause(schema, table, identifier, o)); + } + + protected void cachedValueInClause(String schema, String table, String identifier, Object[] values) { + setValueClause(new CachedValueInClause(schema, table, identifier, values)); + } + + protected void cachedValueNotInClause(String schema, String table, String identifier, Object[] values) { + setValueClause(new CachedValueNotInClause(schema, table, identifier, values)); + } + + protected void cachedValueNullClause(String schema, String table, String identifier) { + setValueClause(new CachedValueNullClause(schema, table, identifier)); + } + + protected void cachedValueNotNullClause(String schema, String table, String identifier) { + setValueClause(new CachedValueNotNullClause(schema, table, identifier)); + } + private void setConditionalClause(Clause clause) { Preconditions.checkState(root != null, "Invalid state! Cannot set conditional clause '" + clause + "' here!"); if (root.clause instanceof ConditionalClause) { @@ -176,8 +203,41 @@ private void setValueClause(Clause clause) { } } - public void buildWhereClause(StringBuilder sb, List parameters) { - buildWhereClauseRecursive(root, sb, parameters); + public void buildWhereClause(DataManager dataManager, UniqueDataMetadata holderMetadata, StringBuilder sb, List parameters) { + buildWhereClauseRecursive(root, dataManager, holderMetadata, sb, parameters); + } + + public ColumnValuePairs isSpecialOnlyUseIdColumns(UniqueDataMetadata metadata) { + if (root == null) { + return null; + } + + List idColumns = new ArrayList<>(metadata.idColumns().size()); + for (ColumnMetadata idColumn : metadata.idColumns()) { + idColumns.add(idColumn.name()); + } + + List columnValuePairs = new ArrayList<>(); + boolean success = isSpecialOnlyUseIdColumnsRecursive(root, metadata.schema(), metadata.table(), idColumns, columnValuePairs) && idColumns.isEmpty(); + if (success) { + return new ColumnValuePairs(columnValuePairs.toArray(ColumnValuePair[]::new)); + } + + return null; + } + + private boolean isSpecialOnlyUseIdColumnsRecursive(Node node, String schema, String table, List columns, List columnValuePairs) { + if (node.clause instanceof EqualsClause equalsClause) { + if (Objects.equals(equalsClause.getSchema(), schema) && + Objects.equals(equalsClause.getTable(), table) && + columns.remove(equalsClause.getColumn())) { + columnValuePairs.add(new ColumnValuePair(equalsClause.getColumn(), equalsClause.getValue())); + return true; + } + } else if (node.clause instanceof AndClause) { + return isSpecialOnlyUseIdColumnsRecursive(node.lhs, schema, table, columns, columnValuePairs) && isSpecialOnlyUseIdColumnsRecursive(node.rhs, schema, table, columns, columnValuePairs); + } + return false; } static class Node { diff --git a/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java b/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java index d2772ceb..80918edf 100644 --- a/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java +++ b/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java @@ -262,6 +262,86 @@ public final QueryWhere lessThanOrEqualTo(String schema, String table, String co return this; } + public final QueryWhere cachedValueEquals(String schema, String table, String identifier, Object value) { + super.cachedValueEqualsClause(schema, table, identifier, value); + return this; + } + + public final QueryWhere cachedValueEquals(String identifier, Object value) { + super.cachedValueEqualsClause(metadata.schema(), metadata.table(), identifier, value); + return this; + } + + public final QueryWhere cachedValueNotEquals(String schema, String table, String identifier, Object value) { + super.cachedValueNotEqualsClause(schema, table, identifier, value); + return this; + } + + public final QueryWhere cachedValueNotEquals(String identifier, Object value) { + super.cachedValueNotEqualsClause(metadata.schema(), metadata.table(), identifier, value); + return this; + } + + public final QueryWhere cachedValueIn(String schema, String table, String identifier, Object[] in) { + super.cachedValueInClause(schema, table, identifier, in); + return this; + } + + public final QueryWhere cachedValueIn(String identifier, Object[] in) { + super.cachedValueInClause(metadata.schema(), metadata.table(), identifier, in); + return this; + } + + public final QueryWhere cachedValueNotIn(String schema, String table, String identifier, Object[] in) { + super.cachedValueNotInClause(schema, table, identifier, in); + return this; + } + + public final QueryWhere cachedValueNotIn(String identifier, Object[] in) { + super.cachedValueNotInClause(metadata.schema(), metadata.table(), identifier, in); + return this; + } + + public final QueryWhere cachedValueIn(String schema, String table, String identifier, List in) { + super.cachedValueInClause(schema, table, identifier, in.toArray()); + return this; + } + + public final QueryWhere cachedValueIn(String identifier, List in) { + super.cachedValueInClause(metadata.schema(), metadata.table(), identifier, in.toArray()); + return this; + } + + public final QueryWhere cachedValueNotIn(String schema, String table, String identifier, List in) { + super.cachedValueNotInClause(schema, table, identifier, in.toArray()); + return this; + } + + public final QueryWhere cachedValueNotIn(String identifier, List in) { + super.cachedValueNotInClause(metadata.schema(), metadata.table(), identifier, in.toArray()); + return this; + } + + public final QueryWhere cachedValueIsNull(String schema, String table, String identifier) { + super.cachedValueNullClause(schema, table, identifier); + return this; + } + + public final QueryWhere cachedValueIsNull(String identifier) { + super.cachedValueNullClause(metadata.schema(), metadata.table(), identifier); + return this; + } + + public final QueryWhere cachedValueIsNotNull(String schema, String table, String identifier) { + super.cachedValueNotNullClause(schema, table, identifier); + return this; + } + + public final QueryWhere cachedValueIsNotNull(String identifier) { + super.cachedValueNotNullClause(metadata.schema(), metadata.table(), identifier); + return this; + } + private void maybeAddInnerJoin(String schema, String table, String column) { if (schema.equals(metadata.schema()) && table.equals(metadata.table())) { return; diff --git a/core/src/main/java/net/staticstudios/data/query/clause/Clause.java b/core/src/main/java/net/staticstudios/data/query/clause/Clause.java index 91b2a950..97a1948e 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/Clause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/Clause.java @@ -1,8 +1,15 @@ package net.staticstudios.data.query.clause; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.util.UniqueDataMetadata; + import java.util.List; public interface Clause { List append(StringBuilder sb); + + default List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { + return append(sb); + } } diff --git a/core/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java b/core/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java index 652dd2e5..e4232948 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java @@ -15,6 +15,22 @@ public EqualsClause(String schema, String table, String column, Object value) { this.value = value; } + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getColumn() { + return column; + } + + public Object getValue() { + return value; + } + @Override public List append(StringBuilder sb) { sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" = ?"); diff --git a/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueEqualsClause.java b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueEqualsClause.java new file mode 100644 index 00000000..874c0a1d --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueEqualsClause.java @@ -0,0 +1,50 @@ +package net.staticstudios.data.query.clause.cv; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.primative.Primitives; +import net.staticstudios.data.query.clause.ValueClause; +import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; + +import java.util.List; + +public class CachedValueEqualsClause implements ValueClause { + private final String schema; + private final String table; + private final String identifier; + private final Object value; + + public CachedValueEqualsClause(String schema, String table, String identifier, Object value) { + this.schema = schema; + this.table = table; + this.identifier = identifier; + this.value = value; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getIdentifier() { + return identifier; + } + + public Object getValue() { + return value; + } + + @Override + public List append(StringBuilder sb) { + throw new UnsupportedOperationException(); + } + + @Override + public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(RedisUtils.getVirtualColumnName(identifier)).append("\" = ?"); + return List.of(Primitives.encode(dataManager.serialize(value))); + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueInClause.java b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueInClause.java new file mode 100644 index 00000000..2fae1e6e --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueInClause.java @@ -0,0 +1,47 @@ +package net.staticstudios.data.query.clause.cv; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.primative.Primitives; +import net.staticstudios.data.query.clause.ValueClause; +import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; + +import java.util.ArrayList; +import java.util.List; + +public class CachedValueInClause implements ValueClause { + private final String schema; + private final String table; + private final String identifier; + private final Object[] values; + + public CachedValueInClause(String schema, String table, String identifier, Object[] values) { + this.schema = schema; + this.table = table; + this.identifier = identifier; + this.values = values; + } + + @Override + public List append(StringBuilder sb) { + throw new UnsupportedOperationException(); + } + + @Override + public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(RedisUtils.getVirtualColumnName(identifier)).append("\" IN ("); + for (int i = 0; i < values.length; i++) { + sb.append("?"); + if (i < values.length - 1) { + sb.append(", "); + } + } + sb.append(")"); + + List encoded = new ArrayList<>(); + for (Object value : values) { + encoded.add(Primitives.encode(dataManager.serialize(value))); + } + return encoded; + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotEqualsClause.java b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotEqualsClause.java new file mode 100644 index 00000000..b0eed600 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotEqualsClause.java @@ -0,0 +1,50 @@ +package net.staticstudios.data.query.clause.cv; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.primative.Primitives; +import net.staticstudios.data.query.clause.ValueClause; +import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; + +import java.util.List; + +public class CachedValueNotEqualsClause implements ValueClause { + private final String schema; + private final String table; + private final String identifier; + private final Object value; + + public CachedValueNotEqualsClause(String schema, String table, String identifier, Object value) { + this.schema = schema; + this.table = table; + this.identifier = identifier; + this.value = value; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getIdentifier() { + return identifier; + } + + public Object getValue() { + return value; + } + + @Override + public List append(StringBuilder sb) { + throw new UnsupportedOperationException(); + } + + @Override + public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(RedisUtils.getVirtualColumnName(identifier)).append("\" <> ?"); + return List.of(Primitives.encode(dataManager.serialize(value))); + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotInClause.java b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotInClause.java new file mode 100644 index 00000000..0d5294e9 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotInClause.java @@ -0,0 +1,47 @@ +package net.staticstudios.data.query.clause.cv; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.primative.Primitives; +import net.staticstudios.data.query.clause.ValueClause; +import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; + +import java.util.ArrayList; +import java.util.List; + +public class CachedValueNotInClause implements ValueClause { + private final String schema; + private final String table; + private final String identifier; + private final Object[] values; + + public CachedValueNotInClause(String schema, String table, String identifier, Object[] values) { + this.schema = schema; + this.table = table; + this.identifier = identifier; + this.values = values; + } + + @Override + public List append(StringBuilder sb) { + throw new UnsupportedOperationException(); + } + + @Override + public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(RedisUtils.getVirtualColumnName(identifier)).append("\" NOT IN ("); + for (int i = 0; i < values.length; i++) { + sb.append("?"); + if (i < values.length - 1) { + sb.append(", "); + } + } + sb.append(")"); + + List encoded = new ArrayList<>(); + for (Object value : values) { + encoded.add(Primitives.encode(dataManager.serialize(value))); + } + return encoded; + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotNullClause.java b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotNullClause.java new file mode 100644 index 00000000..380cf40c --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotNullClause.java @@ -0,0 +1,43 @@ +package net.staticstudios.data.query.clause.cv; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.query.clause.ValueClause; +import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; + +import java.util.List; + +public class CachedValueNotNullClause implements ValueClause { + private final String schema; + private final String table; + private final String identifier; + + public CachedValueNotNullClause(String schema, String table, String identifier) { + this.schema = schema; + this.table = table; + this.identifier = identifier; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getIdentifier() { + return identifier; + } + + @Override + public List append(StringBuilder sb) { + throw new UnsupportedOperationException(); + } + + @Override + public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(RedisUtils.getVirtualColumnName(identifier)).append("\" IS NOT NULL"); + return List.of(); + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNullClause.java b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNullClause.java new file mode 100644 index 00000000..4b384fc9 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNullClause.java @@ -0,0 +1,43 @@ +package net.staticstudios.data.query.clause.cv; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.query.clause.ValueClause; +import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; + +import java.util.List; + +public class CachedValueNullClause implements ValueClause { + private final String schema; + private final String table; + private final String identifier; + + public CachedValueNullClause(String schema, String table, String identifier) { + this.schema = schema; + this.table = table; + this.identifier = identifier; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getIdentifier() { + return identifier; + } + + @Override + public List append(StringBuilder sb) { + throw new UnsupportedOperationException(); + } + + @Override + public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(RedisUtils.getVirtualColumnName(identifier)).append("\" IS NULL"); + return List.of(); + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/CachedValueMetadata.java b/core/src/main/java/net/staticstudios/data/util/CachedValueMetadata.java index ac3491ea..b5bc4e65 100644 --- a/core/src/main/java/net/staticstudios/data/util/CachedValueMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/CachedValueMetadata.java @@ -2,6 +2,104 @@ import net.staticstudios.data.UniqueData; -public record CachedValueMetadata(Class holderClass, String holderSchema, String holderTable, - String identifier, int expireAfterSeconds) { +import java.util.Objects; + +public final class CachedValueMetadata { + private final Class holderClass; + private final String holderSchema; + private final String holderTable; + private final String identifier; + private final Class type; + private final int expireAfterSeconds; + private boolean validatedFallbackSupplier = false; + private boolean validatedRefresher = false; + private boolean validatedUpdateHandlers = false; + + public CachedValueMetadata(Class holderClass, String holderSchema, String holderTable, + String identifier, Class type, int expireAfterSeconds) { + this.holderClass = holderClass; + this.holderSchema = holderSchema; + this.holderTable = holderTable; + this.identifier = identifier; + this.type = type; + this.expireAfterSeconds = expireAfterSeconds; + } + + public Class holderClass() { + return holderClass; + } + + public String holderSchema() { + return holderSchema; + } + + public String holderTable() { + return holderTable; + } + + public String identifier() { + return identifier; + } + + public Class type() { + return type; + } + + public int expireAfterSeconds() { + return expireAfterSeconds; + } + + public boolean hasValidatedFallbackSupplier() { + return validatedFallbackSupplier; + } + + public void setValidatedFallbackSupplier(boolean validatedFallbackSupplier) { + this.validatedFallbackSupplier = validatedFallbackSupplier; + } + + public boolean hasValidatedRefresher() { + return validatedRefresher; + } + + public void setValidatedRefresher(boolean validatedRefresher) { + this.validatedRefresher = validatedRefresher; + } + + public boolean hasValidatedUpdateHandlers() { + return validatedUpdateHandlers; + } + + public void setValidatedUpdateHandlers(boolean validatedUpdateHandlers) { + this.validatedUpdateHandlers = validatedUpdateHandlers; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (CachedValueMetadata) obj; + return Objects.equals(this.holderClass, that.holderClass) && + Objects.equals(this.holderSchema, that.holderSchema) && + Objects.equals(this.holderTable, that.holderTable) && + Objects.equals(this.identifier, that.identifier) && + Objects.equals(this.type, that.type) && + this.expireAfterSeconds == that.expireAfterSeconds; + } + + @Override + public int hashCode() { + return Objects.hash(holderClass, holderSchema, holderTable, identifier, type, expireAfterSeconds); + } + + @Override + public String toString() { + return "CachedValueMetadata[" + + "holderClass=" + holderClass + ", " + + "holderSchema=" + holderSchema + ", " + + "holderTable=" + holderTable + ", " + + "identifier=" + identifier + ", " + + "type=" + type + ", " + + "expireAfterSeconds=" + expireAfterSeconds + ']'; + } + } diff --git a/core/src/main/java/net/staticstudios/data/util/Cell.java b/core/src/main/java/net/staticstudios/data/util/Cell.java new file mode 100644 index 00000000..b256afa0 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/Cell.java @@ -0,0 +1,54 @@ +package net.staticstudios.data.util; + +import java.util.Objects; + +public class Cell { + private final String schema; + private final String table; + private final String column; + private final ColumnValuePairs idColumnValuePairs; + + public Cell(String schema, String table, String column, ColumnValuePairs idColumnValuePairs) { + this.schema = schema; + this.table = table; + this.column = column; + this.idColumnValuePairs = idColumnValuePairs; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getColumn() { + return column; + } + + public ColumnValuePairs getIdColumnValuePairs() { + return idColumnValuePairs; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Cell other)) return false; + return schema.equals(other.schema) && table.equals(other.table) && column.equals(other.column) && idColumnValuePairs.equals(other.idColumnValuePairs); + } + + @Override + public int hashCode() { + return Objects.hash(schema, table, column, idColumnValuePairs); + } + + @Override + public String toString() { + return "Cell[" + + "schema=" + schema + ", " + + "table=" + table + ", " + + "column=" + column + ", " + + "idColumnValuePairs=" + idColumnValuePairs + ']'; + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandlerWrapper.java b/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandlerWrapper.java index 55397327..447770e5 100644 --- a/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandlerWrapper.java +++ b/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandlerWrapper.java @@ -12,10 +12,6 @@ public class CollectionChangeHandlerWrapper { private PersistentCollectionMetadata collectionMetadata; public CollectionChangeHandlerWrapper(CollectionChangeHandler handler, Class dataType, Class holderClass, Type type) { - LambdaUtils.assertLambdaDoesntCapture(handler, "Use thr provided instance to access member variables."); - // we don't want to hold a reference to a UniqueData instances, since it won't get GCed - // and the handler may be called for any holder instance. - this.handler = handler; this.dataType = dataType; this.holderClass = holderClass; diff --git a/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java b/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java index 502f04d3..68a4f8a4 100644 --- a/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java +++ b/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java @@ -2,19 +2,27 @@ import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; -import java.util.List; import java.util.stream.Stream; public final class ColumnValuePairs implements Iterable { + public static final ColumnValuePairs EMPTY = new ColumnValuePairs(); + private final ColumnValuePair[] pairs; public ColumnValuePairs(ColumnValuePair... pairs) { - List pairList = new ArrayList<>(List.of(pairs)); - pairList.sort(Comparator.comparing(ColumnValuePair::column)); - this.pairs = pairList.toArray(new ColumnValuePair[0]); + this.pairs = pairs.clone(); + Arrays.sort(this.pairs, Comparator.comparing(ColumnValuePair::column)); + } + + public static Object getValue(String column, ColumnValuePairs pairs) { + for (ColumnValuePair pair : pairs) { + if (pair.column().equals(column)) { + return pair.value(); + } + } + return null; } public ColumnValuePair[] getPairs() { diff --git a/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java index 98eb27ad..2a815db1 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java @@ -4,4 +4,8 @@ public interface PersistentCollectionMetadata { Class getHolderClass(); + + boolean hasValidatedChangeHandlers(); + + void setValidatedChangeHandlers(boolean validatedChangeHandlers); } diff --git a/core/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java index c9140e2f..ad7a2cec 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java @@ -18,6 +18,7 @@ public class PersistentManyToManyCollectionMetadata implements PersistentCollect private String joinTableName; private List joinTableToDataTableLinks; private List joinTableToReferencedTableLinks; + private boolean validatedChangeHandlers = false; public PersistentManyToManyCollectionMetadata(Class holderClass, Class referencedType, String parsedJoinTableSchema, String parsedJoinTableName, String rawLinks) { this.holderClass = holderClass; @@ -65,6 +66,16 @@ public synchronized List getJoinTableToReferencedTableLinks(DataManager da return joinTableToReferencedTableLinks; } + @Override + public boolean hasValidatedChangeHandlers() { + return validatedChangeHandlers; + } + + @Override + public void setValidatedChangeHandlers(boolean validatedChangeHandlers) { + this.validatedChangeHandlers = validatedChangeHandlers; + } + @Override public Class getHolderClass() { return holderClass; diff --git a/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java index 067518d7..430e9f41 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java @@ -12,6 +12,7 @@ public class PersistentOneToManyCollectionMetadata implements PersistentCollecti private final DataManager dataManager; private final Class referencedType; private final List links; + private boolean validatedChangeHandlers = false; public PersistentOneToManyCollectionMetadata(DataManager dataManager, Class holderClass, Class referencedType, List links) { this.dataManager = dataManager; @@ -33,6 +34,16 @@ public Class getHolderClass() { return holderClass; } + @Override + public boolean hasValidatedChangeHandlers() { + return validatedChangeHandlers; + } + + @Override + public void setValidatedChangeHandlers(boolean validatedChangeHandlers) { + this.validatedChangeHandlers = validatedChangeHandlers; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; diff --git a/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java index 71ccc2ff..ed83cbd0 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java @@ -13,6 +13,7 @@ public class PersistentOneToManyValueCollectionMetadata implements PersistentCol private final String dataTable; private final String dataColumn; private final List links; + private boolean validatedChangeHandlers = false; public PersistentOneToManyValueCollectionMetadata(Class holderClass, Class dataType, String dataSchema, String dataTable, String dataColumn, List links) { this.holderClass = holderClass; @@ -48,6 +49,16 @@ public List getLinks() { return links; } + @Override + public boolean hasValidatedChangeHandlers() { + return validatedChangeHandlers; + } + + @Override + public void setValidatedChangeHandlers(boolean validatedChangeHandlers) { + this.validatedChangeHandlers = validatedChangeHandlers; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; diff --git a/core/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java index b78b15fb..a45e1f8e 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java @@ -8,6 +8,7 @@ public class PersistentValueMetadata { private final Class holderClass; private final ColumnMetadata columnMetadata; private final int updateInterval; + private boolean validatedUpdateHandlers = false; public PersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata, int updateInterval) { this.holderClass = holderClass; @@ -35,6 +36,14 @@ public int getUpdateInterval() { return updateInterval; } + public boolean hasValidatedUpdateHandlers() { + return validatedUpdateHandlers; + } + + public void setValidatedUpdateHandlers(boolean validatedUpdateHandlers) { + this.validatedUpdateHandlers = validatedUpdateHandlers; + } + @Override public int hashCode() { return Objects.hash(holderClass, columnMetadata, updateInterval); diff --git a/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java b/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java new file mode 100644 index 00000000..72009fe4 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java @@ -0,0 +1,28 @@ +package net.staticstudios.data.util; + +import java.util.Set; + +public class ReadCacheResult { + private final Object value; + private final Set dependencies; + + public ReadCacheResult(Object value, Set dependencies) { + this.value = value; + this.dependencies = dependencies; + } + + public Object getValue() { + return value; + } + + public Set getDependencies() { + return dependencies; + } + + @Override + public String toString() { + return "ReadCacheResult[" + + "value=" + value + + ", dependencies=" + dependencies + ']'; + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/RedisUtils.java b/core/src/main/java/net/staticstudios/data/util/RedisUtils.java deleted file mode 100644 index 00ffb0dc..00000000 --- a/core/src/main/java/net/staticstudios/data/util/RedisUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.staticstudios.data.util; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -public class RedisUtils { - public static Pattern globToRegex(String redisPattern) { - StringBuilder regex = new StringBuilder(); - - for (int i = 0; i < redisPattern.length(); i++) { - char c = redisPattern.charAt(i); - switch (c) { - case '*' -> regex.append(".*"); - case '?' -> regex.append('.'); - case '[', ']' -> regex.append(c); - case '\\' -> regex.append("\\\\"); - default -> { - if ("+()^$.{}|".indexOf(c) != -1) { - regex.append('\\'); - } - regex.append(c); - } - } - } - - return Pattern.compile(regex.toString()); - } - - public static String buildRedisKey(String holderSchema, String holderTable, String identifier, ColumnValuePairs icColumns) { - //static-data:[schema]:[table]:[id column-value pairs, seperated by ':']:[identifier] - StringBuilder sb = new StringBuilder("static-data:"); - sb.append(holderSchema).append(":").append(holderTable).append(":"); - for (ColumnValuePair pair : icColumns) { - sb.append(pair.column()).append(":").append(pair.value()).append(":"); - } - sb.append(identifier); - return sb.toString(); - } - - public static String buildPartialRedisKey(String holderSchema, String holderTable, String identifier, List idColumnMetadata) { - StringBuilder sb = new StringBuilder("static-data:"); - sb.append(holderSchema).append(":").append(holderTable).append(":"); - for (ColumnMetadata idColumn : idColumnMetadata) { - sb.append(idColumn.name()).append(":").append("*").append(":"); - } - sb.append(identifier); - return sb.toString(); - } - - public static DeconstructedKey deconstruct(String key) { - List encodedIdValues = new ArrayList<>(); - List encodedIdNames = new ArrayList<>(); - String[] parts = key.split(":"); - StringBuilder sb = new StringBuilder(); - sb.append(parts[0]).append(":").append(parts[1]).append(":").append(parts[2]).append(":"); - for (int i = 3; i < parts.length - 1; i += 2) { - sb.append(parts[i]).append(":").append("*").append(":"); - encodedIdNames.add(parts[i]); - encodedIdValues.add(parts[i + 1]); - } - sb.append(parts[parts.length - 1]); - return new DeconstructedKey(sb.toString(), encodedIdNames, encodedIdValues); - } - - public record DeconstructedKey(String partialKey, List encodedIdNames, List encodedIdValues) { - - } -} diff --git a/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java b/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java index b60e7c9f..4a6b8725 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java @@ -1,10 +1,128 @@ package net.staticstudios.data.util; +import net.staticstudios.data.DataManager; import net.staticstudios.data.UniqueData; import net.staticstudios.data.utils.Link; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.Objects; + +public final class ReferenceMetadata { + private final Class holderClass; + private final Class referencedClass; + private final List links; + private final boolean generateFkey; + private final boolean updateReferencedTable; + private @Nullable String selectReferencedColumnValuePairsQuery; + private boolean validatedUpdateHandlers = false; + + public ReferenceMetadata(Class holderClass, Class referencedClass, + List links, boolean generateFkey, boolean updateReferencedTable) { + this.holderClass = holderClass; + this.referencedClass = referencedClass; + this.links = links; + this.generateFkey = generateFkey; + this.updateReferencedTable = updateReferencedTable; + } + + private @Language("SQL") String buildSelectReferencedColumnValuePairsQuery(DataManager dataManager) { + UniqueDataMetadata holderMetadata = dataManager.getMetadata(holderClass); + UniqueDataMetadata referencedMetadata = dataManager.getMetadata(referencedClass); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + sqlBuilder.append("_referenced.\"").append(idColumn.name()).append("\", "); + } + for (Link entry : links) { + String myColumn = entry.columnInReferringTable(); + sqlBuilder.append("_referring.\"").append(myColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + + sqlBuilder.append(" FROM \"").append(referencedMetadata.schema()).append("\".\"").append(referencedMetadata.table()).append("\" _referenced"); + sqlBuilder.append(" INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" _referring ON "); + for (Link entry : links) { + String myColumn = entry.columnInReferringTable(); + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("_referenced.\"").append(theirColumn).append("\" = "); + sqlBuilder.append("_referring.\"").append(myColumn).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + + sqlBuilder.append(" WHERE "); + + for (ColumnMetadata idColumn : holderMetadata.idColumns()) { + sqlBuilder.append("_referring.\"").append(idColumn.name()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + + return sqlBuilder.toString(); + } + + + public SelectQuery buildSelectReferencedColumnValuePairsSelectQuery(DataManager dataManager, List values) { + if (selectReferencedColumnValuePairsQuery == null) { + selectReferencedColumnValuePairsQuery = buildSelectReferencedColumnValuePairsQuery(dataManager); + } + return new SelectQuery(selectReferencedColumnValuePairsQuery, values); + } + + public Class holderClass() { + return holderClass; + } + + public Class referencedClass() { + return referencedClass; + } + + public List links() { + return links; + } + + public boolean generateFkey() { + return generateFkey; + } + + public boolean updateReferencedTable() { + return updateReferencedTable; + } + + public boolean hasValidatedUpdateHandlers() { + return validatedUpdateHandlers; + } + + public void setValidatedUpdateHandlers(boolean validatedUpdateHandlers) { + this.validatedUpdateHandlers = validatedUpdateHandlers; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (ReferenceMetadata) obj; + return Objects.equals(this.holderClass, that.holderClass) && + Objects.equals(this.referencedClass, that.referencedClass) && + Objects.equals(this.links, that.links) && + this.generateFkey == that.generateFkey && + this.updateReferencedTable == that.updateReferencedTable; + } + + @Override + public int hashCode() { + return Objects.hash(holderClass, referencedClass, links, generateFkey, updateReferencedTable); + } + + @Override + public String toString() { + return "ReferenceMetadata[" + + "holderClass=" + holderClass + ", " + + "referencedClass=" + referencedClass + ", " + + "links=" + links + ", " + + "generateFkey=" + generateFkey + ", " + + "updateReferencedTable=" + updateReferencedTable + ']'; + } + -public record ReferenceMetadata(Class holderClass, Class referencedClass, - List links, boolean generateFkey, boolean updateReferencedTable) { } diff --git a/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandlerWrapper.java b/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandlerWrapper.java index f26ea8b2..b6de42b6 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandlerWrapper.java +++ b/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandlerWrapper.java @@ -9,10 +9,6 @@ public class ReferenceUpdateHandlerWrapper handler) { - LambdaUtils.assertLambdaDoesntCapture(handler, "Use thr provided instance to access member variables."); - // we don't want to hold a reference to a UniqueData instances, since it won't get GCed - // and the handler may be called for any holder instance. - this.handler = handler; } diff --git a/core/src/main/java/net/staticstudios/data/util/SelectQuery.java b/core/src/main/java/net/staticstudios/data/util/SelectQuery.java new file mode 100644 index 00000000..f6355eaa --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/SelectQuery.java @@ -0,0 +1,41 @@ +package net.staticstudios.data.util; + +import java.util.List; +import java.util.Objects; + +public class SelectQuery { + private final String sql; + private final List values; + + public SelectQuery(String sql, List values) { + this.sql = sql; + this.values = List.copyOf(values); + } + + public String getSql() { + return sql; + } + + public List getValues() { + return values; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof SelectQuery other)) return false; + return Objects.equals(sql, other.sql) && Objects.equals(values, other.values); + } + + @Override + public int hashCode() { + return Objects.hash(sql, values); + } + + @Override + public String toString() { + return "SelectQuery[" + + "sql=" + sql + ", " + + "values=" + values + ']'; + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/SlidingWindowCounter.java b/core/src/main/java/net/staticstudios/data/util/SlidingWindowCounter.java new file mode 100644 index 00000000..e8e2270d --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/SlidingWindowCounter.java @@ -0,0 +1,64 @@ +package net.staticstudios.data.util; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + +public class SlidingWindowCounter { + private final long windowNanos; + private final int bucketCount; + private final long bucketDurationNanos; + private final LongAdder[] buckets; + private final AtomicLong currentBucketIndex = new AtomicLong(0); + private final long startNanos; + + public SlidingWindowCounter(long windowMillis, int bucketCount) { + this.windowNanos = windowMillis * 1_000_000L; + this.bucketCount = bucketCount; + this.bucketDurationNanos = windowNanos / bucketCount; + this.buckets = new LongAdder[bucketCount]; + for (int i = 0; i < bucketCount; i++) { + buckets[i] = new LongAdder(); + } + this.startNanos = System.nanoTime(); + this.currentBucketIndex.set(0); + } + + public void increment() { + long now = System.nanoTime(); + long idx = (now - startNanos) / bucketDurationNanos; + advance(idx); + buckets[(int) (idx % bucketCount)].increment(); + } + + public double getPerSecond() { + long now = System.nanoTime(); + long idx = (now - startNanos) / bucketDurationNanos; + advance(idx); + + long total = 0; + long oldestActiveIdx = idx - bucketCount + 1; + for (int i = 0; i < bucketCount; i++) { + long bucketIdx = oldestActiveIdx + i; + if (bucketIdx >= 0 && bucketIdx <= idx) { + total += buckets[(int) (bucketIdx % bucketCount)].sum(); + } + } + + double windowSeconds = windowNanos / 1_000_000_000.0; + return total / windowSeconds; + } + + private void advance(long targetIdx) { + long prev; + while ((prev = currentBucketIndex.get()) < targetIdx) { + if (!currentBucketIndex.compareAndSet(prev, prev + 1)) { + continue; + } + long toClear = prev + 1; + if (toClear <= targetIdx) { + buckets[(int) (toClear % bucketCount)].reset(); + } + } + } +} + diff --git a/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java index 9215590f..157e8aa6 100644 --- a/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java +++ b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java @@ -10,10 +10,6 @@ public class ValueUpdateHandlerWrapper { private final Class holderClass; public ValueUpdateHandlerWrapper(ValueUpdateHandler handler, Class dataType, Class holderClass) { - LambdaUtils.assertLambdaDoesntCapture(handler, "Use thr provided instance to access member variables."); - // we don't want to hold a reference to a UniqueData instances, since it won't get GCed - // and the handler may be called for any holder instance. - this.handler = handler; this.dataType = dataType; this.holderClass = holderClass; diff --git a/core/src/main/java/net/staticstudios/data/util/redis/RedisIdentifier.java b/core/src/main/java/net/staticstudios/data/util/redis/RedisIdentifier.java new file mode 100644 index 00000000..aa82fa3a --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/redis/RedisIdentifier.java @@ -0,0 +1,11 @@ +package net.staticstudios.data.util.redis; + +import net.staticstudios.data.util.ColumnValuePairs; +import org.jspecify.annotations.NonNull; + +public record RedisIdentifier(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns) { + @Override + public @NonNull String toString() { + return RedisUtils.toKey(this); + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/redis/RedisUtils.java b/core/src/main/java/net/staticstudios/data/util/redis/RedisUtils.java new file mode 100644 index 00000000..6dc4b6d3 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/redis/RedisUtils.java @@ -0,0 +1,151 @@ +package net.staticstudios.data.util.redis; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.parse.SQLColumn; +import net.staticstudios.data.parse.SQLSchema; +import net.staticstudios.data.parse.SQLTable; +import net.staticstudios.data.primative.Primitives; +import net.staticstudios.data.util.ColumnMetadata; +import net.staticstudios.data.util.ColumnValuePair; +import net.staticstudios.data.util.ColumnValuePairs; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class RedisUtils { + public static Pattern globToRegex(String redisPattern) { + StringBuilder regex = new StringBuilder(); + + for (int i = 0; i < redisPattern.length(); i++) { + char c = redisPattern.charAt(i); + switch (c) { + case '*' -> regex.append(".*"); + case '?' -> regex.append('.'); + case '[', ']' -> regex.append(c); + case '\\' -> regex.append("\\\\"); + default -> { + if ("+()^$.{}|".indexOf(c) != -1) { + regex.append('\\'); + } + regex.append(c); + } + } + } + + return Pattern.compile(regex.toString()); + } + + public static @Nullable RedisIdentifier fromKey(String key, DataManager dataManager) { + String[] parts = key.split(":"); + String holderSchema = parts[1]; + String holderTable = parts[2]; + String identifier = parts[parts.length - 1]; + + SQLSchema schema = dataManager.getSQLBuilder().getSchema(holderSchema); + if (schema == null) { + return null; + } + + SQLTable table = schema.getTable(holderTable); + if (table == null) { + return null; + } + + List idColumns = new ArrayList<>(); + for (int i = 3; i < parts.length - 1; i += 2) { + String value = parts[i + 1]; + SQLColumn column = table.getColumn(parts[i]); + if (column == null) { + return null; + } + + idColumns.add(new ColumnValuePair(parts[i], Primitives.decodePrimitive(column.getType(), value))); + } + return new RedisIdentifier(holderSchema, holderTable, identifier, new ColumnValuePairs(idColumns.toArray(ColumnValuePair[]::new))); + } + + public static String toKey(RedisIdentifier identifier) { + return buildRedisKey(identifier.holderSchema(), identifier.holderTable(), identifier.identifier(), identifier.idColumns()); + } + + public static String buildRedisKey(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns) { + //static-data:[schema]:[table]:[id column-value pairs, seperated by ':']:[identifier] + StringBuilder sb = new StringBuilder("static-data:"); + sb.append(holderSchema).append(":").append(holderTable).append(":"); + for (ColumnValuePair pair : idColumns) { + sb.append(pair.column()).append(":").append(pair.value()).append(":"); + } + sb.append(identifier); + return sb.toString(); + } + + public static String buildPartialRedisKey(String holderSchema, String holderTable, String identifier, List idColumnMetadata) { + StringBuilder sb = new StringBuilder("static-data:"); + sb.append(holderSchema).append(":").append(holderTable).append(":"); + for (ColumnMetadata idColumn : idColumnMetadata) { + sb.append(idColumn.name()).append(":").append("*").append(":"); + } + sb.append(identifier); + return sb.toString(); + } + + public static DeconstructedKey deconstruct(String key) { + List encodedIdValues = new ArrayList<>(); + List encodedIdNames = new ArrayList<>(); + String[] parts = key.split(":"); + StringBuilder sb = new StringBuilder(); + sb.append(parts[0]).append(":").append(parts[1]).append(":").append(parts[2]).append(":"); + for (int i = 3; i < parts.length - 1; i += 2) { + sb.append(parts[i]).append(":").append("*").append(":"); + encodedIdNames.add(parts[i]); + encodedIdValues.add(parts[i + 1]); + } + sb.append(parts[parts.length - 1]); + return new DeconstructedKey(sb.toString(), encodedIdNames, encodedIdValues); + } + + public static FullyDeconstructedKey fullyDeconstruct(String key, DataManager dataManager) { + String[] parts = key.split(":"); + String holderSchema = parts[1]; + String holderTable = parts[2]; + String identifier = parts[parts.length - 1]; + + SQLSchema schema = dataManager.getSQLBuilder().getSchema(holderSchema); + if (schema == null) { + return null; + } + + SQLTable table = schema.getTable(holderTable); + if (table == null) { + return null; + } + + List idColumns = new ArrayList<>(); + for (int i = 3; i < parts.length - 1; i += 2) { + + for (ColumnMetadata columnMetadata : table.getIdColumns()) { + if (columnMetadata.name().equals(parts[i])) { + idColumns.add(new ColumnValuePair(parts[i], Primitives.decodePrimitive(columnMetadata.type(), parts[i + 1]))); + break; + } + } + } + return new FullyDeconstructedKey(holderSchema, holderTable, identifier, new ColumnValuePairs(idColumns.toArray(ColumnValuePair[]::new))); + } + + public record DeconstructedKey(String partialKey, List encodedIdNames, List encodedIdValues) { + + } + + public record FullyDeconstructedKey(String holderSchema, String holderTable, String identifier, + ColumnValuePairs idColumns) { + + } + + + public static String getVirtualColumnName(String identifier) { + return "__virtual__cv_" + identifier; + } +} diff --git a/core/src/test/java/net/staticstudios/data/CachedValueTest.java b/core/src/test/java/net/staticstudios/data/CachedValueTest.java index f8080017..c0729786 100644 --- a/core/src/test/java/net/staticstudios/data/CachedValueTest.java +++ b/core/src/test/java/net/staticstudios/data/CachedValueTest.java @@ -3,10 +3,11 @@ import com.google.gson.Gson; import net.staticstudios.data.impl.redis.RedisEncodedValue; import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.mock.user.MockUser; import net.staticstudios.data.util.ColumnValuePair; import net.staticstudios.data.util.ColumnValuePairs; -import net.staticstudios.data.util.RedisUtils; +import net.staticstudios.data.util.redis.RedisUtils; import org.junit.jupiter.api.Test; import redis.clients.jedis.Jedis; @@ -162,22 +163,29 @@ public void testLoadCachedValues() { Jedis jedis = getJedis(); - String onCooldownKey = RedisUtils.buildRedisKey("public", "users", "on_cooldown", columnValuePairs); + DataManager dataManager1 = getMockEnvironments().getFirst().dataManager(); + dataManager1.load(MockUser.class); + dataManager1.finishLoading(); + MockUser user1 = MockUser.builder(dataManager1) + .id(userId) + .name("john doe") + .insert(InsertMode.ASYNC); + String cooldownUpdatesKey = RedisUtils.buildRedisKey("public", "users", "cooldown_updates", columnValuePairs); - jedis.set(onCooldownKey, gson.toJson(new RedisEncodedValue(null, "true"))); jedis.set(cooldownUpdatesKey, gson.toJson(new RedisEncodedValue(null, "5"))); - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - dataManager.load(MockUser.class); - dataManager.finishLoading(); - MockUser user = MockUser.builder(dataManager) - .id(userId) - .name("john doe") - .insert(InsertMode.ASYNC); + waitForDataPropagation(); - assertEquals(true, user.onCooldown.get()); - assertEquals(5, user.cooldownUpdates.get()); + assertEquals(5, user1.cooldownUpdates.get()); + + MockEnvironment env2 = createMockEnvironment(); + DataManager dataManager2 = env2.dataManager(); + dataManager2.load(MockUser.class); + dataManager2.finishLoading(); + MockUser user2 = MockUser.query(dataManager2).findAll().getFirst(); + + assertEquals(5, user2.cooldownUpdates.get()); } @Test diff --git a/core/src/test/java/net/staticstudios/data/QueryTest.java b/core/src/test/java/net/staticstudios/data/QueryTest.java index d9db18cf..1ed0eda6 100644 --- a/core/src/test/java/net/staticstudios/data/QueryTest.java +++ b/core/src/test/java/net/staticstudios/data/QueryTest.java @@ -109,99 +109,149 @@ public void testQueryOnForeignColumn() { assertSame(likesGreen, users.get(1)); } + @Test + public void testFindOneCachedValueEquals() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + UUID id = UUID.randomUUID(); + MockUser original = MockUser.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); + + original.settingsUpdates.set(22); + + MockUser got = MockUser.query(dataManager).where(w -> w.settingsUpdatesIs(22)) + .findOne(); + assertSame(original, got); + } + @Test public void testEqualsClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"id\" = ?", MockUser.query(dataManager).where(w -> w.idIs(UUID.randomUUID())).toString()); } @Test public void testBetweenClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"age\" BETWEEN ? AND ?", MockUser.query(dataManager).where(w -> w.ageIsBetween(0, 0)).toString()); } @Test public void testAgeIsLessThanClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"age\" < ?", MockUser.query(dataManager).where(w -> w.ageIsLessThan(0)).toString()); } @Test public void testAgeIsLessThanOrEqualToClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"age\" <= ?", MockUser.query(dataManager).where(w -> w.ageIsLessThanOrEqualTo(0)).toString()); } @Test public void testAgeIsGreaterThanClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"age\" > ?", MockUser.query(dataManager).where(w -> w.ageIsGreaterThan(0)).toString()); } @Test public void testAgeIsGreaterThanOrEqualToClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"age\" >= ?", MockUser.query(dataManager).where(w -> w.ageIsGreaterThanOrEqualTo(0)).toString()); } @Test public void testAgeIsNullClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"age\" IS NULL", MockUser.query(dataManager).where(w -> w.ageIsNull()).toString()); } @Test public void testAgeIsNotNullClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"age\" IS NOT NULL", MockUser.query(dataManager).where(w -> w.ageIsNotNull()).toString()); } @Test public void testNameIsLikeClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"name\" LIKE ?", MockUser.query(dataManager).where(w -> w.nameIsLike("%test%")).toString()); } @Test public void testNameIsNotLikeClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"name\" NOT LIKE ?", MockUser.query(dataManager).where(w -> w.nameIsNotLike("%test%")).toString()); } @Test public void testNameIsInClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"name\" IN (?, ?, ?)", MockUser.query(dataManager).where(w -> w.nameIsIn("name1", "name2", "name3")).toString()); } @Test public void testNameIsInListClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"name\" IN (?, ?, ?)", MockUser.query(dataManager).where(w -> w.nameIsIn(List.of("name1", "name2", "name3"))).toString()); } @Test public void testLimitClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"id\" = ? LIMIT 10", MockUser.query(dataManager).where(w -> w.idIs(UUID.randomUUID())).limit(10).toString()); } @Test public void testOffsetClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"id\" = ? OFFSET 5", MockUser.query(dataManager).where(w -> w.idIs(UUID.randomUUID())).offset(5).toString()); } @Test public void testOrderByClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"id\" = ? ORDER BY \"public\".\"users\".\"age\" ASC", MockUser.query(dataManager).where(w -> w.idIs(UUID.randomUUID())).orderByAge(Order.ASCENDING).toString()); } @Test public void testAndClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE (\"public\".\"users\".\"id\" = ? AND \"public\".\"users\".\"age\" BETWEEN ? AND ?)", MockUser.query(dataManager).where(w -> w.idIs(UUID.randomUUID()) .and() .group(w1 -> w1.ageIsBetween(0, 5)) @@ -211,6 +261,8 @@ public void testAndClause() { @Test public void testOrClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE (\"public\".\"users\".\"id\" = ? OR \"public\".\"users\".\"age\" BETWEEN ? AND ?)", MockUser.query(dataManager).where(w -> w.idIs(UUID.randomUUID()) .or() .ageIsBetween(0, 5)).toString()); @@ -219,6 +271,8 @@ public void testOrClause() { @Test public void testComplexClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); assertEquals("WHERE ((\"public\".\"users\".\"id\" = ? OR \"public\".\"users\".\"age\" BETWEEN ? AND ?) AND \"public\".\"users\".\"name\" LIKE ?) LIMIT 10 OFFSET 5 ORDER BY \"public\".\"users\".\"age\" DESC", MockUser.query(dataManager).where(w -> w .idIs(UUID.randomUUID()) @@ -234,4 +288,69 @@ public void testComplexClause() { } //todo: test more complex cases + + @Test + public void testCachedValueEqualsClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" = ?", MockUser.query(dataManager).where(w -> w.settingsUpdatesIs(1)).toString()); + } + + @Test + public void testCachedValueNotEqualsClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" <> ?", MockUser.query(dataManager).where(w -> w.settingsUpdatesIsNot(1)).toString()); + } + + @Test + public void testCachedValueIsNullClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" IS NULL", MockUser.query(dataManager).where(w -> w.settingsUpdatesIsNull()).toString()); + } + + @Test + public void testCachedValueIsNotNullClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" IS NOT NULL", MockUser.query(dataManager).where(w -> w.settingsUpdatesIsNotNull()).toString()); + } + + @Test + public void testCachedValueIsInClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" IN (?, ?, ?)", MockUser.query(dataManager).where(w -> w.settingsUpdatesIsIn(1, 2, 3)).toString()); + } + + @Test + public void testCachedValueIsInListClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" IN (?, ?, ?)", MockUser.query(dataManager).where(w -> w.settingsUpdatesIsIn(List.of(1, 2, 3))).toString()); + } + + @Test + public void testCachedValueIsNotInClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" NOT IN (?, ?, ?)", MockUser.query(dataManager).where(w -> w.settingsUpdatesIsNotIn(1, 2, 3)).toString()); + } + + @Test + public void testCachedValueIsNotInListClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" NOT IN (?, ?, ?)", MockUser.query(dataManager).where(w -> w.settingsUpdatesIsNotIn(List.of(1, 2, 3))).toString()); + } + } \ No newline at end of file diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/DataPsiAugmentProvider.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/DataPsiAugmentProvider.java index a509dee2..21a532b1 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/DataPsiAugmentProvider.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/DataPsiAugmentProvider.java @@ -15,12 +15,13 @@ import org.jetbrains.annotations.Nullable; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Function; public class DataPsiAugmentProvider extends PsiAugmentProvider { //TODO: I'm not sure if the following is possible, but if it is it would be cool: - // 1. When I ctrl+click on a builder method or query where clause method, it should take me to the field definition in the data class. // 2. When I refactor a field in the data class, the corresponding builder method and query where clause methods should also be refactored. //TODO: This seems to work fine, but tests should probably be added just in case. @@ -29,10 +30,14 @@ public class DataPsiAugmentProvider extends PsiAugmentProvider { private static final Key> BUILDER_CLASS_KEY = Key.create("synthetic.class.builder"); private static final Key> BUILDER_METHOD_KEY = Key.create("synthetic.method.builder"); + private static final Key> BUILDER_METHOD_2_KEY = Key.create("synthetic.method.builder2"); private static final Key> QUERY_CLASS_KEY = Key.create("synthetic.class.query"); private static final Key> QUERY_METHOD_KEY = Key.create("synthetic.method.query"); + private static final Key> QUERY_METHOD_2_KEY = Key.create("synthetic.method.query2"); private static final Key> QUERY_WHERE_CLASS_KEY = Key.create("synthetic.class.query.where"); + private static final ThreadLocal> IN_PROGRESS = ThreadLocal.withInitial(HashSet::new); + @Override protected @NotNull List getAugments(@NotNull PsiElement element, @NotNull Class type, @Nullable String nameHint) { @@ -40,6 +45,18 @@ public class DataPsiAugmentProvider extends PsiAugmentProvider { return Collections.emptyList(); } + Set inProgress = IN_PROGRESS.get(); + if (!inProgress.add(psiClass)) { + return Collections.emptyList(); + } + try { + return doGetAugments(psiClass, type); + } finally { + inProgress.remove(psiClass); + } + } + + private @NotNull List doGetAugments(@NotNull PsiClass psiClass, @NotNull Class type) { if (!IntelliJPluginUtils.extendsClass(psiClass, Constants.UNIQUE_DATA_FQN)) { return Collections.emptyList(); } @@ -81,81 +98,61 @@ private PsiClass getQueryWhereClass(PsiClass parent) { } private PsiMethod getBuilderMethod(PsiClass parent) { -// return CachedValuesManager.getCachedValue(parent, () -> { -// PsiClass builderClass = getBuilderClass(parent); -// PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) -// .createType(builderClass, PsiSubstitutor.EMPTY); -// SyntheticBuilderMethod builderMethod = new SyntheticBuilderMethod(parent, "builder", returnType); -// return CachedValueProvider.Result.create(builderMethod, parent); -// }); - PsiClass builderClass = getBuilderClass(parent); - PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) - .createType(builderClass, PsiSubstitutor.EMPTY); - SyntheticMethod builderMethod = new SyntheticMethod(parent, parent, "builder", returnType); - builderMethod.addModifier(PsiModifier.PUBLIC); - builderMethod.addModifier(PsiModifier.STATIC); - builderMethod.addModifier(PsiModifier.FINAL); - return builderMethod; + return CachedValuesManager.getCachedValue(parent, BUILDER_METHOD_KEY, () -> { + PsiClass builderClass = getBuilderClass(parent); + PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) + .createType(builderClass, PsiSubstitutor.EMPTY); + SyntheticMethod builderMethod = new SyntheticMethod(parent, parent, "builder", returnType); + builderMethod.addModifier(PsiModifier.PUBLIC); + builderMethod.addModifier(PsiModifier.STATIC); + builderMethod.addModifier(PsiModifier.FINAL); + return CachedValueProvider.Result.create(builderMethod, parent); + }); } private PsiMethod getBuilderMethod2(PsiClass parent) { -// return CachedValuesManager.getCachedValue(parent, () -> { -// PsiClass builderClass = getBuilderClass(parent); -// PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) -// .createType(builderClass, PsiSubstitutor.EMPTY); -// SyntheticBuilderMethod builderMethod = new SyntheticBuilderMethod(parent, "builder", returnType); -// return CachedValueProvider.Result.create(builderMethod, parent); -// }); - PsiClass builderClass = getBuilderClass(parent); - PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) - .createType(builderClass, PsiSubstitutor.EMPTY); - SyntheticMethod builderMethod = new SyntheticMethod(parent, parent, "builder", returnType); - PsiType dataManagerType = JavaPsiFacade.getElementFactory(parent.getProject()) - .createTypeFromText("net.staticstudios.data.DataManager", parent); - builderMethod.addParameter("dataManager", dataManagerType); - builderMethod.addModifier(PsiModifier.PUBLIC); - builderMethod.addModifier(PsiModifier.STATIC); - builderMethod.addModifier(PsiModifier.FINAL); - return builderMethod; + return CachedValuesManager.getCachedValue(parent, BUILDER_METHOD_2_KEY, () -> { + PsiClass builderClass = getBuilderClass(parent); + PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) + .createType(builderClass, PsiSubstitutor.EMPTY); + SyntheticMethod builderMethod = new SyntheticMethod(parent, parent, "builder", returnType); + PsiType dataManagerType = JavaPsiFacade.getElementFactory(parent.getProject()) + .createTypeFromText("net.staticstudios.data.DataManager", parent); + builderMethod.addParameter("dataManager", dataManagerType); + builderMethod.addModifier(PsiModifier.PUBLIC); + builderMethod.addModifier(PsiModifier.STATIC); + builderMethod.addModifier(PsiModifier.FINAL); + return CachedValueProvider.Result.create(builderMethod, parent); + }); } private PsiMethod getQueryMethod(PsiClass parent) { -// return CachedValuesManager.getCachedValue(parent, () -> { -// PsiClass builderClass = getBuilderClass(parent); -// PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) -// .createType(builderClass, PsiSubstitutor.EMPTY); -// SyntheticBuilderMethod builderMethod = new SyntheticBuilderMethod(parent, "builder", returnType); -// return CachedValueProvider.Result.create(builderMethod, parent); -// }); - PsiClass queryClass = getQueryClass(parent); - PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) - .createType(queryClass, PsiSubstitutor.EMPTY); - SyntheticMethod builderMethod = new SyntheticMethod(parent, parent, "query", returnType); - builderMethod.addModifier(PsiModifier.PUBLIC); - builderMethod.addModifier(PsiModifier.STATIC); - builderMethod.addModifier(PsiModifier.FINAL); - return builderMethod; + return CachedValuesManager.getCachedValue(parent, QUERY_METHOD_KEY, () -> { + PsiClass queryClass = getQueryClass(parent); + PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) + .createType(queryClass, PsiSubstitutor.EMPTY); + SyntheticMethod queryMethod = new SyntheticMethod(parent, parent, "query", returnType); + queryMethod.addModifier(PsiModifier.PUBLIC); + queryMethod.addModifier(PsiModifier.STATIC); + queryMethod.addModifier(PsiModifier.FINAL); + return CachedValueProvider.Result.create(queryMethod, parent); + }); } private PsiMethod getQueryMethod2(PsiClass parent) { -// return CachedValuesManager.getCachedValue(parent, () -> { -// PsiClass builderClass = getBuilderClass(parent); -// PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) -// .createType(builderClass, PsiSubstitutor.EMPTY); -// SyntheticBuilderMethod builderMethod = new SyntheticBuilderMethod(parent, "builder", returnType); -// return CachedValueProvider.Result.create(builderMethod, parent); -// }); - PsiClass queryClass = getQueryClass(parent); - PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) - .createType(queryClass, PsiSubstitutor.EMPTY); - SyntheticMethod builderMethod = new SyntheticMethod(parent, parent, "query", returnType); - PsiType dataManagerType = JavaPsiFacade.getElementFactory(parent.getProject()) - .createTypeFromText("net.staticstudios.data.DataManager", parent); - builderMethod.addParameter("dataManager", dataManagerType); - builderMethod.addModifier(PsiModifier.PUBLIC); - builderMethod.addModifier(PsiModifier.STATIC); - builderMethod.addModifier(PsiModifier.FINAL); - return builderMethod; + return CachedValuesManager.getCachedValue(parent, QUERY_METHOD_2_KEY, () -> { + PsiClass queryClass = getQueryClass(parent); + PsiType returnType = JavaPsiFacade.getElementFactory(parent.getProject()) + .createType(queryClass, PsiSubstitutor.EMPTY); + SyntheticMethod queryMethod = new SyntheticMethod(parent, parent, "query", returnType); + PsiType dataManagerType = JavaPsiFacade.getElementFactory(parent.getProject()) + .createTypeFromText("net.staticstudios.data.DataManager", parent); + queryMethod.addParameter("dataManager", dataManagerType); + queryMethod.addModifier(PsiModifier.PUBLIC); + queryMethod.addModifier(PsiModifier.STATIC); + queryMethod.addModifier(PsiModifier.FINAL); + return CachedValueProvider.Result.create(queryMethod, parent); + }); } private SyntheticBuilderClass createBuilderBuilderClass(PsiClass parentClass) { @@ -170,6 +167,7 @@ private SyntheticBuilderClass createBuilderBuilderClass(PsiClass parentClass) { PsiType innerType = IntelliJPluginUtils.getGenericParameter(psiClassType, parentClass.getManager()); if (IntelliJPluginUtils.isValidPersistentValue(psiField)) { SyntheticMethod setterMethod = new SyntheticMethod(parentClass, builderClass, psiField.getName(), builderType); + setterMethod.setSourceField(psiField); setterMethod.addParameter(psiField.getName(), innerType); setterMethod.addModifier(PsiModifier.PUBLIC); setterMethod.addModifier(PsiModifier.FINAL); @@ -251,6 +249,7 @@ private SyntheticBuilderClass createQueryBuilderClass(PsiClass parentClass) { for (PsiField psiField : parentClass.getAllFields()) { if (IntelliJPluginUtils.isValidPersistentValue(psiField)) { SyntheticMethod orderByMethod = new SyntheticMethod(parentClass, queryClass, "orderBy" + StringUtil.capitalize(psiField.getName()), queryType); + orderByMethod.setSourceField(psiField); orderByMethod.addParameter("order", orderType); orderByMethod.addModifier(PsiModifier.PUBLIC); orderByMethod.addModifier(PsiModifier.FINAL); @@ -319,7 +318,7 @@ private SyntheticBuilderClass createQueryWhereBuilderClass(PsiClass parentClass) for (PsiField psiField : parentClass.getAllFields()) { PsiType type = psiField.getType(); boolean isValidReference = false; - if (!IntelliJPluginUtils.isValidPersistentValue(psiField) && !(isValidReference = IntelliJPluginUtils.isValidReference(psiField))) { + if (!IntelliJPluginUtils.isValidCachedValue(psiField) && !IntelliJPluginUtils.isValidPersistentValue(psiField) && !(isValidReference = IntelliJPluginUtils.isValidReference(psiField))) { continue; //non-supported field type } if (!(type instanceof PsiClassType psiClassType)) { @@ -331,6 +330,7 @@ private SyntheticBuilderClass createQueryWhereBuilderClass(PsiClass parentClass) for (QueryClause clause : clauses) { String methodName = clause.getMethodName(psiField.getName()); SyntheticMethod queryMethod = new SyntheticMethod(parentClass, whereClass, methodName, whereType); + queryMethod.setSourceField(psiField); List parameterTypes = clause.getMethodParamTypes(parentClass.getManager(), innerType, queryMethod); for (PsiParameter parameterType : parameterTypes) { queryMethod.addParameter(parameterType); diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/IntelliJPluginUtils.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/IntelliJPluginUtils.java index a773bef7..71a7b2a3 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/IntelliJPluginUtils.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/IntelliJPluginUtils.java @@ -3,8 +3,6 @@ import com.intellij.psi.*; import net.staticstudios.data.utils.Constants; -import java.util.Objects; - public class IntelliJPluginUtils { public static boolean genericTypeIs(PsiType type, String classFqn) { if (!(type instanceof PsiClassType psiClassType)) { @@ -22,29 +20,23 @@ public static boolean is(PsiType type, String classFqn) { if (!(type instanceof PsiClassType psiClassType)) { return false; } - PsiClass resolvedClass = psiClassType.resolve(); - if (resolvedClass == null) { - return false; - } - return classFqn.equals(resolvedClass.getQualifiedName()); + String canonicalText = psiClassType.rawType().getCanonicalText(); + return classFqn.equals(canonicalText); } public static boolean extendsClass(PsiClass psiClass, String classFqn) { - boolean extendsClass = false; - for (PsiClassType superType : psiClass.getSuperTypes()) { - String superTypeFqn = superType.resolve() != null ? Objects.requireNonNull(superType.resolve()).getQualifiedName() : null; - if (classFqn.equals(superTypeFqn)) { - extendsClass = true; - break; + for (PsiClass superClass : psiClass.getSupers()) { + String superFqn = superClass.getQualifiedName(); + if (classFqn.equals(superFqn)) { + return true; } - PsiClass superClass = superType.resolve(); - if (superClass != null && extendsClass(superClass, classFqn)) { - extendsClass = true; - break; + if (superFqn != null + && !superFqn.equals(Object.class.getName()) + && extendsClass(superClass, classFqn)) { + return true; } } - - return extendsClass; + return false; } public static boolean hasAnnotation(PsiModifierListOwner element, String annotationFqn) { @@ -96,4 +88,9 @@ public static boolean isValidReference(PsiField psiField) { return IntelliJPluginUtils.is(psiField.getType(), Constants.REFERENCE_FQN) && IntelliJPluginUtils.hasAnnotation(psiField, Constants.ONE_TO_ONE_ANNOTATION_FQN); } + + public static boolean isValidCachedValue(PsiField psiField) { + return IntelliJPluginUtils.is(psiField.getType(), Constants.CACHED_VALUE_FQN) && + IntelliJPluginUtils.hasAnnotation(psiField, Constants.IDENTIFIER_ANNOTATION_FQN); + } } diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticBuilderClass.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticBuilderClass.java index 0e17e391..e5df9566 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticBuilderClass.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticBuilderClass.java @@ -13,6 +13,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * A synthetic builder class generated for data classes. @@ -135,4 +136,17 @@ public PsiElement getParent() { public boolean isEquivalentTo(PsiElement another) { return PsiClassImplUtil.isClassEquivalentTo(this, another); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SyntheticBuilderClass other)) return false; + return Objects.equals(getName(), other.getName()) + && Objects.equals(parentClass.get(), other.parentClass.get()); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), parentClass.get()); + } } \ No newline at end of file diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticMethod.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticMethod.java index 3b02ddc1..d9208e12 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticMethod.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticMethod.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable; import java.lang.ref.WeakReference; +import java.util.Objects; /** * A synthetic method generated for data classes. @@ -19,6 +20,7 @@ public class SyntheticMethod extends LightMethodBuilder implements SyntheticElem private final PsiClass containingClass; private final PsiType returnType; private final String name; + private @Nullable PsiField sourceField; public SyntheticMethod(@NotNull PsiClass parentClass, @NotNull PsiClass containingClass, @NotNull String name, PsiType returnType) { super(parentClass, parentClass.getLanguage()); @@ -29,6 +31,10 @@ public SyntheticMethod(@NotNull PsiClass parentClass, @NotNull PsiClass containi setContainingClass(containingClass); } + public void setSourceField(@Nullable PsiField sourceField) { + this.sourceField = sourceField; + } + @Override public @Nullable PsiType getReturnType() { return returnType; @@ -92,6 +98,9 @@ public String toString() { @Override public @NotNull PsiElement getNavigationElement() { + if (sourceField != null && sourceField.isValid()) { + return sourceField.getNavigationElement(); + } PsiClass cls = parentClass.get(); if (cls != null) { return cls.getNavigationElement(); @@ -113,4 +122,31 @@ public boolean isDeprecated() { public @Nullable PsiDocComment getDocComment() { return null; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SyntheticMethod other)) return false; + if (!Objects.equals(name, other.name)) return false; + if (!Objects.equals(containingClass, other.containingClass)) return false; + PsiParameter[] params = getParameterList().getParameters(); + PsiParameter[] otherParams = other.getParameterList().getParameters(); + if (params.length != otherParams.length) return false; + for (int i = 0; i < params.length; i++) { + if (!Objects.equals(params[i].getName(), otherParams[i].getName())) return false; + if (!Objects.equals(params[i].getType().getCanonicalText(), otherParams[i].getType().getCanonicalText())) + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = Objects.hash(name, containingClass); + PsiParameter[] params = getParameterList().getParameters(); + for (PsiParameter param : params) { + result = 31 * result + Objects.hash(param.getName(), param.getType().getCanonicalText()); + } + return result; + } } \ No newline at end of file diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryBuilderUtils.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryBuilderUtils.java index 9f2d3a0b..709cf0da 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryBuilderUtils.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryBuilderUtils.java @@ -4,6 +4,7 @@ import com.intellij.psi.PsiType; import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; import net.staticstudios.data.ide.intellij.query.clause.*; +import net.staticstudios.data.ide.intellij.query.clause.cv.*; import java.sql.Timestamp; import java.util.ArrayList; @@ -11,6 +12,7 @@ public class QueryBuilderUtils { private static final List pvClauses; + private static final List cvClauses; private static final List referenceClauses; static { @@ -40,6 +42,16 @@ public class QueryBuilderUtils { pvClauses.add(new IsBetweenClause()); pvClauses.add(new IsNotBetweenClause()); + cvClauses = new ArrayList<>(); + cvClauses.add(new CachedValueIsClause()); + cvClauses.add(new CachedValueIsNotClause()); + cvClauses.add(new CachedValueIsNotNullClause()); + cvClauses.add(new CachedValueIsNullClause()); + cvClauses.add(new CachedValueIsInArrayClause()); + cvClauses.add(new CachedValueIsInCollectionClause()); + cvClauses.add(new CachedValueIsNotInArrayClause()); + cvClauses.add(new CachedValueIsNotInCollectionClause()); + referenceClauses = new ArrayList<>(); //todo: supporting these clauses in the java-c plugin is more involved than pvs, so until those are implemented // these will remain diables. at the time of writing this, uncommenting this will cause IJ to behave as expected. @@ -68,6 +80,16 @@ public static List getClausesForType(PsiField psiField, boolean nul } return applicableClauses; } + + if (IntelliJPluginUtils.isValidCachedValue(psiField)) { + List applicableClauses = new ArrayList<>(); + for (QueryClause clause : cvClauses) { + if (clause.matches(psiField, nullable)) { + applicableClauses.add(clause); + } + } + return applicableClauses; + } return List.of(); } diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsClause.java new file mode 100644 index 00000000..132ccf9d --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsClause.java @@ -0,0 +1,25 @@ +package net.staticstudios.data.ide.intellij.query.clause.cv; + +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class CachedValueIsClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return true; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "Is"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of(new LightParameter("value", fieldType, scope)); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsInArrayClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsInArrayClause.java new file mode 100644 index 00000000..57b3558a --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsInArrayClause.java @@ -0,0 +1,49 @@ +package net.staticstudios.data.ide.intellij.query.clause.cv; + +import com.google.common.base.Preconditions; +import com.intellij.lang.java.JavaLanguage; +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.IncorrectOperationException; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class CachedValueIsInArrayClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return true; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsIn"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + PsiElementFactory factory = JavaPsiFacade.getElementFactory(manager.getProject()); + + String arrayTypeName = fieldType.getPresentableText(); + String dummyMethodText = "void dummy(" + arrayTypeName + "... values) {}"; + + try { + PsiMethod dummyMethod = factory.createMethodFromText(dummyMethodText, scope); + PsiParameter varargsParam = dummyMethod.getParameterList().getParameters()[0]; + LightParameter lightParam = new LightParameter( + varargsParam.getName(), + varargsParam.getType(), + scope, + JavaLanguage.INSTANCE, + true + ); + + return List.of(lightParam); + + } catch (IncorrectOperationException e) { + return List.of(new LightParameter("values", fieldType.createArrayType(), scope, JavaLanguage.INSTANCE, true)); + } + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsInCollectionClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsInCollectionClause.java new file mode 100644 index 00000000..05e528be --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsInCollectionClause.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.ide.intellij.query.clause.cv; + +import com.google.common.base.Preconditions; +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; +import com.intellij.psi.search.GlobalSearchScope; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class CachedValueIsInCollectionClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return true; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsIn"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + JavaPsiFacade facade = JavaPsiFacade.getInstance(manager.getProject()); + PsiClass collectionType = facade.findClass("java.util.Collection", GlobalSearchScope.allScope(manager.getProject())); + Preconditions.checkNotNull(collectionType, "Could not find java.util.Collection class"); + PsiTypeParameter[] typeParameters = collectionType.getTypeParameters(); + Preconditions.checkState(typeParameters.length == 1, "Expected Collection to have one type parameter"); + PsiSubstitutor substitutor = PsiSubstitutor.EMPTY; + substitutor = substitutor.put(typeParameters[0], fieldType); + return List.of(new LightParameter("values", facade.getElementFactory().createType(collectionType, substitutor), scope)); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotClause.java new file mode 100644 index 00000000..2130a129 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotClause.java @@ -0,0 +1,25 @@ +package net.staticstudios.data.ide.intellij.query.clause.cv; + +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class CachedValueIsNotClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return true; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNot"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of(new LightParameter("value", fieldType, scope)); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotInArrayClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotInArrayClause.java new file mode 100644 index 00000000..1ea36767 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotInArrayClause.java @@ -0,0 +1,47 @@ +package net.staticstudios.data.ide.intellij.query.clause.cv; + +import com.intellij.lang.java.JavaLanguage; +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; +import com.intellij.util.IncorrectOperationException; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class CachedValueIsNotInArrayClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return true; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNotIn"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + PsiElementFactory factory = JavaPsiFacade.getElementFactory(manager.getProject()); + + String arrayTypeName = fieldType.getPresentableText(); + String dummyMethodText = "void dummy(" + arrayTypeName + "... values) {}"; + + try { + PsiMethod dummyMethod = factory.createMethodFromText(dummyMethodText, scope); + PsiParameter varargsParam = dummyMethod.getParameterList().getParameters()[0]; + LightParameter lightParam = new LightParameter( + varargsParam.getName(), + varargsParam.getType(), + scope, + JavaLanguage.INSTANCE, + true + ); + + return List.of(lightParam); + + } catch (IncorrectOperationException e) { + return List.of(new LightParameter("values", fieldType.createArrayType(), scope, JavaLanguage.INSTANCE, true)); + } + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotInCollectionClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotInCollectionClause.java new file mode 100644 index 00000000..8100ec82 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotInCollectionClause.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.ide.intellij.query.clause.cv; + +import com.google.common.base.Preconditions; +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; +import com.intellij.psi.search.GlobalSearchScope; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class CachedValueIsNotInCollectionClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return true; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNotIn"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + JavaPsiFacade facade = JavaPsiFacade.getInstance(manager.getProject()); + PsiClass collectionType = facade.findClass("java.util.Collection", GlobalSearchScope.allScope(manager.getProject())); + Preconditions.checkNotNull(collectionType, "Could not find java.util.Collection class"); + PsiTypeParameter[] typeParameters = collectionType.getTypeParameters(); + Preconditions.checkState(typeParameters.length == 1, "Expected Collection to have one type parameter"); + PsiSubstitutor substitutor = PsiSubstitutor.EMPTY; + substitutor = substitutor.put(typeParameters[0], fieldType); + return List.of(new LightParameter("values", facade.getElementFactory().createType(collectionType, substitutor), scope)); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotNullClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotNullClause.java new file mode 100644 index 00000000..ac0240a0 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotNullClause.java @@ -0,0 +1,24 @@ +package net.staticstudios.data.ide.intellij.query.clause.cv; + +import com.intellij.psi.*; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class CachedValueIsNotNullClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return true; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNotNull"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of(); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNullClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNullClause.java new file mode 100644 index 00000000..dd9a9f3f --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNullClause.java @@ -0,0 +1,25 @@ +package net.staticstudios.data.ide.intellij.query.clause.cv; + +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class CachedValueIsNullClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return true; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNull"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of(); + } +} diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml index a46e8026..4174896a 100644 --- a/intellij-plugin/src/main/resources/META-INF/plugin.xml +++ b/intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -1,6 +1,6 @@ - net.staticstudios.data.ide.intellij - static-data + net.staticstudios.data.ide + Static-Data Static Studios 1.0.0 diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java index 0f60d890..bfa74021 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java @@ -4,6 +4,7 @@ import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Context; import net.staticstudios.data.Data; +import net.staticstudios.data.compiler.javac.javac.ParsedCachedValue; import net.staticstudios.data.compiler.javac.javac.ParsedPersistentValue; import net.staticstudios.data.compiler.javac.javac.ParsedReference; import net.staticstudios.data.compiler.javac.util.TypeUtils; @@ -19,6 +20,7 @@ public record ProcessorContext( TypeElement dataClassElement, JCTree.JCClassDecl dataClassDecl, Collection persistentValues, + Collection cachedValues, Collection references ) { } diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java index bd196d01..d620633b 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java @@ -8,10 +8,7 @@ import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.Names; import net.staticstudios.data.Data; -import net.staticstudios.data.compiler.javac.javac.BuilderProcessor; -import net.staticstudios.data.compiler.javac.javac.ParsedPersistentValue; -import net.staticstudios.data.compiler.javac.javac.ParsedReference; -import net.staticstudios.data.compiler.javac.javac.QueryBuilderProcessor; +import net.staticstudios.data.compiler.javac.javac.*; import net.staticstudios.data.compiler.javac.util.TypeUtils; import sun.misc.Unsafe; @@ -151,6 +148,7 @@ public boolean process(Set annotations, RoundEnvironment if (!BuilderProcessor.hasProcessed(classDecl)) { Data dataAnnotation = e.getAnnotation(Data.class); Collection persistentValues = ParsedPersistentValue.extractPersistentValues(typeElement, dataAnnotation, typeUtils); + Collection cachedValues = ParsedCachedValue.extractCachedValues(typeElement, dataAnnotation, typeUtils); Collection references = ParsedReference.extractReferences(typeElement, dataAnnotation, typeUtils); ProcessorContext processorContext = new ProcessorContext( javacProcessingEnvironment.getContext(), @@ -160,6 +158,7 @@ public boolean process(Set annotations, RoundEnvironment (TypeElement) e, classDecl, persistentValues, + cachedValues, references ); new BuilderProcessor(processorContext).runProcessor(); diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedCachedValue.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedCachedValue.java new file mode 100644 index 00000000..46df082f --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedCachedValue.java @@ -0,0 +1,91 @@ +package net.staticstudios.data.compiler.javac.javac; + +import net.staticstudios.data.Data; +import net.staticstudios.data.Identifier; +import net.staticstudios.data.compiler.javac.util.SimpleField; +import net.staticstudios.data.compiler.javac.util.TypeUtils; +import net.staticstudios.data.utils.Constants; +import org.jetbrains.annotations.NotNull; + +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import java.util.ArrayList; +import java.util.Collection; + +public class ParsedCachedValue extends ParsedValue { + private final String schema; + private final String table; + private final String identifier; + + public ParsedCachedValue(String fieldName, String schema, String table, String identifier, TypeElement type) { + super(fieldName, type); + this.schema = schema; + this.table = table; + this.identifier = identifier; + } + + public static Collection extractCachedValues(@NotNull TypeElement dataClass, + @NotNull Data dataAnnotation, + @NotNull TypeUtils typeUtils + + ) { + Collection cachedValues = new ArrayList<>(); + Collection fields = typeUtils.getFields(dataClass, Constants.CACHED_VALUE_FQN); + for (SimpleField pvField : fields) { + Element fieldElement = pvField.element(); + Identifier identifierAnnotation = fieldElement.getAnnotation(Identifier.class); + if (identifierAnnotation == null) { + continue; + } + + String schemaValue = dataAnnotation.schema(); + String tableValue = dataAnnotation.table(); + String identifierValue = identifierAnnotation.value(); + + TypeMirror genericTypeMirror = typeUtils.getGenericType(fieldElement, 0); + TypeElement typeElement = (TypeElement) ((DeclaredType) genericTypeMirror).asElement(); + ParsedCachedValue persistentValue = new ParsedCachedValue( + pvField.name(), + schemaValue, + tableValue, + identifierValue, + typeElement + ); + + cachedValues.add(persistentValue); + + } + + return cachedValues; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getIdentifier() { + return identifier; + } + + + public String[] getTypeFQNParts() { + return type.getQualifiedName().toString().split("\\."); + } + + @Override + public String toString() { + return "ParsedCacheValue{" + + "fieldName='" + fieldName + '\'' + + ", schema='" + schema + '\'' + + ", table='" + table + '\'' + + ", identifier='" + identifier + '\'' + + ", type=" + type + + '}'; + } +} diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java index 2479ea59..32c57222 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java @@ -15,12 +15,14 @@ public class QueryBuilderProcessor extends AbstractBuilderProcessor { private final Collection persistentValues; + private final Collection cachedValues; private final Collection references; private final String whereClassName; public QueryBuilderProcessor(ProcessorContext processorContext) { super(processorContext, "QueryBuilder", "query"); this.persistentValues = processorContext.persistentValues(); + this.cachedValues = processorContext.cachedValues(); this.references = processorContext.references(); QueryWhereProcessor whereProcessor = new QueryWhereProcessor(processorContext); @@ -62,6 +64,8 @@ protected void process() { for (ParsedPersistentValue pv : persistentValues) { processValue(pv); } + + } @@ -263,6 +267,10 @@ protected void process() { processValue(pv); } + for (ParsedCachedValue cv : cachedValues) { + processValue(cv); + } + // for (ParsedReference ref : references) { //todo: process references and support is, isNot, isNull and isNotNull // } @@ -308,6 +316,21 @@ private void processValue(ParsedPersistentValue pv) { } } + private void processValue(ParsedCachedValue cv) { + String schemaFieldName = storeSchema(cv.getFieldName(), cv.getSchema()); + String tableFieldName = storeTable(cv.getFieldName(), cv.getTable()); + String identifierFieldName = storeColumn(cv.getFieldName(), cv.getIdentifier()); + + addCachedValueIsMethod(cv, schemaFieldName, tableFieldName, identifierFieldName); + addCachedValueIsNotMethod(cv, schemaFieldName, tableFieldName, identifierFieldName); + addCachedValueIsNullMethod(cv, schemaFieldName, tableFieldName, identifierFieldName); + addCachedValueIsNotNullMethod(cv, schemaFieldName, tableFieldName, identifierFieldName); + addCachedValueIsInCollectionMethod(cv, schemaFieldName, tableFieldName, identifierFieldName); + addCachedValueIsInArrayMethod(cv, schemaFieldName, tableFieldName, identifierFieldName); + addCachedValueIsNotInCollectionMethod(cv, schemaFieldName, tableFieldName, identifierFieldName); + addCachedValueIsNotInArrayMethod(cv, schemaFieldName, tableFieldName, identifierFieldName); + } + private List clause(ParsedPersistentValue pv, JCTree.JCStatement... statements) { java.util.List list = new ArrayList<>(); @@ -1187,5 +1210,331 @@ private void addOrMethod() { null ), builderClassDecl); } + + private void addCachedValueIsMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(cv.getFieldName() + "Is"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(cv.getFieldName()), + chainDots(cv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("cachedValueEqualsClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(identifierFieldName)), + Ident(names.fromString(cv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + ) + ), + null + ), builderClassDecl); + } + + private void addCachedValueIsNotMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(cv.getFieldName() + "IsNot"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(cv.getFieldName()), + chainDots(cv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("cachedValueNotEqualsClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(identifierFieldName)), + Ident(names.fromString(cv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + ) + ), + null + ), builderClassDecl); + } + + private void addCachedValueIsInCollectionMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(cv.getFieldName() + "IsIn"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(cv.getFieldName()), + TypeApply( + chainDots("java", "util", "Collection"), + List.of( + chainDots(cv.getTypeFQNParts()) + ) + ), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("cachedValueInClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(identifierFieldName)), + Apply( + List.nil(), + Select( + Ident(names.fromString(cv.getFieldName())), + names.fromString("toArray") + ), + List.nil() + ) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addCachedValueIsInArrayMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(cv.getFieldName() + "IsIn"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER | Flags.VARARGS), + names.fromString(cv.getFieldName()), + TypeArray( + chainDots(cv.getTypeFQNParts()) + ), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("cachedValueInClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(identifierFieldName)), + Ident(names.fromString(cv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addCachedValueIsNotInCollectionMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(cv.getFieldName() + "IsNotIn"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(cv.getFieldName()), + TypeApply( + chainDots("java", "util", "Collection"), + List.of( + chainDots(cv.getTypeFQNParts()) + ) + ), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("cachedValueNotInClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(identifierFieldName)), + Apply( + List.nil(), + Select( + Ident(names.fromString(cv.getFieldName())), + names.fromString("toArray") + ), + List.nil() + ) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addCachedValueIsNotInArrayMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(cv.getFieldName() + "IsNotIn"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER | Flags.VARARGS), + names.fromString(cv.getFieldName()), + TypeArray( + chainDots(cv.getTypeFQNParts()) + ), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("cachedValueNotInClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(identifierFieldName)), + Ident(names.fromString(cv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addCachedValueIsNullMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(cv.getFieldName() + "IsNull"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("cachedValueNullClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(identifierFieldName)) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addCachedValueIsNotNullMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(cv.getFieldName() + "IsNotNull"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("cachedValueNotNullClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(identifierFieldName)) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } } } diff --git a/utils/src/main/java/net/staticstudios/data/utils/Constants.java b/utils/src/main/java/net/staticstudios/data/utils/Constants.java index 3338afc2..d058be5b 100644 --- a/utils/src/main/java/net/staticstudios/data/utils/Constants.java +++ b/utils/src/main/java/net/staticstudios/data/utils/Constants.java @@ -8,7 +8,9 @@ public class Constants { public static final String MANY_TO_MANY_ANNOTATION_FQN = "net.staticstudios.data.ManyToMany"; public static final String ONE_TO_MANY_ANNOTATION_FQN = "net.staticstudios.data.OneToMany"; public static final String ONE_TO_ONE_ANNOTATION_FQN = "net.staticstudios.data.OneToOne"; + public static final String IDENTIFIER_ANNOTATION_FQN = "net.staticstudios.data.Identifier"; public static final String PERSISTENT_VALUE_FQN = "net.staticstudios.data.PersistentValue"; + public static final String CACHED_VALUE_FQN = "net.staticstudios.data.CachedValue"; public static final String PERSISTENT_COLLECTION_FQN = "net.staticstudios.data.PersistentCollection"; public static final String REFERENCE_FQN = "net.staticstudios.data.Reference"; public static final String UNIQUE_DATA_FQN = "net.staticstudios.data.UniqueData";