From 54b848a5b584477bcebba8b89408ccd9c0e0036e Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 7 Mar 2026 16:12:09 -0500 Subject: [PATCH 01/22] add statistics tracking --- .../net/staticstudios/data/DataManager.java | 4 ++ .../net/staticstudios/data/StaticData.java | 13 ++++ .../data/StaticDataStatistics.java | 23 +++++++ .../staticstudios/data/impl/DataAccessor.java | 4 ++ .../data/impl/h2/H2DataAccessor.java | 24 +++++++ .../data/util/SlidingWindowCounter.java | 64 +++++++++++++++++++ 6 files changed, 132 insertions(+) create mode 100644 core/src/main/java/net/staticstudios/data/StaticDataStatistics.java create mode 100644 core/src/main/java/net/staticstudios/data/util/SlidingWindowCounter.java diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 9cef61c6..7bc1be0d 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -1529,4 +1529,8 @@ public void flushTaskQueue() { //Ignore }).join(); } + + public Optional getStatistics() { + return dataAccessor.getStatistics(); + } } diff --git a/core/src/main/java/net/staticstudios/data/StaticData.java b/core/src/main/java/net/staticstudios/data/StaticData.java index d5ce8f02..43e04de3 100644 --- a/core/src/main/java/net/staticstudios/data/StaticData.java +++ b/core/src/main/java/net/staticstudios/data/StaticData.java @@ -5,6 +5,8 @@ import net.staticstudios.data.query.QueryBuilder; import org.jetbrains.annotations.Blocking; +import java.util.Optional; + /** * Entry point for initializing and interacting with the StaticData system. */ @@ -90,4 +92,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 an Optional containing the StaticDataStatistics if available, or an empty Optional if statistics cannot be retrieved at this time + */ + public static Optional 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..23f0a943 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/StaticDataStatistics.java @@ -0,0 +1,23 @@ +package net.staticstudios.data; + +public class StaticDataStatistics { + private final long queriesPerSecond; + private final long updatesPerSecond; + + public StaticDataStatistics(long queriesPerSecond, long updatesPerSecond) { + this.queriesPerSecond = queriesPerSecond; + this.updatesPerSecond = updatesPerSecond; + } + + public long getQueriesPerSecond() { + return queriesPerSecond; + } + + public long getUpdatesPerSecond() { + return updatesPerSecond; + } + + public long getOperationsPerSecond() { + return queriesPerSecond + updatesPerSecond; + } +} 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..ec12f84f 100644 --- a/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java +++ b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java @@ -1,6 +1,7 @@ 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.SQLTransaction; import net.staticstudios.data.util.SQlStatement; @@ -10,6 +11,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.Optional; public interface DataAccessor { @@ -34,4 +36,6 @@ default void executeUpdate(SQLTransaction.Statement statement, List valu void discoverRedisKeys(List partialRedisKeys); void resync(); + + Optional getStatistics(); } 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..ccb4bb70 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,6 +5,7 @@ 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.H2UpdateHandlerTrigger; import net.staticstudios.data.impl.pg.PostgresListener; @@ -67,6 +68,9 @@ public class H2DataAccessor implements DataAccessor { 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 +157,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 +192,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 +221,7 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener } logger.debug("[H2] [HANDLE POSTGRES DELETE] {}", sql); preparedStatement.executeUpdate(); + h2UpdateCounter.increment(); if (!connection.getAutoCommit()) { connection.commit(); } @@ -386,6 +393,7 @@ public void insert(List sqlStatements, InsertMode insertMode) thro } logger.trace("[H2] {}", sqlStatement.getH2Sql()); preparedStatement.executeUpdate(); + h2UpdateCounter.increment(); } } @@ -437,6 +445,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 +468,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); } @@ -688,6 +699,14 @@ private void handleRedisEvent(RedisEvent event, String key, @Nullable String val } } + public double getH2QueriesPerSecond() { + return h2QueryCounter.getPerSecond(); + } + + public double getH2UpdatesPerSecond() { + return h2UpdateCounter.getPerSecond(); + } + public void onCommit(Runnable callback) { commitCallbacks.get().add(callback); } @@ -733,4 +752,9 @@ private String encodeRedis(Object value) { private RedisEncodedValue decodeRedis(String encoded) { return GSON.fromJson(encoded, RedisEncodedValue.class); } + + @Override + public Optional getStatistics() { + return Optional.of(new StaticDataStatistics((long) h2QueryCounter.getPerSecond(), (long) h2UpdateCounter.getPerSecond())); + } } 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(); + } + } + } +} + From 0276fdf94af9389ed9a93ac81dcbe8ae1356a9c6 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 7 Mar 2026 18:58:36 -0500 Subject: [PATCH 02/22] some caching --- core/build.gradle | 2 + .../net/staticstudios/data/DataManager.java | 78 ++++++++++++ .../data/impl/data/CachedValueImpl.java | 23 ++-- .../PersistentManyToManyCollectionImpl.java | 27 ++-- .../PersistentOneToManyCollectionImpl.java | 27 ++-- ...ersistentOneToManyValueCollectionImpl.java | 41 +++--- .../data/impl/data/PersistentValueImpl.java | 24 ++-- .../data/impl/data/ReadOnlyCachedValue.java | 23 ++-- .../impl/data/ReadOnlyPersistentValue.java | 30 +++-- .../data/impl/data/ReadOnlyReference.java | 34 ++--- .../data/ReadOnlyReferenceCollection.java | 43 ++++--- .../impl/data/ReadOnlyValuedCollection.java | 29 ++--- .../data/impl/data/ReferenceImpl.java | 118 ++++++++++-------- .../data/impl/h2/H2DataAccessor.java | 23 +++- .../H2ReadCacheInvalidatorTrigger.java | 87 +++++++++++++ .../net/staticstudios/data/util/Cell.java | 54 ++++++++ .../data/util/ReadCacheResult.java | 35 ++++++ .../data/util/ReferenceMetadata.java | 113 ++++++++++++++++- .../data/util/ReflectionUtils.java | 2 +- .../staticstudios/data/util/SelectQuery.java | 41 ++++++ 20 files changed, 660 insertions(+), 194 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2ReadCacheInvalidatorTrigger.java create mode 100644 core/src/main/java/net/staticstudios/data/util/Cell.java create mode 100644 core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java create mode 100644 core/src/main/java/net/staticstudios/data/util/SelectQuery.java 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/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 7bc1be0d..150605f4 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; @@ -34,6 +37,7 @@ 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 @@ -57,6 +61,8 @@ public class DataManager { private final Set registeredUpdateHandlersForRedis = ConcurrentHashMap.newKeySet(); private final Set registeredChangeHandlersForCollection = ConcurrentHashMap.newKeySet(); private final Set registeredUpdateHandlersForReference = ConcurrentHashMap.newKeySet(); + private final Cache readCache; + private final Map> dependencyToCacheMapping = new ConcurrentHashMap<>(); private final List> valueSerializers = new CopyOnWriteArrayList<>(); private final Consumer updateHandlerExecutor; @@ -91,6 +97,16 @@ public DataManager(StaticDataConfig config, boolean setGlobal) { sqlBuilder = new SQLBuilder(this); dataAccessor = new H2DataAccessor(this, postgresListener, redisListener, taskQueue); + //todo: params for cache should be configurable + this.readCache = Caffeine.newBuilder() + .maximumSize(50_000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .removalListener((SelectQuery selectQuery, ReadCacheResult result, RemovalCause cause) -> { + cleanupReadCacheEntry(selectQuery, result); + + }) + .build(); + //todo: when we reconnect to postgres, refresh the internal cache from the source } @@ -1533,4 +1549,66 @@ public void flushTaskQueue() { public Optional getStatistics() { return dataAccessor.getStatistics(); } + + public @Nullable ReadCacheResult getReadCacheResult(SelectQuery query) { + return readCache.getIfPresent(query); + } + + public void putReadCacheResult(SelectQuery query, @NotNull ReadCacheResult result) { + logger.trace("Putting result in read cache for query {} with result {}", query, result); + readCache.put(query, result); + for (Cell cell : result.getDependencies()) { + dependencyToCacheMapping.computeIfAbsent(cell, k -> new HashSet<>()) + .add(query); + } + } + + public void invalidateReadCache(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 = dependencyToCacheMapping.remove(cell); + if (queries != null) { + for (SelectQuery query : queries) { + ReadCacheResult res = readCache.getIfPresent(query); + readCache.invalidate(query); + + if (res != null) { + cleanupReadCacheEntry(query, res); + } + + logger.trace("Invalidated read cache for query {} due to change in cell {}", query, cell); + } + } + } + } + } + } + + private void cleanupReadCacheEntry(@NotNull SelectQuery query, @NotNull ReadCacheResult res) { + for (Cell dependency : res.getDependencies()) { + Set dependentQueries = dependencyToCacheMapping.get(dependency); + if (dependentQueries != null) { + dependentQueries.remove(query); + if (dependentQueries.isEmpty()) { + dependencyToCacheMapping.remove(dependency); + } + } + } + } } 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..05ba2d9d 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, ReflectionUtils.getGenericType(field), 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; 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..0d791152 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 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()))); } 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, ReflectionUtils.getGenericType(field), 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..20ad1177 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,12 @@ 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.ReflectionUtils; +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 +36,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, ReflectionUtils.getGenericType(field), 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..35e2ca4c 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,36 @@ 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.getReadCacheResult(query); + + if (cached != null) { + 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 = cached.getValue(idColumn.name()); + 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 +154,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.putReadCacheResult(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 ccb4bb70..b1056f51 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 @@ -7,6 +7,7 @@ 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; @@ -580,7 +581,7 @@ 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()); @@ -589,7 +590,7 @@ private synchronized void updateKnownTables() throws SQLException { 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()); @@ -598,6 +599,24 @@ private synchronized void updateKnownTables() throws SQLException { 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, randomId.toString().replace('-', '_'), schema, table, H2ReadCacheInvalidatorTrigger.class.getName()); + logger.trace("[H2] {}", formatted); + H2ReadCacheInvalidatorTrigger.registerDataManager(randomId, dataManager); + 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, randomId.toString().replace('-', '_'), schema, table, H2ReadCacheInvalidatorTrigger.class.getName()); + logger.trace("[H2] {}", formatted); + H2ReadCacheInvalidatorTrigger.registerDataManager(randomId, dataManager); + createTrigger.execute(formatted); + } + taskQueue.submitTask(realDbConnection -> postgresListener.ensureTableHasTrigger(realDbConnection, schema, table)).join(); } } 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..803d8c43 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2ReadCacheInvalidatorTrigger.java @@ -0,0 +1,87 @@ +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.*; +import java.util.concurrent.ConcurrentHashMap; + +public class H2ReadCacheInvalidatorTrigger implements Trigger { + private static final Map dataManagerMap = new ConcurrentHashMap<>(); + 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; + + 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.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.invalidateReadCache(columnNames, schema, table, changedColumns, oldRow)); + } + + private void handleDelete(Object[] oldRow) { + dataAccessor.onCommit(() -> dataManager.invalidateReadCache(columnNames, schema, table, columnNames, oldRow)); + } +} 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/ReadCacheResult.java b/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java new file mode 100644 index 00000000..c87e2eb4 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java @@ -0,0 +1,35 @@ +package net.staticstudios.data.util; + +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +public class ReadCacheResult { + private final ColumnValuePairs columnValuePairs; + private final Set dependencies; + + public ReadCacheResult(ColumnValuePairs columnValuePairs, Set dependencies) { + this.columnValuePairs = columnValuePairs; + this.dependencies = dependencies; + } + + public @Nullable Object getValue(String column) { + for (ColumnValuePair pair : columnValuePairs) { + if (pair.column().equals(column)) { + return pair.value(); + } + } + return null; + } + + public Set getDependencies() { + return dependencies; + } + + @Override + public String toString() { + return "ReadCacheResult[" + + "columnValuePairs=" + columnValuePairs + ", " + + "dependencies=" + dependencies + ']'; + } +} 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..83642e1d 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,119 @@ 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; + + 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; + } + + @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/ReflectionUtils.java b/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java index d58d96a8..0ff440cb 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java +++ b/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java @@ -62,7 +62,7 @@ public static List getFieldInstances(Object instance, Class fieldType) } - public static @Nullable Class getGenericType(Field field) { + public static @Nullable Class getGenericType(Field field) { //todo: this is called a lot at runtime, this should not be the case. it is only valid to call this during metadata extraction if (field.getGenericType() instanceof Class) { return (Class) field.getGenericType(); } else if (field.getGenericType() instanceof java.lang.reflect.ParameterizedType parameterizedType) { 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 + ']'; + } +} From c3b9e32671291bad25335afa149b2494f5700fdc Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 7 Mar 2026 19:10:00 -0500 Subject: [PATCH 03/22] cache calls to getInstance() --- .../net/staticstudios/data/DataManager.java | 38 ++++++++++++++++--- .../data/util/ColumnValuePairs.java | 2 + 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 150605f4..2e09f14a 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -1044,19 +1044,45 @@ public T getInstance(Class clazz, @NotNull ColumnValue String schema = metadata.schema(); String table = metadata.table(); - StringBuilder sqlBuilder = new StringBuilder(); + boolean exists; + + StringBuilder sqlBuilder = new StringBuilder(); //todo: this query should be cached in the unique data metadata. no need to constantly allocate strings sqlBuilder.append("SELECT 1 FROM \"").append(schema).append("\".\"").append(table).append("\" WHERE "); for (ColumnValuePair columnValuePair : idColumns) { sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); } 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 = getReadCacheResult(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); + putReadCacheResult(selectQuery, result); + } + + } catch (SQLException e) { + throw new RuntimeException(e); } - } catch (SQLException e) { - throw new RuntimeException(e); + } + + if (!exists) { + return null; } PersistentValueImpl.delegate(instance); 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..ed30f177 100644 --- a/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java +++ b/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java @@ -9,6 +9,8 @@ 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) { From 1ae8b1a75464153b821d54f9bce002ab028382ec Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 7 Mar 2026 20:42:40 -0500 Subject: [PATCH 04/22] special case for #findOne() --- .../data/query/BaseQueryBuilder.java | 9 +++- .../data/query/BaseQueryWhere.java | 42 +++++++++++++++++-- .../data/query/clause/EqualsClause.java | 16 +++++++ 3 files changed, 62 insertions(+), 5 deletions(-) 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..6b8658ad 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<>(); 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..b67d9f63 100644 --- a/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java @@ -2,13 +2,14 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.query.clause.*; +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 { @@ -180,6 +181,39 @@ public void buildWhereClause(StringBuilder sb, List parameters) { buildWhereClauseRecursive(root, 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 ConditionalClause) { + return isSpecialOnlyUseIdColumnsRecursive(node.lhs, schema, table, columns, columnValuePairs) && isSpecialOnlyUseIdColumnsRecursive(node.rhs, schema, table, columns, columnValuePairs); + } + return false; + } + static class Node { Clause clause; Node lhs; 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("\" = ?"); From 09cb96b25d4dfdf62d3d6b0036a94d9262df728e Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 7 Mar 2026 21:08:22 -0500 Subject: [PATCH 05/22] optimize CachedValue#get --- .../net/staticstudios/data/DataManager.java | 12 ++--- .../staticstudios/data/impl/DataAccessor.java | 5 +- .../data/impl/h2/H2DataAccessor.java | 31 ++++++++---- .../data/impl/redis/RedisListener.java | 2 +- .../data/util/redis/RedisIdentifier.java | 11 +++++ .../data/util/{ => redis}/RedisUtils.java | 49 +++++++++++++++++-- .../staticstudios/data/CachedValueTest.java | 2 +- 7 files changed, 89 insertions(+), 23 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/util/redis/RedisIdentifier.java rename core/src/main/java/net/staticstudios/data/util/{ => redis}/RedisUtils.java (58%) diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 2e09f14a..09401042 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -20,6 +20,8 @@ import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.data.util.redis.RedisIdentifier; +import net.staticstudios.data.util.redis.RedisUtils; import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; @@ -1400,9 +1402,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(new RedisIdentifier(holderSchema, holderTable, identifier, idColumns)); if (encoded == null) { return null; } @@ -1410,11 +1411,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(new RedisIdentifier(holderSchema, holderTable, identifier, idColumns), encoded, expireAfterSeconds); } private boolean hasCycle(SQLTable table, Map> dependencyGraph, Set visited, Set stack) { 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 ec12f84f..e4bb0ff5 100644 --- a/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java +++ b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java @@ -5,6 +5,7 @@ import net.staticstudios.data.parse.DDLStatement; import net.staticstudios.data.util.SQLTransaction; import net.staticstudios.data.util.SQlStatement; +import net.staticstudios.data.util.redis.RedisIdentifier; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; @@ -29,9 +30,9 @@ default void executeUpdate(SQLTransaction.Statement statement, List valu void postDDL() throws SQLException; - @Nullable String getRedisValue(String key); + @Nullable String getRedisValue(RedisIdentifier identifier); - void setRedisValue(String key, String value, int expirationSeconds); + void setRedisValue(RedisIdentifier identifier, String value, int expirationSeconds); void discoverRedisKeys(List partialRedisKeys); 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 b1056f51..77ef7f8e 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 @@ -20,6 +20,8 @@ import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.data.util.redis.RedisIdentifier; +import net.staticstudios.data.util.redis.RedisUtils; import net.staticstudios.utils.Pair; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; @@ -63,7 +65,7 @@ public class H2DataAccessor implements DataAccessor { return t; }); private final RedisListener redisListener; - private final Map redisCache = new ConcurrentHashMap<>(); + private final Map redisCache = new ConcurrentHashMap<>(); private final Set knownRedisPartialKeys = ConcurrentHashMap.newKeySet(); private final ThreadLocal> commitCallbacks = ThreadLocal.withInitial(LinkedList::new); @@ -325,7 +327,11 @@ public synchronized void sync(List schemaTables, List redis cursor = scanResult.getCursor(); for (String key : scanResult.getResult()) { - redisCache.put(key, decodeRedis(jedis.get(key)).value()); + RedisIdentifier identifier = RedisUtils.fromKey(key, dataManager); + if (identifier == null) { + continue; // we aren't tracking this table + } + redisCache.put(identifier, decodeRedis(jedis.get(key)).value()); } } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); @@ -521,20 +527,21 @@ public void postDDL() throws SQLException { } @Override - public @Nullable String getRedisValue(String key) { - return redisCache.get(key); + public @Nullable String getRedisValue(RedisIdentifier identifier) { + return redisCache.get(identifier); } @Override - public void setRedisValue(String key, String value, int expirationSeconds) { + public void setRedisValue(RedisIdentifier identifier, String value, int expirationSeconds) { + String key = RedisUtils.toKey(identifier); String prev; if (value == null) { - prev = redisCache.remove(key); + prev = redisCache.remove(identifier); taskQueue.submitTask((connection, jedis) -> { jedis.del(key); }); } else { - prev = redisCache.put(key, value); + prev = redisCache.put(identifier, value); taskQueue.submitTask((connection, jedis) -> { if (expirationSeconds > 0) { jedis.setex(key, expirationSeconds, encodeRedis(value)); @@ -701,16 +708,20 @@ private void handleRedisEvent(RedisEvent event, String key, @Nullable String val return; // ignore events from ourselves } + RedisIdentifier identifier = RedisUtils.fromKey(key, dataManager); + if (identifier == null) { + return; // we aren't tracking this key + } if (event == RedisEvent.SET) { - String entry = redisCache.get(key); + String entry = redisCache.get(identifier); if (entry != null && Objects.equals(entry, redisValue)) { return; } - redisCache.put(key, redisValue); + redisCache.put(identifier, redisValue); RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), entry, redisValue); } else if (event == RedisEvent.DEL || event == RedisEvent.EXPIRED) { - String entry = redisCache.remove(key); + String entry = redisCache.remove(identifier); if (entry != null) { RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), entry, null); 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/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/RedisUtils.java b/core/src/main/java/net/staticstudios/data/util/redis/RedisUtils.java similarity index 58% rename from core/src/main/java/net/staticstudios/data/util/RedisUtils.java rename to core/src/main/java/net/staticstudios/data/util/redis/RedisUtils.java index 00ffb0dc..bbc538f4 100644 --- a/core/src/main/java/net/staticstudios/data/util/RedisUtils.java +++ b/core/src/main/java/net/staticstudios/data/util/redis/RedisUtils.java @@ -1,4 +1,14 @@ -package net.staticstudios.data.util; +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; @@ -27,11 +37,44 @@ public static Pattern globToRegex(String redisPattern) { return Pattern.compile(regex.toString()); } - public static String buildRedisKey(String holderSchema, String holderTable, String identifier, ColumnValuePairs icColumns) { + 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 : icColumns) { + for (ColumnValuePair pair : idColumns) { sb.append(pair.column()).append(":").append(pair.value()).append(":"); } sb.append(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..44d241d4 100644 --- a/core/src/test/java/net/staticstudios/data/CachedValueTest.java +++ b/core/src/test/java/net/staticstudios/data/CachedValueTest.java @@ -6,7 +6,7 @@ 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; From 32a3589e660b4f7b0b8b22608f91588e08458f1f Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 7 Mar 2026 21:29:19 -0500 Subject: [PATCH 06/22] avoid extra allocation in ColumnValuePairs constructor --- .../java/net/staticstudios/data/util/ColumnValuePairs.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 ed30f177..e9c9cc76 100644 --- a/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java +++ b/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java @@ -2,10 +2,8 @@ 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 { @@ -14,9 +12,8 @@ public final class ColumnValuePairs implements Iterable { 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 ColumnValuePair[] getPairs() { From a592fb0e04845d24c8e4f1d8ec72f73c9f592660 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 7 Mar 2026 22:06:40 -0500 Subject: [PATCH 07/22] avoid additional runtime reflection calls outside of the metadata extraction phase --- .../net/staticstudios/data/CachedValue.java | 20 +++- .../data/PersistentCollection.java | 13 ++- .../staticstudios/data/PersistentValue.java | 13 ++- .../net/staticstudios/data/Reference.java | 13 ++- .../data/impl/data/CachedValueImpl.java | 5 +- .../data/impl/data/PersistentValueImpl.java | 2 +- .../impl/data/ReadOnlyPersistentValue.java | 3 +- .../data/util/CachedValueMetadata.java | 102 +++++++++++++++++- .../util/CollectionChangeHandlerWrapper.java | 4 - .../util/PersistentCollectionMetadata.java | 4 + ...ersistentManyToManyCollectionMetadata.java | 11 ++ ...PersistentOneToManyCollectionMetadata.java | 11 ++ ...stentOneToManyValueCollectionMetadata.java | 11 ++ .../data/util/PersistentValueMetadata.java | 9 ++ .../data/util/ReferenceMetadata.java | 9 ++ .../util/ReferenceUpdateHandlerWrapper.java | 4 - .../data/util/ReflectionUtils.java | 2 +- .../data/util/ValueUpdateHandlerWrapper.java | 4 - 18 files changed, 206 insertions(+), 34 deletions(-) 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/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..29220056 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; @@ -42,6 +39,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/impl/data/CachedValueImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java index 05ba2d9d..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 @@ -50,7 +50,7 @@ public static void delegate(T instance) { if (value instanceof CachedValue.ProxyCachedValue proxyCv) { CachedValueImpl.createAndDelegate(proxyCv, cvMetadata); } else { - field.set(instance, CachedValueImpl.create(instance, ReflectionUtils.getGenericType(field), cvMetadata)); + field.set(instance, CachedValueImpl.create(instance, cvMetadata.type(), cvMetadata)); } } } catch (IllegalAccessException e) { @@ -79,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/PersistentValueImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index 5187a20d..1d54b175 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -45,7 +45,7 @@ public static void delegate(T instance) { if (value instanceof PersistentValue.ProxyPersistentValue proxyPv) { PersistentValueImpl.createAndDelegate(proxyPv, pvMetadata); } else { - field.set(instance, PersistentValueImpl.create(instance, ReflectionUtils.getGenericType(field), pvMetadata)); + field.set(instance, PersistentValueImpl.create(instance, pvMetadata.getColumnMetadata().type(), pvMetadata)); } } } catch (IllegalAccessException 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 20ad1177..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 @@ -3,7 +3,6 @@ import net.staticstudios.data.PersistentValue; import net.staticstudios.data.UniqueData; import net.staticstudios.data.util.PersistentValueMetadata; -import net.staticstudios.data.util.ReflectionUtils; import net.staticstudios.data.util.UniqueDataMetadata; import net.staticstudios.data.util.ValueUpdateHandler; @@ -45,7 +44,7 @@ public static void delegate(U instance) { if (value instanceof PersistentValue.ProxyPersistentValue proxyPv) { ReadOnlyPersistentValue.createAndDelegate(proxyPv, pvMetadata); } else { - field.set(instance, ReadOnlyPersistentValue.create(instance, ReflectionUtils.getGenericType(field), pvMetadata)); + field.set(instance, ReadOnlyPersistentValue.create(instance, pvMetadata.getColumnMetadata().type(), pvMetadata)); } } } catch (IllegalAccessException e) { 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/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/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/ReferenceMetadata.java b/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java index 83642e1d..4a6b8725 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java @@ -16,6 +16,7 @@ public final class ReferenceMetadata { 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) { @@ -88,6 +89,14 @@ 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; 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/ReflectionUtils.java b/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java index 0ff440cb..d58d96a8 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java +++ b/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java @@ -62,7 +62,7 @@ public static List getFieldInstances(Object instance, Class fieldType) } - public static @Nullable Class getGenericType(Field field) { //todo: this is called a lot at runtime, this should not be the case. it is only valid to call this during metadata extraction + public static @Nullable Class getGenericType(Field field) { if (field.getGenericType() instanceof Class) { return (Class) field.getGenericType(); } else if (field.getGenericType() instanceof java.lang.reflect.ParameterizedType parameterizedType) { 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; From 0b2119c93367bd946120532b49064df6aab75eb5 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 01:53:11 -0500 Subject: [PATCH 08/22] queryable cached values --- .../net/staticstudios/data/DataManager.java | 17 +- .../data/impl/h2/H2DataAccessor.java | 47 ++- .../H2ReadCacheInvalidatorTrigger.java | 12 +- .../h2/trigger/H2UpdateHandlerTrigger.java | 13 +- .../data/primative/Primitive.java | 9 +- .../data/primative/PrimitiveBuilder.java | 10 +- .../data/primative/Primitives.java | 15 + .../data/query/BaseQueryBuilder.java | 2 +- .../data/query/BaseQueryWhere.java | 38 +- .../data/query/QueryBuilder.java | 5 + .../data/query/clause/Clause.java | 7 + .../clause/cv/CachedValueEqualsClause.java | 57 +++ .../query/clause/cv/CachedValueInClause.java | 53 +++ .../clause/cv/CachedValueNotEqualsClause.java | 57 +++ .../clause/cv/CachedValueNotInClause.java | 53 +++ .../clause/cv/CachedValueNotNullClause.java | 50 +++ .../clause/cv/CachedValueNullClause.java | 50 +++ .../net/staticstudios/data/QueryTest.java | 62 ++++ .../ide/intellij/DataPsiAugmentProvider.java | 2 +- .../ide/intellij/IntelliJPluginUtils.java | 5 + .../ide/intellij/query/QueryBuilderUtils.java | 22 ++ .../query/clause/cv/CachedValueIsClause.java | 25 ++ .../clause/cv/CachedValueIsInArrayClause.java | 49 +++ .../cv/CachedValueIsInCollectionClause.java | 34 ++ .../clause/cv/CachedValueIsNotClause.java | 25 ++ .../cv/CachedValueIsNotInArrayClause.java | 47 +++ .../CachedValueIsNotInCollectionClause.java | 34 ++ .../clause/cv/CachedValueIsNotNullClause.java | 24 ++ .../clause/cv/CachedValueIsNullClause.java | 25 ++ .../data/compiler/javac/ProcessorContext.java | 2 + .../compiler/javac/StaticDataProcessor.java | 7 +- .../javac/javac/ParsedCachedValue.java | 91 +++++ .../javac/javac/QueryBuilderProcessor.java | 349 ++++++++++++++++++ .../staticstudios/data/utils/Constants.java | 2 + 34 files changed, 1261 insertions(+), 39 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueEqualsClause.java create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueInClause.java create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotEqualsClause.java create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotInClause.java create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotNullClause.java create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNullClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsInArrayClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsInCollectionClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotInArrayClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotInCollectionClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotNullClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNullClause.java create mode 100644 processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedCachedValue.java diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 09401042..fd9cc763 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -44,9 +44,11 @@ @ApiStatus.Internal public class DataManager { + private static final Map DATA_MANAGER_INSTANCES = new HashMap<>(); 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; @@ -92,7 +94,10 @@ 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); @@ -117,10 +122,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; } 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 77ef7f8e..8faef000 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 @@ -25,6 +25,7 @@ import net.staticstudios.utils.Pair; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; +import org.h2.value.Value; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -87,6 +88,12 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener this.jdbcUrl = "jdbc:h2:mem:static-data-cache;DB_CLOSE_DELAY=-1;LOCK_MODE=3;CACHE_SIZE=65536;QUERY_CACHE_SIZE=1024;CACHE_TYPE=SOFT_LRU"; this.dataManager = dataManager; + try (Statement statement = getConnection().createStatement()) { + statement.execute("CREATE ALIAS CACHED_VALUE FOR \"" + H2DataAccessor.class.getName() + ".cachedValueEquals\""); + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize H2 database", e); + } + postgresListener.addHandler(notification -> { try { SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(notification.getSchema()); @@ -259,6 +266,33 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener }); } + // called via reflection from h2 + @SuppressWarnings("unused") + public static String cachedValueEquals(UUID dataManagerId, String schema, String table, String identifier, Value... ids) { + DataManager dataManager = DataManager.getInstance(dataManagerId); + + SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(schema); + if (sqlSchema == null) { + return null; + } + SQLTable sqlTable = sqlSchema.getTable(table); + if (sqlTable == null) { + return null; + } + + ColumnValuePair[] columnValuePairs = new ColumnValuePair[sqlTable.getIdColumns().size()]; + + for (int i = 0; i < sqlTable.getIdColumns().size(); i++) { + ColumnMetadata columnMetadata = sqlTable.getIdColumns().get(i); + String columnName = columnMetadata.name(); + columnValuePairs[i] = new ColumnValuePair(columnName, Primitives.fromValue(columnMetadata.type(), ids[i])); + } + + RedisIdentifier redisIdentifier = new RedisIdentifier(schema, table, identifier, new ColumnValuePairs(columnValuePairs)); + + return dataManager.getDataAccessor().getRedisValue(redisIdentifier); + } + public synchronized void sync(List schemaTables, List redisPartialKeys) throws SQLException { taskQueue.submitTask((realDbConnection, jedis) -> { if (!schemaTables.isEmpty()) { @@ -587,40 +621,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_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_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); - H2UpdateHandlerTrigger.registerDataManager(randomId, dataManager); 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, randomId.toString().replace('-', '_'), schema, table, H2ReadCacheInvalidatorTrigger.class.getName()); + String formatted = sql.formatted(table, dataManager.getApplicationId().toString().replace('-', '_'), schema, table, H2ReadCacheInvalidatorTrigger.class.getName()); logger.trace("[H2] {}", formatted); - H2ReadCacheInvalidatorTrigger.registerDataManager(randomId, dataManager); 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, randomId.toString().replace('-', '_'), schema, table, H2ReadCacheInvalidatorTrigger.class.getName()); + String formatted = sql.formatted(table, dataManager.getApplicationId().toString().replace('-', '_'), schema, table, H2ReadCacheInvalidatorTrigger.class.getName()); logger.trace("[H2] {}", formatted); - H2ReadCacheInvalidatorTrigger.registerDataManager(randomId, dataManager); createTrigger.execute(formatted); } 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 index 803d8c43..fcacc71d 100644 --- 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 @@ -10,11 +10,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 H2ReadCacheInvalidatorTrigger implements Trigger { - private static final Map dataManagerMap = new ConcurrentHashMap<>(); private final Logger logger = LoggerFactory.getLogger(H2ReadCacheInvalidatorTrigger.class); private final List columnNames = new ArrayList<>(); private DataManager dataManager; @@ -22,15 +23,12 @@ public class H2ReadCacheInvalidatorTrigger 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/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/primative/Primitive.java b/core/src/main/java/net/staticstudios/data/primative/Primitive.java index 22f426f4..ea4f901a 100644 --- a/core/src/main/java/net/staticstudios/data/primative/Primitive.java +++ b/core/src/main/java/net/staticstudios/data/primative/Primitive.java @@ -1,5 +1,6 @@ package net.staticstudios.data.primative; +import org.h2.value.Value; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -10,15 +11,17 @@ public class Primitive { private final Function<@NotNull String, @NotNull T> decoder; private final Function<@NotNull T, @NotNull String> encoder; private final Function<@NotNull T, @NotNull T> copier; + private final Function<@NotNull Value, @NotNull T> valueExtractor; private final String h2SQLType; private final String pgSQLType; - public Primitive(Class runtimeType, Function<@NotNull String, @NotNull T> decoder, Function<@NotNull T, @NotNull String> encoder, Function<@NotNull T, @NotNull T> copier, + public Primitive(Class runtimeType, Function<@NotNull String, @NotNull T> decoder, Function<@NotNull T, @NotNull String> encoder, Function<@NotNull T, @NotNull T> copier, Function<@NotNull Value, @NotNull T> valueExtractor, String h2SQLType, String pgSQLType) { this.runtimeType = runtimeType; this.decoder = decoder; this.encoder = encoder; this.copier = copier; + this.valueExtractor = valueExtractor; this.h2SQLType = h2SQLType; this.pgSQLType = pgSQLType; } @@ -48,6 +51,10 @@ public static PrimitiveBuilder builder(Class runtimeType) { return copier.apply(value); } + public @NotNull T fromvalue(@NotNull Value value) { + return valueExtractor.apply(value); + } + public String getH2SQLType() { return h2SQLType; } diff --git a/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java b/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java index 9effd493..55727977 100644 --- a/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java +++ b/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java @@ -1,6 +1,7 @@ package net.staticstudios.data.primative; import com.google.common.base.Preconditions; +import org.h2.value.Value; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; @@ -11,6 +12,7 @@ public class PrimitiveBuilder { private Function decoder; private Function encoder; private Function copier; + private Function valueExtractor; private String h2SQLType; private String pgSQLType; @@ -39,6 +41,11 @@ public PrimitiveBuilder copier(Function<@NotNull T, @NotNull T> copier) { return this; } + public PrimitiveBuilder valueExtractor(Function<@NotNull Value, @NotNull T> valueExtractor) { + this.valueExtractor = valueExtractor; + return this; + } + public PrimitiveBuilder h2SQLType(String h2SQLType) { this.h2SQLType = h2SQLType; return this; @@ -54,11 +61,12 @@ public Primitive build(Consumer> consumer) { Preconditions.checkNotNull(decoder, "Decoder is null"); Preconditions.checkNotNull(encoder, "Encoder is null"); Preconditions.checkNotNull(copier, "Copier is null"); + Preconditions.checkNotNull(valueExtractor, "Value extractor is null"); Preconditions.checkNotNull(consumer, "Consumer is null"); Preconditions.checkNotNull(h2SQLType, "H2 SQL Type is null"); Preconditions.checkNotNull(pgSQLType, "Postgres SQL Type is null"); - Primitive primitive = new Primitive<>(runtimeType, decoder, encoder, copier, h2SQLType, pgSQLType); + Primitive primitive = new Primitive<>(runtimeType, decoder, encoder, copier, valueExtractor, h2SQLType, pgSQLType); consumer.accept(primitive); return primitive; diff --git a/core/src/main/java/net/staticstudios/data/primative/Primitives.java b/core/src/main/java/net/staticstudios/data/primative/Primitives.java index 17e8da6f..00df22c1 100644 --- a/core/src/main/java/net/staticstudios/data/primative/Primitives.java +++ b/core/src/main/java/net/staticstudios/data/primative/Primitives.java @@ -1,6 +1,7 @@ package net.staticstudios.data.primative; import com.google.common.base.Preconditions; +import org.h2.value.Value; import org.jetbrains.annotations.Nullable; import java.sql.Timestamp; @@ -26,12 +27,14 @@ public class Primitives { .encoder(s -> s) .decoder(s -> s) .copier(s -> s) + .valueExtractor(Value::getString) .build(Primitives::register); public static final Primitive INTEGER = Primitive.builder(Integer.class) .h2SQLType("INTEGER") .pgSQLType("INTEGER") .encoder(i -> Integer.toString(i)) .copier(i -> i) + .valueExtractor(Value::getInt) .decoder(Integer::parseInt) .build(Primitives::register); public static final Primitive LONG = Primitive.builder(Long.class) @@ -40,6 +43,7 @@ public class Primitives { .encoder(l -> Long.toString(l)) .decoder(Long::parseLong) .copier(l -> l) + .valueExtractor(Value::getLong) .build(Primitives::register); public static final Primitive FLOAT = Primitive.builder(Float.class) .h2SQLType("REAL") @@ -47,6 +51,7 @@ public class Primitives { .encoder(f -> Float.toString(f)) .decoder(Float::parseFloat) .copier(f -> f) + .valueExtractor(Value::getFloat) .build(Primitives::register); public static final Primitive DOUBLE = Primitive.builder(Double.class) .h2SQLType("DOUBLE PRECISION") @@ -54,6 +59,7 @@ public class Primitives { .encoder(d -> Double.toString(d)) .decoder(Double::parseDouble) .copier(d -> d) + .valueExtractor(Value::getDouble) .build(Primitives::register); public static final Primitive BOOLEAN = Primitive.builder(Boolean.class) .h2SQLType("BOOLEAN") @@ -61,6 +67,7 @@ public class Primitives { .encoder(b -> Boolean.toString(b)) .decoder(Boolean::parseBoolean) .copier(b -> b) + .valueExtractor(Value::getBoolean) .build(Primitives::register); public static final Primitive UUID = Primitive.builder(java.util.UUID.class) .h2SQLType("UUID") @@ -68,6 +75,7 @@ public class Primitives { .encoder(java.util.UUID::toString) .decoder(java.util.UUID::fromString) .copier(uuid -> uuid) + .valueExtractor(value -> java.util.UUID.fromString(value.getString())) .build(Primitives::register); public static final Primitive TIMESTAMP = Primitive.builder(Timestamp.class) .h2SQLType("TIMESTAMP WITH TIME ZONE") @@ -75,6 +83,7 @@ public class Primitives { .encoder(timestamp -> TIMESTAMP_FORMATTER.format(timestamp.toInstant())) .decoder(s -> Timestamp.from(OffsetDateTime.parse(s, TIMESTAMP_FORMATTER).toInstant())) .copier(timestamp -> new Timestamp(timestamp.getTime())) + .valueExtractor(value -> Timestamp.from(OffsetDateTime.parse(value.getString(), TIMESTAMP_FORMATTER).toInstant())) .build(Primitives::register); // dropping support for byte[] for the time being, im running into weird issues on the h2 side. also the javac stuff is having issues parsing ... types. @@ -116,6 +125,12 @@ public static String encode(@Nullable Object value) { return encode(value, value.getClass()); } + public static Object fromValue(Class type, Value value) { + Primitive primitive = getPrimitive(type); + Preconditions.checkNotNull(primitive, "No primitive found for type: " + type.getName()); + return primitive.fromvalue(value); + } + public static T copy(T value, Class type) { return getPrimitive(type).copy(value); } 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 6b8658ad..15adfacd 100644 --- a/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java @@ -81,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 b67d9f63..4a616984 100644 --- a/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java @@ -1,7 +1,9 @@ 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; @@ -17,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; } @@ -26,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(")"); } @@ -153,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) { @@ -177,8 +203,8 @@ 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) { 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..96ab26a6 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,11 @@ public final QueryWhere lessThanOrEqualTo(String schema, String table, String co return this; } + public final QueryWhere cachedValueEquals(String schema, String table, String identifier, String value) { + super.cachedValueEqualsClause(schema, table, identifier, value); + 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/cv/CachedValueEqualsClause.java b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueEqualsClause.java new file mode 100644 index 00000000..b29c92cf --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueEqualsClause.java @@ -0,0 +1,57 @@ +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.ColumnMetadata; +import net.staticstudios.data.util.UniqueDataMetadata; + +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("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); + + for (int i = 0; i < holderMetadata.idColumns().size(); i++) { + ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); + sb.append(", \"").append(columnMetadata.name()).append("\""); + } + + sb.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..f2a32689 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueInClause.java @@ -0,0 +1,53 @@ +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.ColumnMetadata; +import net.staticstudios.data.util.UniqueDataMetadata; + +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("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); + + for (int i = 0; i < holderMetadata.idColumns().size(); i++) { + ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); + sb.append(", \"").append(columnMetadata.name()).append("\""); + } + + sb.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..fa012730 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotEqualsClause.java @@ -0,0 +1,57 @@ +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.ColumnMetadata; +import net.staticstudios.data.util.UniqueDataMetadata; + +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("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); + + for (int i = 0; i < holderMetadata.idColumns().size(); i++) { + ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); + sb.append(", \"").append(columnMetadata.name()).append("\""); + } + + sb.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..6c28fdf7 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotInClause.java @@ -0,0 +1,53 @@ +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.ColumnMetadata; +import net.staticstudios.data.util.UniqueDataMetadata; + +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("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); + + for (int i = 0; i < holderMetadata.idColumns().size(); i++) { + ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); + sb.append(", \"").append(columnMetadata.name()).append("\""); + } + + sb.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..ca97ba2d --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNotNullClause.java @@ -0,0 +1,50 @@ +package net.staticstudios.data.query.clause.cv; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.query.clause.ValueClause; +import net.staticstudios.data.util.ColumnMetadata; +import net.staticstudios.data.util.UniqueDataMetadata; + +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("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); + + for (int i = 0; i < holderMetadata.idColumns().size(); i++) { + ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); + sb.append(", \"").append(columnMetadata.name()).append("\""); + } + + sb.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..a8a285bc --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/cv/CachedValueNullClause.java @@ -0,0 +1,50 @@ +package net.staticstudios.data.query.clause.cv; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.query.clause.ValueClause; +import net.staticstudios.data.util.ColumnMetadata; +import net.staticstudios.data.util.UniqueDataMetadata; + +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("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); + + for (int i = 0; i < holderMetadata.idColumns().size(); i++) { + ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); + sb.append(", \"").append(columnMetadata.name()).append("\""); + } + + sb.append(") IS NULL"); + return List.of(); + } +} diff --git a/core/src/test/java/net/staticstudios/data/QueryTest.java b/core/src/test/java/net/staticstudios/data/QueryTest.java index d9db18cf..12e2703e 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,12 @@ 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 CACHED_VALUE(UUID '%s', 'public', 'users', 'settings_updates', \"id\") = ?".formatted(dataManager.getApplicationId()), MockUser.query(dataManager).where(w -> w.settingsUpdatesIs(1)).toString()); + } //todo: test other cached value clauses as well (not in, in, is null, is not null) } \ 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..4a87987c 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 @@ -319,7 +319,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)) { 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..50753316 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 @@ -96,4 +96,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/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..f33391c7 --- /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 + "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/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/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..c04fcf9c --- /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 "PersistentValue{" + + "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..2bcb0b2b 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("inClause") + ), + 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); + } + + public 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("notInClause") + ), + 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); + } + + public 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); + } + + public 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"; From 8f624e2b37af8db6cc7f1d62237bb91a204bf2dd Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 04:57:30 -0400 Subject: [PATCH 09/22] store cachedvalues in the db --- .../net/staticstudios/data/Identifier.java | 2 + .../net/staticstudios/data/DataManager.java | 5 +- .../staticstudios/data/impl/DataAccessor.java | 6 +- .../data/impl/h2/H2DataAccessor.java | 211 +++++++++++++----- .../staticstudios/data/parse/SQLBuilder.java | 71 +++++- .../staticstudios/data/parse/SQLColumn.java | 8 +- .../data/primative/Primitive.java | 9 +- .../data/primative/PrimitiveBuilder.java | 10 +- .../data/primative/Primitives.java | 15 -- .../clause/cv/CachedValueEqualsClause.java | 11 +- .../query/clause/cv/CachedValueInClause.java | 12 +- .../clause/cv/CachedValueNotEqualsClause.java | 11 +- .../clause/cv/CachedValueNotInClause.java | 12 +- .../clause/cv/CachedValueNotNullClause.java | 11 +- .../clause/cv/CachedValueNullClause.java | 11 +- .../data/util/redis/RedisUtils.java | 39 ++++ .../staticstudios/data/CachedValueTest.java | 30 ++- .../net/staticstudios/data/QueryTest.java | 2 +- 18 files changed, 308 insertions(+), 168 deletions(-) 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/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index fd9cc763..8978f24f 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -20,7 +20,6 @@ import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; -import net.staticstudios.data.util.redis.RedisIdentifier; import net.staticstudios.data.util.redis.RedisUtils; import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; @@ -1418,7 +1417,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC public @Nullable T getRedis(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns, Class type) { - String encoded = dataAccessor.getRedisValue(new RedisIdentifier(holderSchema, holderTable, identifier, idColumns)); + String encoded = dataAccessor.getRedisValue(holderSchema, holderTable, identifier, idColumns); if (encoded == null) { return null; } @@ -1429,7 +1428,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC 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(new RedisIdentifier(holderSchema, holderTable, identifier, idColumns), encoded, expireAfterSeconds); + dataAccessor.setRedisValue(holderSchema, holderTable, identifier, idColumns, encoded, expireAfterSeconds); } private boolean hasCycle(SQLTable table, Map> dependencyGraph, Set visited, Set stack) { 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 e4bb0ff5..ce6c2a74 100644 --- a/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java +++ b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java @@ -3,9 +3,9 @@ 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 net.staticstudios.data.util.redis.RedisIdentifier; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; @@ -30,9 +30,9 @@ default void executeUpdate(SQLTransaction.Statement statement, List valu void postDDL() throws SQLException; - @Nullable String getRedisValue(RedisIdentifier identifier); + @Nullable String getRedisValue(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns); - void setRedisValue(RedisIdentifier identifier, String value, int expirationSeconds); + void setRedisValue(String holderSchema, String holderTable, String identifier, ColumnValuePairs idColumns, String value, int expirationSeconds); void discoverRedisKeys(List partialRedisKeys); 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 8faef000..0f376a3a 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 @@ -20,12 +20,10 @@ import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; -import net.staticstudios.data.util.redis.RedisIdentifier; import net.staticstudios.data.util.redis.RedisUtils; import net.staticstudios.utils.Pair; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; -import org.h2.value.Value; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -66,7 +64,6 @@ 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); @@ -88,12 +85,6 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener this.jdbcUrl = "jdbc:h2:mem:static-data-cache;DB_CLOSE_DELAY=-1;LOCK_MODE=3;CACHE_SIZE=65536;QUERY_CACHE_SIZE=1024;CACHE_TYPE=SOFT_LRU"; this.dataManager = dataManager; - try (Statement statement = getConnection().createStatement()) { - statement.execute("CREATE ALIAS CACHED_VALUE FOR \"" + H2DataAccessor.class.getName() + ".cachedValueEquals\""); - } catch (SQLException e) { - throw new RuntimeException("Failed to initialize H2 database", e); - } - postgresListener.addHandler(notification -> { try { SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(notification.getSchema()); @@ -266,33 +257,6 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener }); } - // called via reflection from h2 - @SuppressWarnings("unused") - public static String cachedValueEquals(UUID dataManagerId, String schema, String table, String identifier, Value... ids) { - DataManager dataManager = DataManager.getInstance(dataManagerId); - - SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(schema); - if (sqlSchema == null) { - return null; - } - SQLTable sqlTable = sqlSchema.getTable(table); - if (sqlTable == null) { - return null; - } - - ColumnValuePair[] columnValuePairs = new ColumnValuePair[sqlTable.getIdColumns().size()]; - - for (int i = 0; i < sqlTable.getIdColumns().size(); i++) { - ColumnMetadata columnMetadata = sqlTable.getIdColumns().get(i); - String columnName = columnMetadata.name(); - columnValuePairs[i] = new ColumnValuePair(columnName, Primitives.fromValue(columnMetadata.type(), ids[i])); - } - - RedisIdentifier redisIdentifier = new RedisIdentifier(schema, table, identifier, new ColumnValuePairs(columnValuePairs)); - - return dataManager.getDataAccessor().getRedisValue(redisIdentifier); - } - public synchronized void sync(List schemaTables, List redisPartialKeys) throws SQLException { taskQueue.submitTask((realDbConnection, jedis) -> { if (!schemaTables.isEmpty()) { @@ -361,11 +325,12 @@ public synchronized void sync(List schemaTables, List redis cursor = scanResult.getCursor(); for (String key : scanResult.getResult()) { - RedisIdentifier identifier = RedisUtils.fromKey(key, dataManager); - if (identifier == null) { + RedisUtils.FullyDeconstructedKey fullyDeconstructedKey = RedisUtils.fullyDeconstruct(key, dataManager); + if (fullyDeconstructedKey == null) { continue; // we aren't tracking this table } - redisCache.put(identifier, decodeRedis(jedis.get(key)).value()); + + setRedisValueCache(fullyDeconstructedKey.holderSchema(), fullyDeconstructedKey.holderTable(), fullyDeconstructedKey.identifier(), fullyDeconstructedKey.idColumns(), decodeRedis(jedis.get(key)).value()); } } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); @@ -561,21 +526,49 @@ public void postDDL() throws SQLException { } @Override - public @Nullable String getRedisValue(RedisIdentifier identifier) { - return redisCache.get(identifier); + 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(RedisIdentifier identifier, String value, int expirationSeconds) { - String key = RedisUtils.toKey(identifier); - 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(identifier); taskQueue.submitTask((connection, jedis) -> { jedis.del(key); }); } else { - prev = redisCache.put(identifier, value); taskQueue.submitTask((connection, jedis) -> { if (expirationSeconds > 0) { jedis.setex(key, expirationSeconds, encodeRedis(value)); @@ -589,6 +582,107 @@ public void setRedisValue(RedisIdentifier identifier, String value, int expirati 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(); + } 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); @@ -670,7 +764,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); } } } @@ -737,23 +837,22 @@ private void handleRedisEvent(RedisEvent event, String key, @Nullable String val return; // ignore events from ourselves } - RedisIdentifier identifier = RedisUtils.fromKey(key, dataManager); - if (identifier == null) { + 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(identifier); - 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(identifier, 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(identifier); - 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); } } } 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); diff --git a/core/src/main/java/net/staticstudios/data/primative/Primitive.java b/core/src/main/java/net/staticstudios/data/primative/Primitive.java index ea4f901a..22f426f4 100644 --- a/core/src/main/java/net/staticstudios/data/primative/Primitive.java +++ b/core/src/main/java/net/staticstudios/data/primative/Primitive.java @@ -1,6 +1,5 @@ package net.staticstudios.data.primative; -import org.h2.value.Value; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -11,17 +10,15 @@ public class Primitive { private final Function<@NotNull String, @NotNull T> decoder; private final Function<@NotNull T, @NotNull String> encoder; private final Function<@NotNull T, @NotNull T> copier; - private final Function<@NotNull Value, @NotNull T> valueExtractor; private final String h2SQLType; private final String pgSQLType; - public Primitive(Class runtimeType, Function<@NotNull String, @NotNull T> decoder, Function<@NotNull T, @NotNull String> encoder, Function<@NotNull T, @NotNull T> copier, Function<@NotNull Value, @NotNull T> valueExtractor, + public Primitive(Class runtimeType, Function<@NotNull String, @NotNull T> decoder, Function<@NotNull T, @NotNull String> encoder, Function<@NotNull T, @NotNull T> copier, String h2SQLType, String pgSQLType) { this.runtimeType = runtimeType; this.decoder = decoder; this.encoder = encoder; this.copier = copier; - this.valueExtractor = valueExtractor; this.h2SQLType = h2SQLType; this.pgSQLType = pgSQLType; } @@ -51,10 +48,6 @@ public static PrimitiveBuilder builder(Class runtimeType) { return copier.apply(value); } - public @NotNull T fromvalue(@NotNull Value value) { - return valueExtractor.apply(value); - } - public String getH2SQLType() { return h2SQLType; } diff --git a/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java b/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java index 55727977..9effd493 100644 --- a/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java +++ b/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java @@ -1,7 +1,6 @@ package net.staticstudios.data.primative; import com.google.common.base.Preconditions; -import org.h2.value.Value; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; @@ -12,7 +11,6 @@ public class PrimitiveBuilder { private Function decoder; private Function encoder; private Function copier; - private Function valueExtractor; private String h2SQLType; private String pgSQLType; @@ -41,11 +39,6 @@ public PrimitiveBuilder copier(Function<@NotNull T, @NotNull T> copier) { return this; } - public PrimitiveBuilder valueExtractor(Function<@NotNull Value, @NotNull T> valueExtractor) { - this.valueExtractor = valueExtractor; - return this; - } - public PrimitiveBuilder h2SQLType(String h2SQLType) { this.h2SQLType = h2SQLType; return this; @@ -61,12 +54,11 @@ public Primitive build(Consumer> consumer) { Preconditions.checkNotNull(decoder, "Decoder is null"); Preconditions.checkNotNull(encoder, "Encoder is null"); Preconditions.checkNotNull(copier, "Copier is null"); - Preconditions.checkNotNull(valueExtractor, "Value extractor is null"); Preconditions.checkNotNull(consumer, "Consumer is null"); Preconditions.checkNotNull(h2SQLType, "H2 SQL Type is null"); Preconditions.checkNotNull(pgSQLType, "Postgres SQL Type is null"); - Primitive primitive = new Primitive<>(runtimeType, decoder, encoder, copier, valueExtractor, h2SQLType, pgSQLType); + Primitive primitive = new Primitive<>(runtimeType, decoder, encoder, copier, h2SQLType, pgSQLType); consumer.accept(primitive); return primitive; diff --git a/core/src/main/java/net/staticstudios/data/primative/Primitives.java b/core/src/main/java/net/staticstudios/data/primative/Primitives.java index 00df22c1..17e8da6f 100644 --- a/core/src/main/java/net/staticstudios/data/primative/Primitives.java +++ b/core/src/main/java/net/staticstudios/data/primative/Primitives.java @@ -1,7 +1,6 @@ package net.staticstudios.data.primative; import com.google.common.base.Preconditions; -import org.h2.value.Value; import org.jetbrains.annotations.Nullable; import java.sql.Timestamp; @@ -27,14 +26,12 @@ public class Primitives { .encoder(s -> s) .decoder(s -> s) .copier(s -> s) - .valueExtractor(Value::getString) .build(Primitives::register); public static final Primitive INTEGER = Primitive.builder(Integer.class) .h2SQLType("INTEGER") .pgSQLType("INTEGER") .encoder(i -> Integer.toString(i)) .copier(i -> i) - .valueExtractor(Value::getInt) .decoder(Integer::parseInt) .build(Primitives::register); public static final Primitive LONG = Primitive.builder(Long.class) @@ -43,7 +40,6 @@ public class Primitives { .encoder(l -> Long.toString(l)) .decoder(Long::parseLong) .copier(l -> l) - .valueExtractor(Value::getLong) .build(Primitives::register); public static final Primitive FLOAT = Primitive.builder(Float.class) .h2SQLType("REAL") @@ -51,7 +47,6 @@ public class Primitives { .encoder(f -> Float.toString(f)) .decoder(Float::parseFloat) .copier(f -> f) - .valueExtractor(Value::getFloat) .build(Primitives::register); public static final Primitive DOUBLE = Primitive.builder(Double.class) .h2SQLType("DOUBLE PRECISION") @@ -59,7 +54,6 @@ public class Primitives { .encoder(d -> Double.toString(d)) .decoder(Double::parseDouble) .copier(d -> d) - .valueExtractor(Value::getDouble) .build(Primitives::register); public static final Primitive BOOLEAN = Primitive.builder(Boolean.class) .h2SQLType("BOOLEAN") @@ -67,7 +61,6 @@ public class Primitives { .encoder(b -> Boolean.toString(b)) .decoder(Boolean::parseBoolean) .copier(b -> b) - .valueExtractor(Value::getBoolean) .build(Primitives::register); public static final Primitive UUID = Primitive.builder(java.util.UUID.class) .h2SQLType("UUID") @@ -75,7 +68,6 @@ public class Primitives { .encoder(java.util.UUID::toString) .decoder(java.util.UUID::fromString) .copier(uuid -> uuid) - .valueExtractor(value -> java.util.UUID.fromString(value.getString())) .build(Primitives::register); public static final Primitive TIMESTAMP = Primitive.builder(Timestamp.class) .h2SQLType("TIMESTAMP WITH TIME ZONE") @@ -83,7 +75,6 @@ public class Primitives { .encoder(timestamp -> TIMESTAMP_FORMATTER.format(timestamp.toInstant())) .decoder(s -> Timestamp.from(OffsetDateTime.parse(s, TIMESTAMP_FORMATTER).toInstant())) .copier(timestamp -> new Timestamp(timestamp.getTime())) - .valueExtractor(value -> Timestamp.from(OffsetDateTime.parse(value.getString(), TIMESTAMP_FORMATTER).toInstant())) .build(Primitives::register); // dropping support for byte[] for the time being, im running into weird issues on the h2 side. also the javac stuff is having issues parsing ... types. @@ -125,12 +116,6 @@ public static String encode(@Nullable Object value) { return encode(value, value.getClass()); } - public static Object fromValue(Class type, Value value) { - Primitive primitive = getPrimitive(type); - Preconditions.checkNotNull(primitive, "No primitive found for type: " + type.getName()); - return primitive.fromvalue(value); - } - public static T copy(T value, Class type) { return getPrimitive(type).copy(value); } 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 index b29c92cf..874c0a1d 100644 --- 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 @@ -3,8 +3,8 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.query.clause.ValueClause; -import net.staticstudios.data.util.ColumnMetadata; import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; import java.util.List; @@ -44,14 +44,7 @@ public List append(StringBuilder sb) { @Override public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { - sb.append("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); - - for (int i = 0; i < holderMetadata.idColumns().size(); i++) { - ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); - sb.append(", \"").append(columnMetadata.name()).append("\""); - } - - sb.append(") = ?"); + 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 index f2a32689..2fae1e6e 100644 --- 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 @@ -3,8 +3,8 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.query.clause.ValueClause; -import net.staticstudios.data.util.ColumnMetadata; import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; import java.util.ArrayList; import java.util.List; @@ -29,14 +29,7 @@ public List append(StringBuilder sb) { @Override public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { - sb.append("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); - - for (int i = 0; i < holderMetadata.idColumns().size(); i++) { - ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); - sb.append(", \"").append(columnMetadata.name()).append("\""); - } - - sb.append(") IN ("); + 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) { @@ -44,6 +37,7 @@ public List append(StringBuilder sb, DataManager dataManager, UniqueData } } sb.append(")"); + List encoded = new ArrayList<>(); for (Object value : values) { encoded.add(Primitives.encode(dataManager.serialize(value))); 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 index fa012730..b0eed600 100644 --- 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 @@ -3,8 +3,8 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.query.clause.ValueClause; -import net.staticstudios.data.util.ColumnMetadata; import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; import java.util.List; @@ -44,14 +44,7 @@ public List append(StringBuilder sb) { @Override public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { - sb.append("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); - - for (int i = 0; i < holderMetadata.idColumns().size(); i++) { - ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); - sb.append(", \"").append(columnMetadata.name()).append("\""); - } - - sb.append(") <> ?"); + 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 index 6c28fdf7..0d5294e9 100644 --- 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 @@ -3,8 +3,8 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.query.clause.ValueClause; -import net.staticstudios.data.util.ColumnMetadata; import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; import java.util.ArrayList; import java.util.List; @@ -29,14 +29,7 @@ public List append(StringBuilder sb) { @Override public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { - sb.append("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); - - for (int i = 0; i < holderMetadata.idColumns().size(); i++) { - ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); - sb.append(", \"").append(columnMetadata.name()).append("\""); - } - - sb.append(") NOT IN ("); + 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) { @@ -44,6 +37,7 @@ public List append(StringBuilder sb, DataManager dataManager, UniqueData } } sb.append(")"); + List encoded = new ArrayList<>(); for (Object value : values) { encoded.add(Primitives.encode(dataManager.serialize(value))); 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 index ca97ba2d..380cf40c 100644 --- 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 @@ -2,8 +2,8 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.query.clause.ValueClause; -import net.staticstudios.data.util.ColumnMetadata; import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; import java.util.List; @@ -37,14 +37,7 @@ public List append(StringBuilder sb) { @Override public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { - sb.append("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); - - for (int i = 0; i < holderMetadata.idColumns().size(); i++) { - ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); - sb.append(", \"").append(columnMetadata.name()).append("\""); - } - - sb.append(") IS NOT NULL"); + 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 index a8a285bc..4b384fc9 100644 --- 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 @@ -2,8 +2,8 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.query.clause.ValueClause; -import net.staticstudios.data.util.ColumnMetadata; import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.util.redis.RedisUtils; import java.util.List; @@ -37,14 +37,7 @@ public List append(StringBuilder sb) { @Override public List append(StringBuilder sb, DataManager dataManager, UniqueDataMetadata holderMetadata) { - sb.append("CACHED_VALUE(UUID ").append("'").append(dataManager.getApplicationId()).append("', '").append(schema).append("', '").append(table).append("', '").append(identifier).append("'"); - - for (int i = 0; i < holderMetadata.idColumns().size(); i++) { - ColumnMetadata columnMetadata = holderMetadata.idColumns().get(i); - sb.append(", \"").append(columnMetadata.name()).append("\""); - } - - sb.append(") IS NULL"); + 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/redis/RedisUtils.java b/core/src/main/java/net/staticstudios/data/util/redis/RedisUtils.java index bbc538f4..6dc4b6d3 100644 --- a/core/src/main/java/net/staticstudios/data/util/redis/RedisUtils.java +++ b/core/src/main/java/net/staticstudios/data/util/redis/RedisUtils.java @@ -106,7 +106,46 @@ public static DeconstructedKey deconstruct(String key) { 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 44d241d4..c0729786 100644 --- a/core/src/test/java/net/staticstudios/data/CachedValueTest.java +++ b/core/src/test/java/net/staticstudios/data/CachedValueTest.java @@ -3,6 +3,7 @@ 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; @@ -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 12e2703e..ee74ccb0 100644 --- a/core/src/test/java/net/staticstudios/data/QueryTest.java +++ b/core/src/test/java/net/staticstudios/data/QueryTest.java @@ -294,6 +294,6 @@ public void testCachedValueEqualsClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); dataManager.finishLoading(); - assertEquals("WHERE CACHED_VALUE(UUID '%s', 'public', 'users', 'settings_updates', \"id\") = ?".formatted(dataManager.getApplicationId()), MockUser.query(dataManager).where(w -> w.settingsUpdatesIs(1)).toString()); + assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" = ?", MockUser.query(dataManager).where(w -> w.settingsUpdatesIs(1)).toString()); } //todo: test other cached value clauses as well (not in, in, is null, is not null) } \ No newline at end of file From ed1be01381b463170eea37cc9008c5be165659d5 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 09:53:04 -0400 Subject: [PATCH 10/22] more caching --- .../net/staticstudios/data/DataManager.java | 155 ++++++++++++++---- .../net/staticstudios/data/StaticData.java | 6 +- .../data/StaticDataStatistics.java | 46 +++++- .../staticstudios/data/impl/DataAccessor.java | 3 +- .../PersistentManyToManyCollectionImpl.java | 86 +++++++++- .../PersistentOneToManyCollectionImpl.java | 42 ++++- .../data/impl/data/ReferenceImpl.java | 7 +- .../data/impl/h2/H2DataAccessor.java | 5 +- .../H2ReadCacheInvalidatorTrigger.java | 10 +- .../data/util/ColumnValuePairs.java | 9 + .../data/util/ReadCacheResult.java | 21 +-- 11 files changed, 316 insertions(+), 74 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 8978f24f..443dae5c 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -64,8 +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 readCache; - private final Map> dependencyToCacheMapping = new ConcurrentHashMap<>(); + 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; @@ -104,13 +106,15 @@ public DataManager(StaticDataConfig config, boolean setGlobal) { dataAccessor = new H2DataAccessor(this, postgresListener, redisListener, taskQueue); //todo: params for cache should be configurable - this.readCache = Caffeine.newBuilder() - .maximumSize(50_000) + this.relationCache = Caffeine.newBuilder() + .maximumSize(10_000) .expireAfterWrite(5, TimeUnit.MINUTES) - .removalListener((SelectQuery selectQuery, ReadCacheResult result, RemovalCause cause) -> { - cleanupReadCacheEntry(selectQuery, result); - - }) + .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 @@ -1062,7 +1066,7 @@ public T getInstance(Class clazz, @NotNull ColumnValue boolean exists; - StringBuilder sqlBuilder = new StringBuilder(); //todo: this query should be cached in the unique data metadata. no need to constantly allocate strings + StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT 1 FROM \"").append(schema).append("\".\"").append(table).append("\" WHERE "); for (ColumnValuePair columnValuePair : idColumns) { sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); @@ -1076,7 +1080,7 @@ public T getInstance(Class clazz, @NotNull ColumnValue } SelectQuery selectQuery = new SelectQuery(sql, values); - ReadCacheResult cacheResult = getReadCacheResult(selectQuery); + ReadCacheResult cacheResult = getRelationCacheResult(selectQuery); if (cacheResult != null) { exists = true; } else { @@ -1089,7 +1093,7 @@ public T getInstance(Class clazz, @NotNull ColumnValue dependencies.add(new Cell(schema, table, columnValuePair.column(), idColumns)); } ReadCacheResult result = new ReadCacheResult(ColumnValuePairs.EMPTY, dependencies); - putReadCacheResult(selectQuery, result); + putRelationCacheResult(selectQuery, result); } } catch (SQLException e) { @@ -1279,7 +1283,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(); @@ -1293,13 +1296,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); } @@ -1586,24 +1619,30 @@ public void flushTaskQueue() { }).join(); } - public Optional getStatistics() { - return dataAccessor.getStatistics(); + 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 getReadCacheResult(SelectQuery query) { - return readCache.getIfPresent(query); + public @Nullable ReadCacheResult getRelationCacheResult(SelectQuery query) { + return relationCache.getIfPresent(query); } - public void putReadCacheResult(SelectQuery query, @NotNull ReadCacheResult result) { - logger.trace("Putting result in read cache for query {} with result {}", query, result); - readCache.put(query, result); + 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()) { - dependencyToCacheMapping.computeIfAbsent(cell, k -> new HashSet<>()) + dependencyToRelationCacheMapping.computeIfAbsent(cell, k -> ConcurrentHashMap.newKeySet()) .add(query); } } - public void invalidateReadCache(List columnNames, String schema, String table, List changedColumns, Object[] oldValues) { + 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()]; @@ -1622,17 +1661,67 @@ public void invalidateReadCache(List columnNames, String schema, String ColumnValuePairs idCols = new ColumnValuePairs(idColumns); for (String changedColumn : changedColumns) { Cell cell = new Cell(schema, table, changedColumn, idCols); - Set queries = dependencyToCacheMapping.remove(cell); + Set queries = dependencyToRelationCacheMapping.remove(cell); if (queries != null) { for (SelectQuery query : queries) { - ReadCacheResult res = readCache.getIfPresent(query); - readCache.invalidate(query); + relationCache.invalidate(query); + logger.trace("Invalidated relation cache for query {} due to change in cell {}", query, cell); + } + } + } + } + } + } - if (res != null) { - cleanupReadCacheEntry(query, res); - } + 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)); + } - logger.trace("Invalidated read cache for query {} due to change in cell {}", query, cell); + 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); } } } @@ -1640,13 +1729,13 @@ public void invalidateReadCache(List columnNames, String schema, String } } - private void cleanupReadCacheEntry(@NotNull SelectQuery query, @NotNull ReadCacheResult res) { + private void cleanupCellCacheEntry(@NotNull SelectQuery query, @NotNull ReadCacheResult res) { for (Cell dependency : res.getDependencies()) { - Set dependentQueries = dependencyToCacheMapping.get(dependency); + Set dependentQueries = dependencyToCellCacheMapping.get(dependency); if (dependentQueries != null) { dependentQueries.remove(query); if (dependentQueries.isEmpty()) { - dependencyToCacheMapping.remove(dependency); + dependencyToCellCacheMapping.remove(dependency); } } } diff --git a/core/src/main/java/net/staticstudios/data/StaticData.java b/core/src/main/java/net/staticstudios/data/StaticData.java index 43e04de3..6755f0b9 100644 --- a/core/src/main/java/net/staticstudios/data/StaticData.java +++ b/core/src/main/java/net/staticstudios/data/StaticData.java @@ -5,8 +5,6 @@ import net.staticstudios.data.query.QueryBuilder; import org.jetbrains.annotations.Blocking; -import java.util.Optional; - /** * Entry point for initializing and interacting with the StaticData system. */ @@ -97,9 +95,9 @@ public static void flushTaskQueue() { /** * Retrieves the current performance statistics of the StaticData system, including metrics such as queries per second and updates per second. * - * @return an Optional containing the StaticDataStatistics if available, or an empty Optional if statistics cannot be retrieved at this time + * @return a StaticDataStatistics object containing the current performance metrics of the StaticData system */ - public static Optional getStatistics() { + 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 index 23f0a943..723b5690 100644 --- a/core/src/main/java/net/staticstudios/data/StaticDataStatistics.java +++ b/core/src/main/java/net/staticstudios/data/StaticDataStatistics.java @@ -1,14 +1,37 @@ package net.staticstudios.data; public class StaticDataStatistics { - private final long queriesPerSecond; - private final long updatesPerSecond; + 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 StaticDataStatistics(long queriesPerSecond, long updatesPerSecond) { + 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; } @@ -20,4 +43,21 @@ public long getUpdatesPerSecond() { 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 ce6c2a74..df352280 100644 --- a/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java +++ b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java @@ -12,7 +12,6 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; -import java.util.Optional; public interface DataAccessor { @@ -38,5 +37,5 @@ default void executeUpdate(SQLTransaction.Statement statement, List valu void resync(); - Optional getStatistics(); + void populateStatistics(StaticDataStatistics stats); } 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 0d791152..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 @@ -513,7 +513,6 @@ public boolean removeIds(List 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(); @@ -528,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 "); @@ -555,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 1450bd4e..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 @@ -405,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(); @@ -429,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()]; @@ -453,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/ReferenceImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java index 35e2ca4c..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 @@ -116,14 +116,15 @@ public ColumnValuePairs getReferencedColumnValuePairs() { SelectQuery query = metadata.buildSelectReferencedColumnValuePairsSelectQuery(holder.getDataManager(), values); - ReadCacheResult cached = dataManager.getReadCacheResult(query); + 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 = cached.getValue(idColumn.name()); + Object val = ColumnValuePairs.getValue(idColumn.name(), columnValuePairs); if (val == null) { return null; } @@ -173,7 +174,7 @@ public ColumnValuePairs getReferencedColumnValuePairs() { } ReadCacheResult cacheResult = new ReadCacheResult(theirIdColumns, dependencies); - dataManager.putReadCacheResult(query, cacheResult); + dataManager.putRelationCacheResult(query, cacheResult); return theirIdColumns; } catch (SQLException 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 0f376a3a..161506cc 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 @@ -912,7 +912,8 @@ private RedisEncodedValue decodeRedis(String encoded) { } @Override - public Optional getStatistics() { - return Optional.of(new StaticDataStatistics((long) h2QueryCounter.getPerSecond(), (long) h2UpdateCounter.getPerSecond())); + 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 index fcacc71d..00631f97 100644 --- 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 @@ -76,10 +76,16 @@ private void handleUpdate(Object[] oldRow, Object[] newRow) { } } - dataAccessor.onCommit(() -> dataManager.invalidateReadCache(columnNames, schema, table, changedColumns, oldRow)); + dataAccessor.onCommit(() -> { + dataManager.invalidateRelationCache(columnNames, schema, table, changedColumns, oldRow); + dataManager.invalidateCellCache(columnNames, schema, table, changedColumns, oldRow); + }); } private void handleDelete(Object[] oldRow) { - dataAccessor.onCommit(() -> dataManager.invalidateReadCache(columnNames, schema, table, columnNames, 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/util/ColumnValuePairs.java b/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java index e9c9cc76..68a4f8a4 100644 --- a/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java +++ b/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java @@ -16,6 +16,15 @@ public ColumnValuePairs(ColumnValuePair... pairs) { 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() { return pairs; } diff --git a/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java b/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java index c87e2eb4..4419e19f 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java +++ b/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java @@ -1,25 +1,18 @@ package net.staticstudios.data.util; -import org.jetbrains.annotations.Nullable; - import java.util.Set; public class ReadCacheResult { - private final ColumnValuePairs columnValuePairs; + private final Object value; private final Set dependencies; - public ReadCacheResult(ColumnValuePairs columnValuePairs, Set dependencies) { - this.columnValuePairs = columnValuePairs; + public ReadCacheResult(Object value, Set dependencies) { + this.value = value; this.dependencies = dependencies; } - public @Nullable Object getValue(String column) { - for (ColumnValuePair pair : columnValuePairs) { - if (pair.column().equals(column)) { - return pair.value(); - } - } - return null; + public Object getValue() { + return value; } public Set getDependencies() { @@ -29,7 +22,7 @@ public Set getDependencies() { @Override public String toString() { return "ReadCacheResult[" + - "columnValuePairs=" + columnValuePairs + ", " + - "dependencies=" + dependencies + ']'; + "values=" + value + + ", dependencies=" + dependencies + ']'; } } From 8533a01af75b398426323c0cc4b9a59e96815534 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 10:06:04 -0400 Subject: [PATCH 11/22] address todo --- .../net/staticstudios/data/DataManager.java | 1 - .../net/staticstudios/data/QueryTest.java | 59 ++++++++++++++++++- .../CachedValueIsNotInCollectionClause.java | 2 +- .../javac/javac/QueryBuilderProcessor.java | 4 +- 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 443dae5c..47fcc570 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -105,7 +105,6 @@ public DataManager(StaticDataConfig config, boolean setGlobal) { sqlBuilder = new SQLBuilder(this); dataAccessor = new H2DataAccessor(this, postgresListener, redisListener, taskQueue); - //todo: params for cache should be configurable this.relationCache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(5, TimeUnit.MINUTES) diff --git a/core/src/test/java/net/staticstudios/data/QueryTest.java b/core/src/test/java/net/staticstudios/data/QueryTest.java index ee74ccb0..1ed0eda6 100644 --- a/core/src/test/java/net/staticstudios/data/QueryTest.java +++ b/core/src/test/java/net/staticstudios/data/QueryTest.java @@ -295,5 +295,62 @@ public void testCachedValueEqualsClause() { dataManager.load(MockUser.class); dataManager.finishLoading(); assertEquals("WHERE \"public\".\"users\".\"__virtual__cv_settings_updates\" = ?", MockUser.query(dataManager).where(w -> w.settingsUpdatesIs(1)).toString()); - } //todo: test other cached value clauses as well (not in, in, is null, is not null) + } + + @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/query/clause/cv/CachedValueIsNotInCollectionClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/cv/CachedValueIsNotInCollectionClause.java index f33391c7..8100ec82 100644 --- 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 @@ -17,7 +17,7 @@ public boolean matches(PsiField psiField, boolean nullable) { @Override public String getMethodName(String fieldName) { - return fieldName + "IsIn"; + return fieldName + "IsNotIn"; } @Override 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 2bcb0b2b..4010d3cb 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 @@ -1365,7 +1365,7 @@ private void addCachedValueIsInArrayMethod(ParsedCachedValue cv, String schemaFi List.nil(), Select( Ident(names.fromString("super")), - names.fromString("inClause") + names.fromString("cachedValueInClause") ), List.of( Ident(names.fromString(schemaFieldName)), @@ -1457,7 +1457,7 @@ public void addCachedValueIsNotInArrayMethod(ParsedCachedValue cv, String schema List.nil(), Select( Ident(names.fromString("super")), - names.fromString("notInClause") + names.fromString("cachedValueNotInClause") ), List.of( Ident(names.fromString(schemaFieldName)), From 1c010ea7838fdb93a95d0fb13e1c15ff77bce1f7 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 10:08:05 -0400 Subject: [PATCH 12/22] delete todo --- core/src/main/java/net/staticstudios/data/PersistentValue.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/net/staticstudios/data/PersistentValue.java b/core/src/main/java/net/staticstudios/data/PersistentValue.java index 29220056..afc50b02 100644 --- a/core/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/core/src/main/java/net/staticstudios/data/PersistentValue.java @@ -13,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); From 02c250d833fa354448266ad4cfbdfb3a161cb142 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 16:58:32 -0400 Subject: [PATCH 13/22] update the ide plugin --- .../ide/intellij/DataPsiAugmentProvider.java | 134 +++++++++--------- .../ide/intellij/IntelliJPluginUtils.java | 30 ++-- .../ide/intellij/SyntheticBuilderClass.java | 14 ++ .../data/ide/intellij/SyntheticMethod.java | 31 ++++ 4 files changed, 123 insertions(+), 86 deletions(-) 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 4a87987c..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); @@ -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 50753316..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) { 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..3d5c71d6 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()); + } } \ 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..62f1f1b9 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,26 @@ 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() { + return Objects.hash(name, containingClass); + } } \ No newline at end of file From 5bd282beea5e07468cd3ee09ad1f814aae2194d7 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 17:16:16 -0400 Subject: [PATCH 14/22] updates --- .../data/query/QueryBuilder.java | 72 ++++++++++++++++++- .../ide/intellij/SyntheticBuilderClass.java | 2 +- .../data/ide/intellij/SyntheticMethod.java | 7 +- 3 files changed, 78 insertions(+), 3 deletions(-) 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 96ab26a6..42dd6c85 100644 --- a/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java +++ b/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java @@ -262,11 +262,81 @@ public final QueryWhere lessThanOrEqualTo(String schema, String table, String co return this; } - public final QueryWhere cachedValueEquals(String schema, String table, String identifier, String value) { + 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 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/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 3d5c71d6..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 @@ -147,6 +147,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(getName()); + 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 62f1f1b9..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 @@ -142,6 +142,11 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(name, containingClass); + 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 From 7ec95de9753eb5a817581db0f7401101af2f12cc Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 17:17:32 -0400 Subject: [PATCH 15/22] update toString --- .../data/compiler/javac/javac/ParsedCachedValue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index c04fcf9c..46df082f 100644 --- 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 @@ -80,7 +80,7 @@ public String[] getTypeFQNParts() { @Override public String toString() { - return "PersistentValue{" + + return "ParsedCacheValue{" + "fieldName='" + fieldName + '\'' + ", schema='" + schema + '\'' + ", table='" + table + '\'' + From 2f2e416cd38fd9748627c17ffd91e52bddfbdf00 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 17:18:21 -0400 Subject: [PATCH 16/22] make methods private --- .../data/compiler/javac/javac/QueryBuilderProcessor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 4010d3cb..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 @@ -1434,7 +1434,7 @@ private void addCachedValueIsNotInCollectionMethod(ParsedCachedValue cv, String ), builderClassDecl); } - public void addCachedValueIsNotInArrayMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + private void addCachedValueIsNotInArrayMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { createMethod(MethodDef( Modifiers(Flags.PUBLIC | Flags.FINAL), names.fromString(cv.getFieldName() + "IsNotIn"), @@ -1475,7 +1475,7 @@ public void addCachedValueIsNotInArrayMethod(ParsedCachedValue cv, String schema ), builderClassDecl); } - public void addCachedValueIsNullMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + private void addCachedValueIsNullMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { createMethod(MethodDef( Modifiers(Flags.PUBLIC | Flags.FINAL), names.fromString(cv.getFieldName() + "IsNull"), @@ -1506,7 +1506,7 @@ public void addCachedValueIsNullMethod(ParsedCachedValue cv, String schemaFieldN ), builderClassDecl); } - public void addCachedValueIsNotNullMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { + private void addCachedValueIsNotNullMethod(ParsedCachedValue cv, String schemaFieldName, String tableFieldName, String identifierFieldName) { createMethod(MethodDef( Modifiers(Flags.PUBLIC | Flags.FINAL), names.fromString(cv.getFieldName() + "IsNotNull"), From a4f2baa18d11909615bc60a1e8e34a31e5ea79cf Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 17:20:06 -0400 Subject: [PATCH 17/22] concurrent hashmap --- core/src/main/java/net/staticstudios/data/DataManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 47fcc570..92592b82 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -43,7 +43,7 @@ @ApiStatus.Internal public class DataManager { - private static final Map DATA_MANAGER_INSTANCES = new HashMap<>(); + 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()); From 42a8af94fbae8628cd048fe5261b33dd4ff403d0 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 17:21:00 -0400 Subject: [PATCH 18/22] update sql column --- .../src/main/java/net/staticstudios/data/parse/SQLColumn.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/net/staticstudios/data/parse/SQLColumn.java b/core/src/main/java/net/staticstudios/data/parse/SQLColumn.java index 09707305..85953f6a 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLColumn.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLColumn.java @@ -76,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 @@ -88,6 +89,7 @@ public String toString() { ", indexed=" + indexed + ", unique=" + unique + ", defaultValue='" + defaultValue + '\'' + + ", virtual=" + virtual + '}'; } } From e0072cc82ff207806af07d50a86101c97fcaecf8 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 17:30:53 -0400 Subject: [PATCH 19/22] changes --- build.gradle | 2 +- .../java/net/staticstudios/data/impl/h2/H2DataAccessor.java | 3 +++ .../java/net/staticstudios/data/query/BaseQueryWhere.java | 4 ++-- .../java/net/staticstudios/data/util/ReadCacheResult.java | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) 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/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/core/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index 161506cc..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 @@ -669,6 +669,9 @@ private void setRedisValueCache(String holderSchema, String holderTable, String 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); 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 4a616984..02d932b4 100644 --- a/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java @@ -178,7 +178,7 @@ protected void cachedValueNullClause(String schema, String table, String identif 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) { @@ -234,7 +234,7 @@ private boolean isSpecialOnlyUseIdColumnsRecursive(Node node, String schema, Str columnValuePairs.add(new ColumnValuePair(equalsClause.getColumn(), equalsClause.getValue())); return true; } - } else if (node.clause instanceof ConditionalClause) { + } else if (node.clause instanceof AndClause) { return isSpecialOnlyUseIdColumnsRecursive(node.lhs, schema, table, columns, columnValuePairs) && isSpecialOnlyUseIdColumnsRecursive(node.rhs, schema, table, columns, columnValuePairs); } return false; diff --git a/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java b/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java index 4419e19f..72009fe4 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java +++ b/core/src/main/java/net/staticstudios/data/util/ReadCacheResult.java @@ -22,7 +22,7 @@ public Set getDependencies() { @Override public String toString() { return "ReadCacheResult[" + - "values=" + value + + "value=" + value + ", dependencies=" + dependencies + ']'; } } From dd97c93da59408e3b62403b0dc5fdc9feef7d9f9 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 17:34:11 -0400 Subject: [PATCH 20/22] update plugin id --- intellij-plugin/src/main/resources/META-INF/plugin.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml index a46e8026..76695a47 100644 --- a/intellij-plugin/src/main/resources/META-INF/plugin.xml +++ b/intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -1,5 +1,5 @@ - net.staticstudios.data.ide.intellij + net.staticstudios.data.ide static-data Static Studios 1.0.0 From a74c76ad4c59a2d50df7b4ba2b9e0926a882a7ef Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 17:35:24 -0400 Subject: [PATCH 21/22] capitalize plugin name --- intellij-plugin/src/main/resources/META-INF/plugin.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml index 76695a47..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 - static-data + Static-Data Static Studios 1.0.0 From 62aa590a6b1438ee9ad2453c91cece6f643426f8 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 8 Mar 2026 17:41:34 -0400 Subject: [PATCH 22/22] update query builder --- .../main/java/net/staticstudios/data/query/QueryBuilder.java | 5 +++++ 1 file changed, 5 insertions(+) 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 42dd6c85..80918edf 100644 --- a/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java +++ b/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java @@ -307,6 +307,11 @@ public final QueryWhere cachedValueIn(String schema, String table, String identi 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;