From 3674f5959df14d2ef1673b3cfaf4f9d9042c01db Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 6 Sep 2025 11:44:22 -0400 Subject: [PATCH 01/75] progress --- build.gradle | 14 +- .../net/staticstudios/data/DataAccessor.java | 15 + .../net/staticstudios/data/DataManager.java | 1184 ++--------------- .../staticstudios/data/PersistentValue.java | 390 ++---- .../java/net/staticstudios/data/Relation.java | 4 + .../net/staticstudios/data/UniqueData.java | 147 +- .../data/impl/sqlite/SQLiteDataAccessor.java | 128 ++ .../impl/sqlite/SQLitePersistentValue.java | 123 ++ .../net/staticstudios/data/parse/Column.java | 18 + .../net/staticstudios/data/parse/Data.java | 14 + .../staticstudios/data/parse/SQLBuilder.java | 110 ++ .../staticstudios/data/parse/SQLColumn.java | 33 + .../staticstudios/data/parse/SQLSchema.java | 41 + .../staticstudios/data/parse/SQLTable.java | 47 + .../data/parse/UniqueDataMetadata.java | 5 + .../util/EnvironmentVariableAccessor.java | 7 + .../data/util/FieldInstancePair.java | 8 + .../staticstudios/data/util/PrimaryKey.java | 11 + .../data/util/ReflectionUtils.java | 51 + .../staticstudios/data/util/UUIDUtils.java | 21 + .../staticstudios/data/util/ValueUtils.java | 26 + .../net/staticstudios/data/CachedValue.java | 0 .../net/staticstudios/data/DataManager.java | 1149 ++++++++++++++++ .../data/PersistentCollection.java | 0 .../staticstudios/data/PersistentValue.java | 343 +++++ .../net/staticstudios/data/Reference.java | 0 .../net/staticstudios/data/UniqueData.java | 157 +++ .../staticstudios/data/ValueSerializer.java | 0 .../net/staticstudios/data/data/Data.java | 0 .../staticstudios/data/data/DataHolder.java | 0 .../staticstudios/data/data/Deletable.java | 0 .../staticstudios/data/data/InitialValue.java | 0 .../data/data/collection/CollectionEntry.java | 0 .../collection/CollectionEntryIdentifier.java | 0 .../PersistentCollectionChangeHandler.java | 0 .../PersistentManyToManyCollection.java | 0 .../PersistentUniqueDataCollection.java | 0 .../collection/PersistentValueCollection.java | 0 .../SimplePersistentCollection.java | 0 .../data/data/value/InitialCachedValue.java | 0 .../data/value/InitialPersistentValue.java | 0 .../staticstudios/data/data/value/Value.java | 0 .../data/impl/CachedValueManager.java | 0 .../impl/PersistentCollectionManager.java | 0 .../data/impl/PersistentValueManager.java | 0 .../data/impl/pg/PostgresData.java | 0 .../data/impl/pg/PostgresListener.java | 0 .../data/impl/pg/PostgresNotification.java | 0 .../data/impl/pg/PostgresOperation.java | 0 .../net/staticstudios/data/key/CellKey.java | 0 .../staticstudios/data/key/CollectionKey.java | 0 .../net/staticstudios/data/key/DataKey.java | 0 .../staticstudios/data/key/DatabaseKey.java | 0 .../net/staticstudios/data/key/RedisKey.java | 0 .../data/key/UniqueIdentifier.java | 0 .../data/primative/Primitive.java | 0 .../data/primative/PrimitiveBuilder.java | 0 .../data/primative/Primitives.java | 0 .../staticstudios/data/util/BatchInsert.java | 0 .../staticstudios/data/util/CacheEntry.java | 0 .../data/util/ConnectionConsumer.java | 8 + .../data/util/ConnectionJedisConsumer.java | 10 + .../data/util/DataDoesNotExistException.java | 0 .../data/util/DataSourceConfig.java | 12 + .../data/util/DeleteContext.java | 0 .../data/util/DeletionStrategy.java | 0 .../data/util/InsertContext.java | 0 .../data/util/InsertionStrategy.java | 0 .../data/util/JunctionTable.java | 0 .../data/util/PostgresUtils.java | 0 .../data/util/ReflectionUtils.java | 24 + .../staticstudios/data/util/SQLLogger.java | 0 .../staticstudios/data/util/TaskQueue.java | 111 ++ .../staticstudios/data/util/ValueUpdate.java | 6 + .../data/util/ValueUpdateHandler.java | 11 + .../staticstudios/data/CachedValueTest.java | 10 +- .../net/staticstudios/data/DeletionTest.java | 16 +- .../net/staticstudios/data/InsertionTest.java | 4 +- .../data/PersistentCollectionTest.java | 6 +- .../data/PersistentValueTest.java | 354 +++++ .../data/PostgresListenerTest.java | 2 +- .../staticstudios/data/PrimitivesTest.java | 24 +- .../net/staticstudios/data/ReferenceTest.java | 6 +- .../net/staticstudios/data/misc/DataTest.java | 112 ++ .../data/misc/MockEnvironment.java | 10 + .../data/misc/MockThreadProvider.java | 125 ++ .../staticstudios/data/misc/TestUtils.java | 26 + .../data/mock/cachedvalue/RedditUser.java | 0 .../data/mock/deletions/MinecraftServer.java | 0 .../data/mock/deletions/MinecraftSkin.java | 0 .../deletions/MinecraftUserStatistics.java | 0 ...ecraftUserWithCascadeDeletionStrategy.java | 0 ...craftUserWithNoActionDeletionStrategy.java | 0 ...necraftUserWithUnlinkDeletionStrategy.java | 0 .../mock/insertions/TwitchChatMessage.java | 0 .../data/mock/insertions/TwitchUser.java | 0 .../persistentcollection/FacebookPost.java | 0 .../persistentcollection/FacebookUser.java | 0 .../mock/persistentvalue/DiscordUser.java | 0 .../persistentvalue/DiscordUserSettings.java | 0 .../primative/BooleanPrimitiveTestObject.java | 0 .../ByteArrayPrimitiveTestObject.java | 0 .../primative/BytePrimitiveTestObject.java | 0 .../CharacterPrimitiveTestObject.java | 0 .../primative/DoublePrimitiveTestObject.java | 0 .../primative/FloatPrimitiveTestObject.java | 0 .../primative/IntegerPrimitiveTestObject.java | 0 .../primative/LongPrimitiveTestObject.java | 0 .../primative/ShortPrimitiveTestObject.java | 0 .../primative/StringPrimitiveTestObject.java | 0 .../TimestampPrimitiveTestObject.java | 0 .../primative/UUIDPrimitiveTestObject.java | 0 .../data/mock/reference/SnapchatUser.java | 0 .../mock/reference/SnapchatUserSettings.java | 0 src/ogtest/resources/log4j.properties | 6 + .../data/PersistentValueTest.java | 351 +---- .../net/staticstudios/data/SQLParseTest.java | 13 + .../staticstudios/data/ValueParseTest.java | 32 + .../net/staticstudios/data/mock/MockUser.java | 34 + static-data-cache.db | 0 120 files changed, 3470 insertions(+), 1889 deletions(-) create mode 100644 src/main/java/net/staticstudios/data/DataAccessor.java create mode 100644 src/main/java/net/staticstudios/data/Relation.java create mode 100644 src/main/java/net/staticstudios/data/impl/sqlite/SQLiteDataAccessor.java create mode 100644 src/main/java/net/staticstudios/data/impl/sqlite/SQLitePersistentValue.java create mode 100644 src/main/java/net/staticstudios/data/parse/Column.java create mode 100644 src/main/java/net/staticstudios/data/parse/Data.java create mode 100644 src/main/java/net/staticstudios/data/parse/SQLBuilder.java create mode 100644 src/main/java/net/staticstudios/data/parse/SQLColumn.java create mode 100644 src/main/java/net/staticstudios/data/parse/SQLSchema.java create mode 100644 src/main/java/net/staticstudios/data/parse/SQLTable.java create mode 100644 src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java create mode 100644 src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java create mode 100644 src/main/java/net/staticstudios/data/util/FieldInstancePair.java create mode 100644 src/main/java/net/staticstudios/data/util/PrimaryKey.java create mode 100644 src/main/java/net/staticstudios/data/util/UUIDUtils.java create mode 100644 src/main/java/net/staticstudios/data/util/ValueUtils.java rename src/{main => og}/java/net/staticstudios/data/CachedValue.java (100%) create mode 100644 src/og/java/net/staticstudios/data/DataManager.java rename src/{main => og}/java/net/staticstudios/data/PersistentCollection.java (100%) create mode 100644 src/og/java/net/staticstudios/data/PersistentValue.java rename src/{main => og}/java/net/staticstudios/data/Reference.java (100%) create mode 100644 src/og/java/net/staticstudios/data/UniqueData.java rename src/{main => og}/java/net/staticstudios/data/ValueSerializer.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/Data.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/DataHolder.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/Deletable.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/InitialValue.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/collection/CollectionEntry.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/collection/PersistentValueCollection.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/value/InitialCachedValue.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/value/InitialPersistentValue.java (100%) rename src/{main => og}/java/net/staticstudios/data/data/value/Value.java (100%) rename src/{main => og}/java/net/staticstudios/data/impl/CachedValueManager.java (100%) rename src/{main => og}/java/net/staticstudios/data/impl/PersistentCollectionManager.java (100%) rename src/{main => og}/java/net/staticstudios/data/impl/PersistentValueManager.java (100%) rename src/{main => og}/java/net/staticstudios/data/impl/pg/PostgresData.java (100%) rename src/{main => og}/java/net/staticstudios/data/impl/pg/PostgresListener.java (100%) rename src/{main => og}/java/net/staticstudios/data/impl/pg/PostgresNotification.java (100%) rename src/{main => og}/java/net/staticstudios/data/impl/pg/PostgresOperation.java (100%) rename src/{main => og}/java/net/staticstudios/data/key/CellKey.java (100%) rename src/{main => og}/java/net/staticstudios/data/key/CollectionKey.java (100%) rename src/{main => og}/java/net/staticstudios/data/key/DataKey.java (100%) rename src/{main => og}/java/net/staticstudios/data/key/DatabaseKey.java (100%) rename src/{main => og}/java/net/staticstudios/data/key/RedisKey.java (100%) rename src/{main => og}/java/net/staticstudios/data/key/UniqueIdentifier.java (100%) rename src/{main => og}/java/net/staticstudios/data/primative/Primitive.java (100%) rename src/{main => og}/java/net/staticstudios/data/primative/PrimitiveBuilder.java (100%) rename src/{main => og}/java/net/staticstudios/data/primative/Primitives.java (100%) rename src/{main => og}/java/net/staticstudios/data/util/BatchInsert.java (100%) rename src/{main => og}/java/net/staticstudios/data/util/CacheEntry.java (100%) create mode 100644 src/og/java/net/staticstudios/data/util/ConnectionConsumer.java create mode 100644 src/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java rename src/{main => og}/java/net/staticstudios/data/util/DataDoesNotExistException.java (100%) create mode 100644 src/og/java/net/staticstudios/data/util/DataSourceConfig.java rename src/{main => og}/java/net/staticstudios/data/util/DeleteContext.java (100%) rename src/{main => og}/java/net/staticstudios/data/util/DeletionStrategy.java (100%) rename src/{main => og}/java/net/staticstudios/data/util/InsertContext.java (100%) rename src/{main => og}/java/net/staticstudios/data/util/InsertionStrategy.java (100%) rename src/{main => og}/java/net/staticstudios/data/util/JunctionTable.java (100%) rename src/{main => og}/java/net/staticstudios/data/util/PostgresUtils.java (100%) create mode 100644 src/og/java/net/staticstudios/data/util/ReflectionUtils.java rename src/{main => og}/java/net/staticstudios/data/util/SQLLogger.java (100%) create mode 100644 src/og/java/net/staticstudios/data/util/TaskQueue.java create mode 100644 src/og/java/net/staticstudios/data/util/ValueUpdate.java create mode 100644 src/og/java/net/staticstudios/data/util/ValueUpdateHandler.java rename src/{test => ogtest}/java/net/staticstudios/data/CachedValueTest.java (95%) rename src/{test => ogtest}/java/net/staticstudios/data/DeletionTest.java (97%) rename src/{test => ogtest}/java/net/staticstudios/data/InsertionTest.java (96%) rename src/{test => ogtest}/java/net/staticstudios/data/PersistentCollectionTest.java (99%) create mode 100644 src/ogtest/java/net/staticstudios/data/PersistentValueTest.java rename src/{test => ogtest}/java/net/staticstudios/data/PostgresListenerTest.java (98%) rename src/{test => ogtest}/java/net/staticstudios/data/PrimitivesTest.java (96%) rename src/{test => ogtest}/java/net/staticstudios/data/ReferenceTest.java (96%) create mode 100644 src/ogtest/java/net/staticstudios/data/misc/DataTest.java create mode 100644 src/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java create mode 100644 src/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java create mode 100644 src/ogtest/java/net/staticstudios/data/misc/TestUtils.java rename src/{test => ogtest}/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/deletions/MinecraftServer.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/insertions/TwitchUser.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/reference/SnapchatUser.java (100%) rename src/{test => ogtest}/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java (100%) create mode 100644 src/ogtest/resources/log4j.properties create mode 100644 src/test/java/net/staticstudios/data/SQLParseTest.java create mode 100644 src/test/java/net/staticstudios/data/ValueParseTest.java create mode 100644 src/test/java/net/staticstudios/data/mock/MockUser.java create mode 100644 static-data-cache.db diff --git a/build.gradle b/build.gradle index 628027e6..a2b794cb 100644 --- a/build.gradle +++ b/build.gradle @@ -23,11 +23,17 @@ dependencies { implementation 'com.zaxxer:HikariCP:5.1.0' implementation 'redis.clients:jedis:5.1.2' implementation 'net.staticstudios:static-utils:1.0.1' + implementation 'com.h2database:h2:2.3.232' + implementation 'org.xerial:sqlite-jdbc:3.45.1.0' + implementation 'org.jetbrains:annotations:24.0.1' - testImplementation(platform('org.junit:junit-bom:5.10.3')) - testImplementation('org.junit.jupiter:junit-jupiter') - testImplementation('org.junit-pioneer:junit-pioneer:2.3.0'); - testRuntimeOnly('org.junit.platform:junit-platform-launcher') +// testImplementation(platform('org.junit:junit-bom:5.10.3')) +// testImplementation('org.junit.jupiter:junit-jupiter') +// testImplementation('org.junit-pioneer:junit-pioneer:2.3.0'); +// testRuntimeOnly('org.junit.platform:junit-platform-launcher') + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testImplementation("org.testcontainers:postgresql:1.19.8") testImplementation("com.redis:testcontainers-redis:2.2.2") diff --git a/src/main/java/net/staticstudios/data/DataAccessor.java b/src/main/java/net/staticstudios/data/DataAccessor.java new file mode 100644 index 00000000..6066c0a7 --- /dev/null +++ b/src/main/java/net/staticstudios/data/DataAccessor.java @@ -0,0 +1,15 @@ +package net.staticstudios.data; + +import net.staticstudios.data.util.PrimaryKey; +import org.intellij.lang.annotations.Language; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public interface DataAccessor { + PreparedStatement prepareStatement(@Language("SQL") String sql) throws SQLException; + + PersistentValue createPersistentValue(PrimaryKey primaryKey, Class dataType, String schema, String table, String dataColumn); + + void insertIntoCache(UniqueData uniqueData) throws SQLException; +} diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index a02b1483..3fd9681e 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -1,1149 +1,197 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; -import com.google.common.base.Predicate; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; -import net.staticstudios.data.data.Data; -import net.staticstudios.data.data.InitialValue; -import net.staticstudios.data.data.collection.PersistentManyToManyCollection; -import net.staticstudios.data.data.collection.PersistentUniqueDataCollection; -import net.staticstudios.data.data.collection.SimplePersistentCollection; -import net.staticstudios.data.data.value.InitialCachedValue; -import net.staticstudios.data.data.value.InitialPersistentValue; -import net.staticstudios.data.data.value.Value; -import net.staticstudios.data.impl.CachedValueManager; -import net.staticstudios.data.impl.PersistentCollectionManager; -import net.staticstudios.data.impl.PersistentValueManager; -import net.staticstudios.data.impl.pg.PostgresListener; -import net.staticstudios.data.impl.pg.PostgresOperation; -import net.staticstudios.data.key.CellKey; -import net.staticstudios.data.key.DataKey; -import net.staticstudios.data.key.DatabaseKey; -import net.staticstudios.data.primative.Primitives; -import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.data.impl.sqlite.SQLiteDataAccessor; +import net.staticstudios.data.parse.Column; +import net.staticstudios.data.parse.Data; +import net.staticstudios.data.parse.SQLBuilder; +import net.staticstudios.data.parse.UniqueDataMetadata; import net.staticstudios.data.util.*; -import net.staticstudios.utils.ShutdownStage; -import net.staticstudios.utils.ThreadUtils; +import net.staticstudios.data.util.TaskQueue; import org.jetbrains.annotations.Blocking; -import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import redis.clients.jedis.Jedis; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.sql.Connection; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.time.Instant; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; - -/** - * Manages all data operations. - */ -public class DataManager extends SQLLogger { - private static final Object NULL_MARKER = new Object(); - private final Logger logger = LoggerFactory.getLogger(DataManager.class); - private final Map cache; - private final Multimap> valueUpdateHandlers; - private final Multimap, UUID> uniqueDataIds; - private final Map, Map> uniqueDataCache; - private final Multimap> dummyValueMap; - private final Multimap> dummySimplePersistentCollectionMap; - private final Multimap> dummyPersistentManyToManyCollectionMap; - private final Multimap dummyUniqueDataMap; - private final Map, UniqueData> dummyInstances; - private final Set> loadedDependencies = new HashSet<>(); - private final List> valueSerializers; - private final PostgresListener pgListener; - private final String applicationName; - private final PersistentValueManager persistentValueManager; - private final PersistentCollectionManager persistentCollectionManager; - private final CachedValueManager cachedValueManager; +public class DataManager { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private final DataAccessor dataAccessor; + private final SQLBuilder sqlBuilder; private final TaskQueue taskQueue; + private final ConcurrentHashMap, UniqueDataMetadata> uniqueDataMetadataMap = new ConcurrentHashMap<>(); - - /** - * Create a new data manager. - * - * @param dataSourceConfig the data source configuration - */ public DataManager(DataSourceConfig dataSourceConfig) { - this.applicationName = "static_data_manager-" + UUID.randomUUID(); - this.cache = new ConcurrentHashMap<>(); - this.valueUpdateHandlers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); - this.uniqueDataCache = new ConcurrentHashMap<>(); - this.dummyValueMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.dummySimplePersistentCollectionMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.dummyPersistentManyToManyCollectionMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.dummyUniqueDataMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.dummyInstances = new ConcurrentHashMap<>(); - this.uniqueDataIds = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.valueSerializers = new CopyOnWriteArrayList<>(); - - pgListener = new PostgresListener(this, dataSourceConfig); - - pgListener.addHandler(notification -> { //Call insert handler first so that we can access the data in the other handlers - if (notification.getOperation() == PostgresOperation.INSERT) { - logger.trace("Handing INSERT operation"); - Collection dummyUniqueData = dummyUniqueDataMap.get(notification.getSchema() + "." + notification.getTable()); - - for (UniqueData dummy : dummyUniqueData) { - String identifierColumn = dummy.getIdentifier().getColumn(); - UUID id = UUID.fromString(notification.getData().newDataValueMap().get(identifierColumn)); - - UniqueData instance = createInstance(dummy.getClass(), id); - addUniqueData(instance); - } - } - }); - - this.persistentValueManager = new PersistentValueManager(this, pgListener); - this.persistentCollectionManager = new PersistentCollectionManager(this, pgListener); - this.cachedValueManager = new CachedValueManager(this, dataSourceConfig); - - pgListener.addHandler(notification -> { //Call delete handler last so that we can access the data in the other handlers - if (notification.getOperation() == PostgresOperation.DELETE) { - logger.trace("Handling DELETE operation"); - Collection dummyUniqueData = dummyUniqueDataMap.get(notification.getSchema() + "." + notification.getTable()); - - for (UniqueData dummy : dummyUniqueData) { - String identifierColumn = dummy.getIdentifier().getColumn(); - UUID id = UUID.fromString(notification.getData().oldDataValueMap().get(identifierColumn)); - removeUniqueData(dummy.getClass(), id); - } - } - }); - - + String applicationName = "static_data_manager_v3-" + UUID.randomUUID(); + sqlBuilder = new SQLBuilder(); + dataAccessor = new SQLiteDataAccessor(sqlBuilder); this.taskQueue = new TaskQueue(dataSourceConfig, applicationName); - ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, () -> { - cache.clear(); - uniqueDataCache.clear(); - uniqueDataIds.clear(); - }); - } - - /** - * Submit a blocking task to the priority task queue. - * This method is blocking and will wait for the task to complete before returning. - * - * @param task The task to submit - */ - @Blocking - public void submitBlockingTask(ConnectionConsumer task) { - taskQueue.submitTask(task).join(); - } - - public void submitAsyncTask(ConnectionConsumer task) { - taskQueue.submitTask(task) - .exceptionally(e -> { - logger.error("Error submitting async task", e); - return null; - }); - } - - /** - * Submit a blocking task to the priority task queue. - * This method is blocking and will wait for the task to complete before returning. - * - * @param task The task to submit - */ - @Blocking - public void submitBlockingTask(ConnectionJedisConsumer task) { - taskQueue.submitTask(task).join(); - } - - public void submitAsyncTask(ConnectionJedisConsumer task) { - taskQueue.submitTask(task); - } - - public PersistentValueManager getPersistentValueManager() { - return persistentValueManager; - } - - public PersistentCollectionManager getPersistentCollectionManager() { - return persistentCollectionManager; - } - - public CachedValueManager getCachedValueManager() { - return cachedValueManager; - } - - public String getApplicationName() { - return applicationName; - } - - public Collection> getDummyValues(String schemaTable) { - return dummyValueMap.get(schemaTable); - } - - public Collection getDummyUniqueData(String schemaTable) { - return dummyUniqueDataMap.get(schemaTable); - } - - public UniqueData getDummyInstance(Class clazz) { - return dummyInstances.get(clazz); - } - - public Collection> getDummyPersistentCollections(String schemaTable) { - return dummySimplePersistentCollectionMap.get(schemaTable); - } - - public Collection> getDummyPersistentManyToManyCollection(String schemaTable) { - return dummyPersistentManyToManyCollectionMap.get(schemaTable); - } - - public Collection> getAllDummyPersistentManyToManyCollections() { - return new HashSet<>(dummyPersistentManyToManyCollectionMap.values()); - } - - /** - * Load all the data for a given class from the datasource(s) into the cache. - * This method will recursively load all dependencies of the given class. - * Note that calling this method registers a specific data type. - * Without calling this method, the data manager will have no knowledge of the data type. - * This should be called at the start of the application to ensure all data types are registered. - * This method is inherently blocking. - * - * @param clazz The class to load data for - * @param The type of data to load - * @return A list of all the loaded data - */ - @Blocking - public List loadAll(Class clazz) { - logger.debug("Registering: {}", clazz.getName()); - try { - // A dependency is just another UniqueData that is referenced in some way. - // This clazz is also treated as a dependency, these will all be loaded at the same time - Set> dependencies = new HashSet<>(); - extractDependencies(clazz, dependencies); - dependencies.removeIf(loadedDependencies::contains); - - List dummyHolders = new ArrayList<>(); - Multimap dummyDatabaseKeys = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); - Multimap> dummySimplePersistentCollections = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); - Multimap> dummyPersistentManyToManyCollections = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); - Multimap> dummyCachedValues = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); - - - for (Class dependency : dependencies) { - // A data dependency is some data field on one of our dependencies - List> dataDependencies = extractDataDependencies(dependency); - - dummyHolders.add(createInstance(dependency, null)); - - for (Data data : dataDependencies) { - DataKey key = data.getKey(); - if (key instanceof DatabaseKey dbKey) { - dummyDatabaseKeys.put(data.getHolder().getRootHolder(), dbKey); - } - - // This is our first time seeing this value, so we add it to the map for later use - if (data instanceof PersistentValue value) { - dummyValueMap.put(value.getSchema() + "." + value.getTable(), value); - } - - if (data instanceof Reference reference) { - PersistentValue value = reference.getBackingValue(); - dummyValueMap.put(value.getSchema() + "." + value.getTable(), value); - } - - if (data instanceof SimplePersistentCollection collection) { - dummySimplePersistentCollectionMap.put(collection.getSchema() + "." + collection.getTable(), collection); - dummySimplePersistentCollections.put(data.getHolder().getRootHolder(), collection); - } - - if (data instanceof PersistentManyToManyCollection collection) { - dummyPersistentManyToManyCollectionMap.put(collection.getSchema() + "." + collection.getJunctionTable(), collection); - dummyPersistentManyToManyCollections.put(data.getHolder().getRootHolder(), collection); - } - - if (data instanceof CachedValue cachedValue) { - dummyValueMap.put(cachedValue.getSchema() + "." + cachedValue.getTable(), cachedValue); - dummyCachedValues.put(data.getHolder().getRootHolder(), cachedValue); - } - } - } - - submitBlockingTask((connection, jedis) -> { - // Load all the unique data first - for (UniqueData dummyHolder : dummyHolders) { - loadUniqueData(connection, dummyHolder); - } - - //Load PersistentValues - for (UniqueData dummyHolder : dummyHolders) { - Collection keys = dummyDatabaseKeys.get(dummyHolder); - - // Use a multimap so we can easily group CellKeys together - // The actual key (DataKey) for this map is solely used for grouping entries with the same schema, table, data column, and id column - Multimap persistentDataColumns = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new); - - for (DatabaseKey key : keys) { - if (key instanceof CellKey cellKey) { - persistentDataColumns.put(new DataKey(cellKey.getSchema(), cellKey.getTable(), cellKey.getColumn(), cellKey.getIdColumn()), cellKey); - } - } - - for (DataKey key : persistentDataColumns.keySet()) { - persistentValueManager.loadAllFromDatabase(connection, dummyHolder, persistentDataColumns.get(key)); - } - } - - //Load SimplePersistentCollections - for (UniqueData dummyHolder : dummyHolders) { - Collection> dummyCollections = dummySimplePersistentCollections.get(dummyHolder); - - for (SimplePersistentCollection dummyCollection : dummyCollections) { - persistentCollectionManager.loadAllFromDatabase(connection, dummyHolder, dummyCollection); - } - } - - //Load PersistentManyToManyCollections - for (UniqueData dummyHolder : dummyHolders) { - Collection> dummyCollections = dummyPersistentManyToManyCollections.get(dummyHolder); - - for (PersistentManyToManyCollection dummyCollection : dummyCollections) { - persistentCollectionManager.loadJunctionTablesFromDatabase(connection, dummyCollection); - } - } - - //Load CachedValues - for (UniqueData dummyHolder : dummyHolders) { - Collection> dummyValues = dummyCachedValues.get(dummyHolder); - - for (CachedValue dummyValue : dummyValues) { - cachedValueManager.loadAllFromRedis(jedis, dummyValue); - } - } - }); - - loadedDependencies.addAll(dependencies); - } catch (Exception e) { - throw new RuntimeException(e); - } - - //to load data, we must first find a list of dependencies, and recursively grab their dependencies. after we've found all of them, we should load all data, and then we can establish relationships - -// try (Connection connection = getConnection()) { -// -// } catch (SQLException e) { -// throw new RuntimeException(e); -// } - - // All the entries we either just load or were previously loaded due to them being a dependency are now in the cache, so just return them via getAll() - return getAll(clazz); + //todo: when we parse UniqueData objects we should build an internal map, and then when we are done auto create the sql if the tables dont exist + //todo: this will be extremely useful for building the internal cache tables } - /** - * Create a new batch insert operation. - * - * @return The batch insert operation - */ - public final BatchInsert batchInsert() { - return new BatchInsert(this); + public DataAccessor getDataAccessor() { + return dataAccessor; } - /** - * Synchronously insert data into the datasource(s) and cache. - * - * @param batch The batch of data to insert - */ - @Blocking - public final void insertBatch(BatchInsert batch, List intermediateActions, List preInsertActions, List postInsertActions) { - submitBlockingTask((connection, jedis) -> { - connection.setAutoCommit(false); - for (InsertContext context : batch.getInsertionContexts()) { - insertIntoDataSource(connection, jedis, context); - } - for (ConnectionConsumer action : intermediateActions) { - action.accept(connection); - } - connection.setAutoCommit(true); - - for (InsertContext context : batch.getInsertionContexts()) { - insertIntoCache(context); - } - - for (Runnable action : preInsertActions) { - try { - action.run(); - } catch (Exception e) { - logger.error("Error running pre-insert action", e); - } - } - - for (Runnable action : postInsertActions) { - try { - action.run(); - } catch (Exception e) { - logger.error("Error running post-insert action", e); - } - } - }); + public SQLBuilder getSQLBuilder() { + return sqlBuilder; } - /** - * Asynchronously insert data into the datasource(s) and cache. - * The data will be instantly inserted into the cache, and then the datasource(s) will be updated asynchronously. - * This method is non-blocking and will return immediately. - * The cached data can be accessed immediately after this method is called. - * - * @param batch The batch of data to insert - */ - public final void insertBatchAsync(BatchInsert batch, List intermediateActions, List preInsertActions, List postInsertActions) { - for (InsertContext context : batch.getInsertionContexts()) { - insertIntoCache(context); - } - for (Runnable action : preInsertActions) { - try { - action.run(); - } catch (Exception e) { - logger.error("Error running pre-insert action", e); - } + @SafeVarargs + public final void load(Class... classes) { + for (Class clazz : classes) { + extractMetadata(clazz); } - submitAsyncTask((connection, jedis) -> { - connection.setAutoCommit(false); - for (InsertContext context : batch.getInsertionContexts()) { - insertIntoDataSource(connection, jedis, context); - } - for (ConnectionConsumer action : intermediateActions) { - action.accept(connection); - } - connection.setAutoCommit(true); - - for (Runnable action : postInsertActions) { - try { - action.run(); - } catch (Exception e) { - logger.error("Error running post-insert action", e); - } - } - }); - } - - /** - * Synchronously insert data into the datasource(s) and cache. - * - * @param holder The root holder of the data to insert - * @param initialData The initial data to insert - */ - @Blocking - public final void insert(UniqueData holder, InitialValue... initialData) { - InsertContext context = buildInsertContext(holder, initialData); - - submitBlockingTask((connection, jedis) -> { - insertIntoDataSource(connection, jedis, context); - insertIntoCache(context); - }); + //todo: create source tables if not exists + //todo tell cache accessor to create its cache tables } - /** - * Asynchronously insert data into the datasource(s) and cache. - * The data will be instantly inserted into the cache, and then the datasource(s) will be updated asynchronously. - * This method is non-blocking and will return immediately. - * The cached data can be accessed immediately after this method is called. - * - * @param holder The root holder of the data to insert - * @param initialData The initial data to insert - */ - public final void insertAsync(UniqueData holder, InitialValue... initialData) { - InsertContext context = buildInsertContext(holder, initialData); - insertIntoCache(context); + public void extractMetadata(Class clazz) { + Preconditions.checkArgument(!uniqueDataMetadataMap.containsKey(clazz), "UniqueData class %s has already been parsed", clazz.getName()); + Data dataAnnotation = clazz.getAnnotation(Data.class); + Preconditions.checkNotNull(dataAnnotation, "UniqueData class %s is missing @Data annotation", clazz.getName()); - submitAsyncTask((connection, jedis) -> { - insertIntoDataSource(connection, jedis, context); - }); + sqlBuilder.parse(clazz); + UniqueDataMetadata metadata = new UniqueDataMetadata(ValueUtils.parseValue(dataAnnotation.schema()), ValueUtils.parseValue(dataAnnotation.table()), ValueUtils.parseValue(dataAnnotation.idColumn())); + uniqueDataMetadataMap.put(clazz, metadata); } - public InsertContext buildInsertContext(UniqueData holder, InitialValue... initialData) { - Map, InitialPersistentValue> initialPersistentValues = new HashMap<>(); - Map, InitialCachedValue> initialCachedValues = new HashMap<>(); - - for (Field field : ReflectionUtils.getFields(holder.getClass())) { - field.setAccessible(true); - - if (Data.class.isAssignableFrom(field.getType())) { - try { - Data data = (Data) field.get(holder); - if (data instanceof PersistentValue pv) { - initialPersistentValues.put(pv, new InitialPersistentValue(pv, pv.getDefaultValue())); - } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - - for (InitialValue data : initialData) { - if (data instanceof InitialPersistentValue initial) { - initialPersistentValues.put(initial.getValue(), initial); - } else if (data instanceof InitialCachedValue initial) { - if (initial.getInitialDataValue() != null) { - initialCachedValues.put(initial.getValue(), initial); - } - } else { - throw new IllegalArgumentException("Unsupported initial data type: " + data.getClass()); - } - } - - for (Map.Entry, InitialPersistentValue> initial : initialPersistentValues.entrySet()) { - PersistentValue pv = initial.getKey(); - InitialPersistentValue value = initial.getValue(); - - if (Primitives.isPrimitive(pv.getDataType()) && !Primitives.getPrimitive(pv.getDataType()).isNullable()) { - Preconditions.checkNotNull(value.getInitialDataValue(), "Initial data value cannot be null for primitive type: " + pv.getDataType()); - } - } - - for (Map.Entry, InitialCachedValue> initial : initialCachedValues.entrySet()) { - CachedValue cv = initial.getKey(); - InitialCachedValue value = initial.getValue(); - - if (Primitives.isPrimitive(cv.getDataType()) && !Primitives.getPrimitive(cv.getDataType()).isNullable()) { - Preconditions.checkNotNull(value.getInitialDataValue(), "Initial data value cannot be null for primitive type: " + cv.getDataType()); - } - } - - return new InsertContext(holder, initialPersistentValues, initialCachedValues); + public UniqueDataMetadata getMetadata(Class clazz) { + UniqueDataMetadata metadata = uniqueDataMetadataMap.get(clazz); + Preconditions.checkNotNull(metadata, "UniqueData class %s has not been parsed yet", clazz.getName()); + return metadata; } - private void insertIntoCache(InsertContext context) { - addUniqueData(context.holder()); - //todo: REFACTOR: similar to deletions, delegate this to the managers - - List initialPersistentDataWrappers = new ArrayList<>(); - for (InitialPersistentValue data : context.initialPersistentValues().values()) { - InsertionStrategy insertionStrategy = data.getValue().getInsertionStrategy(); + public void init(UniqueData uniqueData) { + Data dataAnnotation = uniqueData.getClass().getAnnotation(Data.class); + Preconditions.checkNotNull(dataAnnotation, "UniqueData class %s is missing @Data annotation", uniqueData.getClass().getName()); + for (FieldInstancePair<@Nullable PersistentValue> pair : ReflectionUtils.getFieldInstancePairs(uniqueData, PersistentValue.class)) { + Column columnAnnotation = pair.field().getAnnotation(Column.class); + Preconditions.checkNotNull(columnAnnotation, "PersistentValue field %s is missing @Column annotation", pair.field().getName()); - //do not call PersistentValueManager#updateCache since we need to cache both values - //before PersistentCollectionManager#handlePersistentValueCacheUpdated is called, otherwise we get unexpected behavior - boolean updateCache = !cache.containsKey(data.getValue().getKey()) || insertionStrategy == InsertionStrategy.OVERWRITE_EXISTING; - Object oldValue; + String columnName = ValueUtils.parseValue(columnAnnotation.value()); + String tableName = ValueUtils.parseValue(columnAnnotation.table().isEmpty() ? dataAnnotation.table() : columnAnnotation.table()); + String schemaName = ValueUtils.parseValue(columnAnnotation.schema().isEmpty() ? dataAnnotation.schema() : columnAnnotation.schema()); - try { - oldValue = get(data.getValue().getKey()); - if (oldValue == NULL_MARKER) { - oldValue = null; - } - } catch (DataDoesNotExistException e) { - oldValue = null; - } - initialPersistentDataWrappers.add(new InitialPersistentDataWrapper(data, updateCache, oldValue)); + //todo: the primary key gets a bit more complicated when we are dealing with a foreign key. this needs to be handled, and a new ForeignKey created which properly maps my id column to the foreign key column. + // for the time being, all id columns are just "id" - if (updateCache) { - cache(data.getValue().getKey(), data.getValue().getDataType(), data.getInitialDataValue(), Instant.now(), false); - } - } - - for (InitialPersistentDataWrapper wrapper : initialPersistentDataWrappers) { - InitialPersistentValue data = wrapper.data; - boolean updateCache = wrapper.updateCache; - Object oldValue = wrapper.oldValue; - UniqueData pvHolder = data.getValue().getHolder().getRootHolder(); - CellKey idColumn = new CellKey( - pvHolder.getSchema(), - pvHolder.getTable(), - pvHolder.getIdentifier().getColumn(), - pvHolder.getId(), - pvHolder.getIdentifier().getColumn() + PersistentValue newPv = dataAccessor.createPersistentValue( + uniqueData, + ReflectionUtils.getGenericType(pair.field()), + schemaName, + tableName, + columnName ); - //Alert the collection manager of this change so it can update what it's keeping track of - if (updateCache) { - persistentCollectionManager.handlePersistentValueCacheUpdated( - data.getValue().getSchema(), - data.getValue().getTable(), - data.getValue().getColumn(), - context.holder().getId(), - data.getValue().getIdColumn(), - oldValue, - data.getInitialDataValue() - ); - } - - persistentCollectionManager.handlePersistentValueCacheUpdated( - idColumn.getSchema(), - idColumn.getTable(), - idColumn.getColumn(), - pvHolder.getId(), - idColumn.getIdColumn(), - null, - pvHolder.getId() - ); - } - - for (InitialCachedValue data : context.initialCachedValues().values()) { - cache(data.getValue().getKey(), data.getValue().getDataType(), data.getInitialDataValue(), Instant.now(), false); - } - } - - private void insertIntoDataSource(Connection connection, Jedis jedis, InsertContext context) throws SQLException { - if (context.initialPersistentValues().isEmpty() && context.initialCachedValues().isEmpty()) { - String sql = "INSERT INTO " + context.holder().getSchema() + "." + context.holder().getTable() + " (" + context.holder().getIdentifier().getColumn() + ") VALUES (?)"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, context.holder().getId()); - statement.executeUpdate(); - } - return; - } - persistentValueManager.insertInDatabase(connection, context.holder(), new ArrayList<>(context.initialPersistentValues().values())); - cachedValueManager.setInRedis(jedis, new ArrayList<>(context.initialCachedValues().values())); - } - - /** - * Delete data from the datasource(s) and cache. - * This method will immediately delete the data from the cache, and then asynchronously delete the data from the datasource(s). - * This method is non-blocking and will return immediately after deleting the data from the cache. - * Data will be recursively deleted based off of the {@link DeletionStrategy} of each member of the data object. - * - * @param holder The root holder of the data to delete - */ - public void delete(UniqueData holder) { - DeleteContext context = buildDeleteContext(holder); - logger.trace("Deleting: {}", context); - deleteFromCache(context); - //todo: i really dislike that update handlers are called when things are deleted. revisit this - - submitAsyncTask((connection, jedis) -> deleteFromDataSource(connection, jedis, context)); - } - - /** - * Synchronously delete data from the datasource(s) and cache. - * Data will be recursively deleted based off of the {@link DeletionStrategy} of each member of the data object. - * This method is inherently blocking. - * - * @param holder The root holder of the data to delete - */ - @Blocking - public void deleteSync(UniqueData holder) { - DeleteContext context = buildDeleteContext(holder); - logger.trace("Deleting: {}", context); - deleteFromCache(context); - - submitBlockingTask((connection, jedis) -> deleteFromDataSource(connection, jedis, context)); - } - - private DeleteContext buildDeleteContext(UniqueData holder) { - Set> toDelete = new HashSet<>(); - Set holders = new HashSet<>(); - extractDataToDelete(holder, holders, toDelete); - Map oldValues = new HashMap<>(); - for (Data data : toDelete) { - if (data instanceof PersistentValue || data instanceof CachedValue) { - try { - Object value = get(data.getKey()); - oldValues.put(data.getKey(), value == NULL_MARKER ? null : value); - } catch (DataDoesNotExistException e) { - // This is fine, it just means the value was null - } - } - } - - return new DeleteContext(holders, toDelete, oldValues); - } - - private void extractDataToDelete(UniqueData holder, Set holders, Set> toDelete) { - if (holders.contains(holder)) { - return; - } - holders.add(holder); - - //Add the root holder's id column to the list of things to delete, just in case the holder is empty - toDelete.add(PersistentValue.of(holder.getRootHolder(), UUID.class, holder.getRootHolder().getIdentifier().getColumn())); - - for (Field field : ReflectionUtils.getFields(holder.getClass())) { - field.setAccessible(true); - - if (Data.class.isAssignableFrom(field.getType())) { - try { - Data data = (Data) field.get(holder); - - //Always delete the backing value since it's in the same table as the holder - if (data instanceof Reference reference) { - toDelete.add(reference.getBackingValue()); - } - - if (data.getDeletionStrategy() == DeletionStrategy.NO_ACTION) { - continue; - } - - if (data instanceof Reference reference) { - UUID id = reference.getForeignId(); - if (id != null) { - UniqueData foreignData = reference.get(); - if (foreignData != null) { - extractDataToDelete(foreignData, holders, toDelete); - } - } - } - - if (data instanceof PersistentUniqueDataCollection collection) { - if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) { - for (UniqueData dataInCollection : collection) { - extractDataToDelete(dataInCollection, holders, toDelete); - } - } - } - - if (data instanceof PersistentManyToManyCollection collection) { - if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) { - for (UniqueData dataInCollection : collection) { - extractDataToDelete(dataInCollection, holders, toDelete); - } - } - } - - toDelete.add(data); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - } - - private void deleteFromCache(DeleteContext context) { - persistentCollectionManager.deleteFromCache(context); - persistentValueManager.deleteFromCache(context); - cachedValueManager.deleteFromCache(context); - - for (UniqueData holder : context.holders()) { - removeUniqueData(holder.getClass(), holder.getId()); - } - } - - @Blocking - private void deleteFromDataSource(Connection connection, Jedis jedis, DeleteContext context) throws SQLException { - for (UniqueData holder : context.holders()) { - String sql = "DELETE FROM " + holder.getSchema() + "." + holder.getTable() + " WHERE " + holder.getIdentifier().getColumn() + " = ?"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, holder.getId()); - statement.executeUpdate(); - } - } - persistentValueManager.deleteFromDatabase(connection, context); - persistentCollectionManager.deleteFromDatabase(connection, context); - cachedValueManager.deleteFromRedis(jedis, context); - } - - /** - * Get all data of a given type from the cache. - * - * @param clazz The class of data to get - * @param The type of data to get - * @return A list of all the data of the given type in the cache - */ - public List getAll(Class clazz) { - return uniqueDataIds.get(clazz).stream().map(id -> get(clazz, id)).toList(); - } - - private void extractDependencies(Class clazz, @NotNull Set> dependencies) throws Exception { - if (dependencies.contains(clazz)) { - return; - } - - dependencies.add(clazz); - - if (!UniqueData.class.isAssignableFrom(clazz)) { - return; - } - - UniqueData dummy = createInstance(clazz, null); - dummyInstances.put(clazz, dummy); - - for (Field field : ReflectionUtils.getFields(clazz)) { - field.setAccessible(true); - - Class type = field.getType(); - - if (Data.class.isAssignableFrom(type)) { - Data data = (Data) field.get(dummy); - Class dataType = data.getDataType(); - - if (UniqueData.class.isAssignableFrom(dataType)) { - extractDependencies((Class) dataType, dependencies); - } - } - } - } - - private List> extractDataDependencies(Class clazz) throws Exception { - UniqueData dummy = createInstance(clazz, null); - - List> dependencies = new ArrayList<>(); - - for (Field field : ReflectionUtils.getFields(clazz)) { - field.setAccessible(true); - - Class type = field.getType(); + logger.debug("Initialized PersistentValue field {}.{} -> {}.{}.{}", uniqueData.getClass().getSimpleName(), pair.field().getName(), schemaName, tableName, columnName); - if (Data.class.isAssignableFrom(type)) { + if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { + proxyPv.setDelegate(newPv); + } else { + pair.field().setAccessible(true); try { - Data data = (Data) field.get(dummy); - dependencies.add(data); + pair.field().set(uniqueData, newPv); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } - - dummyUniqueDataMap.put(dummy.getSchema() + "." + dummy.getTable(), dummy); - - return dependencies; - } - - public synchronized void removeFromCacheIf(Predicate predicate) { - Set keysToRemove = cache.keySet().stream().filter(predicate).collect(Collectors.toSet()); - keysToRemove.forEach(cache::remove); - } - - /** - * Dump the internal cache to the log. - */ - public void dump() { - logger.debug("Dumping cache:"); - for (Map.Entry entry : cache.entrySet()) { - logger.debug("{} -> {}", entry.getKey(), entry.getValue().value()); - } - } - - private void loadUniqueData(Connection connection, UniqueData dummyHolder) throws SQLException { - String sql = "SELECT " + dummyHolder.getIdentifier().getColumn() + " FROM " + dummyHolder.getSchema() + "." + dummyHolder.getTable(); - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - ResultSet resultSet = statement.executeQuery(); - while (resultSet.next()) { - UUID id = resultSet.getObject(dummyHolder.getIdentifier().getColumn(), UUID.class); - UniqueData instance = createInstance(dummyHolder.getClass(), id); - addUniqueData(instance); - } - } - - logger.info("Loaded {} instances of {}", uniqueDataIds.get(dummyHolder.getClass()).size(), dummyHolder.getClass().getName()); } /** - * Get a data object from the cache. + * Submit a blocking task to the priority task queue. + * This method is blocking and will wait for the task to complete before returning. * - * @param data The data object to get - * @param The value type stored in the data object - * @return The data object from the cache - * @throws DataDoesNotExistException If the data object does not exist in the cache + * @param task The task to submit */ - public T get(Data data) throws DataDoesNotExistException { - return get(data.getKey()); + @Blocking + public void submitBlockingTask(ConnectionConsumer task) { + taskQueue.submitTask(task).join(); } /** - * Get a data object from the cache. + * Submit a blocking task to the priority task queue. + * This method is blocking and will wait for the task to complete before returning. * - * @param key The key of the data object to get - * @param The value type stored in the data object - * @return The data object from the cache - * @throws DataDoesNotExistException If the data object does not exist in the cache + * @param task The task to submit */ - @SuppressWarnings("unchecked") - public T get(DataKey key) throws DataDoesNotExistException { - CacheEntry cacheEntry = cache.get(key); - - if (cacheEntry == null) { - throw new DataDoesNotExistException("Data does not exist in cache: " + key); - } - - Object value = cacheEntry.value(); - if (value == NULL_MARKER) { - return null; - } - - return (T) value; + @Blocking + public void submitBlockingTask(ConnectionJedisConsumer task) { + taskQueue.submitTask(task).join(); } - /** - * Get a {@link UniqueData} object from the cache. - * - * @param clazz The class of the data object to get - * @param id The id of the data object to get - * @param The type of data object to get - * @return The data object from the cache, or null if it does not exist - */ - public T get(Class clazz, UUID id) { - if (id == null) { - return null; - } - Map uniqueData = uniqueDataCache.get(clazz); - if (uniqueData != null) { - UniqueData data = uniqueData.get(id); - if (data != null) { - return clazz.cast(data); - } - } - - return null; + public void submitAsyncTask(ConnectionConsumer task) { + taskQueue.submitTask(task); } - public void addUniqueData(UniqueData data) { - logger.trace("Adding unique data to cache: {}({})", data.getClass(), data.getId()); - - CellKey idColumn = new CellKey( - data.getSchema(), - data.getTable(), - data.getIdentifier().getColumn(), - data.getId(), - data.getIdentifier().getColumn() - ); - cache(idColumn, UUID.class, data.getId(), Instant.now(), false); - uniqueDataIds.put(data.getClass(), data.getId()); - uniqueDataCache.computeIfAbsent(data.getClass(), k -> new ConcurrentHashMap<>()).put(data.getId(), data); - } - - public void removeUniqueData(Class clazz, UUID id) { - try { - logger.trace("Removing unique data from cache: {}({})", clazz, id); - uniqueDataIds.remove(clazz, id); - uniqueDataCache.get(clazz).remove(id); - } catch (DataDoesNotExistException e) { - logger.trace("Data does not exist in cache: {}({})", clazz, id); - } + public void submitAsyncTask(ConnectionJedisConsumer task) { + taskQueue.submitTask(task); } - /** - * Add an object to the cache. Null values are permitted. - * If an entry with the given key already exists in the cache, - * it will only be replaced if this value's instant is newer than the existing entry's instant. - * - * @param key The key to cache the value under - * @param valueDataType The type of the value - * @param value The value to cache - * @param instant The instant at which the value was set - */ - public void cache(DataKey key, Class valueDataType, T value, Instant instant, boolean callUpdateHandlers) { - if (value != null && !valueDataType.isInstance(value)) { - throw new IllegalArgumentException("Value is not of the correct type! Expected: " + valueDataType + ", got: " + value.getClass()); - } - if (Primitives.isPrimitive(valueDataType) && !Primitives.getPrimitive(valueDataType).isNullable()) { - Preconditions.checkNotNull(value, "Value cannot be null for primitive type: " + valueDataType); - } - - CacheEntry existing = cache.get(key); + public void insert(UniqueData uniqueData, boolean async) { + ConnectionConsumer task = (connection) -> { + boolean autoCommit = connection.getAutoCommit(); + connection.setAutoCommit(false); + insertIntoSource(connection, uniqueData); + connection.commit(); + connection.setAutoCommit(autoCommit); + }; - if (existing != null && existing.instant().isAfter(instant)) { - logger.trace("Not caching value: {} -> {}, existing entry is newer. {} vs {} (existing)", key, value, instant, existing.instant()); - return; + if (async) { + submitAsyncTask(task); } else { - logger.trace("Caching value: {} -> {}", key, value); - } - - if (existing != null) { - logger.trace("Caching value to replace existing entry. Difference in instants: {} vs {} (existing)", instant, existing.instant()); - } - - cache.put(key, CacheEntry.of(Objects.requireNonNullElse(value, NULL_MARKER), instant)); - - Collection> updateHandlers = valueUpdateHandlers.get(key); - - Object oldValue = existing == null ? null : existing.value() == NULL_MARKER ? null : existing.value(); - Object newValue = value == NULL_MARKER ? null : value; - - if (Objects.equals(oldValue, newValue)) { - return; - } - - if (callUpdateHandlers) { - - for (ValueUpdateHandler updateHandler : updateHandlers) { - try { - updateHandler.unsafeHandle(oldValue, newValue); - } catch (Exception e) { - logger.error("Error handling value update. Key: {}", key, e); - } - } - } - } - - public int getCacheSize() { - return cache.size(); - } - - public void uncache(DataKey key) { - uncache(key, true); - } - - public void uncache(DataKey key, boolean removeUpdateHandlers) { - Collection> updateHandlers = valueUpdateHandlers.get(key); - CacheEntry existing = cache.get(key); - - for (ValueUpdateHandler updateHandler : updateHandlers) { - try { - updateHandler.unsafeHandle(existing.value(), null); - } catch (Exception e) { - logger.error("Error handling value update. Key: {}", key, e); - } - } - - cache.remove(key); - - if (removeUpdateHandlers) { - valueUpdateHandlers.removeAll(key); - } - } - - public void registerValueUpdateHandler(DataKey key, ValueUpdateHandler handler) { - valueUpdateHandlers.put(key, handler); - } - - public void registerSerializer(ValueSerializer serializer) { - if (Primitives.isPrimitive(serializer.getDeserializedType())) { - throw new IllegalArgumentException("Cannot register a serializer for a primitive type"); - } - - if (!Primitives.isPrimitive(serializer.getSerializedType())) { - throw new IllegalArgumentException("Cannot register a ValueSerializer that serializes to a non-primitive type"); + submitBlockingTask(task); } - - for (ValueSerializer v : valueSerializers) { - if (v.getDeserializedType().isAssignableFrom(serializer.getDeserializedType())) { - throw new IllegalArgumentException("A serializer for " + serializer.getDeserializedType() + " is already registered! (" + v.getClass() + ")"); - } - } - - valueSerializers.add(serializer); - } - - public boolean isSupportedType(Class type) { - if (Primitives.isPrimitive(type)) { - return true; - } - - for (ValueSerializer serializer : valueSerializers) { - if (serializer.getDeserializedType().isAssignableFrom(type)) { - return true; - } - } - - return false; - } - - public T deserialize(Class type, Object serialized) { - if (serialized == null) { - return null; - } - if (Primitives.isPrimitive(type)) { - return (T) serialized; - } - - for (ValueSerializer serializer : valueSerializers) { - if (serializer.getDeserializedType().isAssignableFrom(type)) { - return (T) serializer.unsafeDeserialize(serialized); - } + try { + dataAccessor.insertIntoCache(uniqueData); + } catch (SQLException e) { + throw new RuntimeException(e); } - - throw new IllegalArgumentException("No serializer found for type: " + type); } - public Class getSerializedDataType(Class deserializedType) { - if (Primitives.isPrimitive(deserializedType)) { - return deserializedType; - } - - for (ValueSerializer serializer : valueSerializers) { - if (serializer.getDeserializedType().isAssignableFrom(deserializedType)) { - return serializer.getSerializedType(); - } - } - - throw new IllegalArgumentException("No serializer found for type: " + deserializedType); - } + private void insertIntoSource(Connection connection, UniqueData uniqueData) throws SQLException { //todo: async handling and such + UniqueDataMetadata metadata = uniqueData.getMetadata(); - public Object serialize(T deserialized) { - if (deserialized == null) { - return null; + Map> tables = new HashMap<>(); + tables.computeIfAbsent(metadata.schema() + "." + metadata.table(), k -> new ArrayList<>()).add(new PrimaryKey.ColumnValuePair(metadata.idColumn(), uniqueData.getId())); + for (PersistentValue pv : ReflectionUtils.getFieldInstances(uniqueData, PersistentValue.class)) { + tables.computeIfAbsent(pv.getSchema() + "." + pv.getTable(), k -> new ArrayList<>()).add(new PrimaryKey.ColumnValuePair(pv.getColumn(), pv.get())); } - if (Primitives.isPrimitive(deserialized.getClass())) { - return deserialized; - } + for (Map.Entry> entry : tables.entrySet()) { + String fullTableName = entry.getKey(); + List columnValuePairs = entry.getValue(); - for (ValueSerializer serializer : valueSerializers) { - if (serializer.getDeserializedType().isAssignableFrom(deserialized.getClass())) { - return serializer.unsafeSerialize(deserialized); + StringBuilder sqlBuilder = new StringBuilder("INSERT INTO " + fullTableName + " ("); + for (PrimaryKey.ColumnValuePair pair : columnValuePairs) { + sqlBuilder.append(pair.column()).append(", "); } - } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") VALUES ("); + sqlBuilder.append("?, ".repeat(columnValuePairs.size())); + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(")"); - throw new IllegalArgumentException("No serializer found for type: " + deserialized.getClass()); - } - - public Object decode(Class type, String value) { - if (Primitives.isPrimitive(type)) { - return Primitives.decode(type, value); - } + //todo: on conflict : use insertion strategy for this - ValueSerializer serializer = valueSerializers.stream() - .filter(s -> s.getDeserializedType().isAssignableFrom(type)) - .findFirst() - .orElseThrow(); - - Class serializedType = serializer.getSerializedType(); - - return Primitives.decode(serializedType, value); - } - - public UniqueData createInstance(Class clazz, UUID id) { - logger.trace("Creating instance of {} with id {}", clazz, id); - try { - Constructor constructor = clazz.getDeclaredConstructor(DataManager.class, UUID.class); - constructor.setAccessible(true); - return constructor.newInstance(this, id); - } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | - InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - public PostgresListener getPostgresListener() { - return pgListener; - } - - public String getIdColumn(Class clazz) { - if (!dummyInstances.containsKey(clazz)) { - try { - dummyInstances.put(clazz, createInstance(clazz, null)); - } catch (Exception e) { - throw new RuntimeException(e); + String sql = sqlBuilder.toString(); + PreparedStatement preparedStatement = connection.prepareStatement(sql); + for (int i = 0; i < columnValuePairs.size(); i++) { + PrimaryKey.ColumnValuePair pair = columnValuePairs.get(i); + preparedStatement.setObject(i + 1, pair.value()); } + preparedStatement.executeUpdate(); } - return dummyInstances.get(clazz).getIdentifier().getColumn(); - } - - /** - * Block the calling thread until all previously enqueued tasks have been completed - */ - @Blocking - public void flushTaskQueue() { - //This will add a task to the queue and block until it's done - submitBlockingTask(connection -> { - //Ignore - }); - } - - private record InitialPersistentDataWrapper(InitialPersistentValue data, boolean updateCache, Object oldValue) { } } diff --git a/src/main/java/net/staticstudios/data/PersistentValue.java b/src/main/java/net/staticstudios/data/PersistentValue.java index 2e51d70e..0135c485 100644 --- a/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/src/main/java/net/staticstudios/data/PersistentValue.java @@ -1,343 +1,129 @@ package net.staticstudios.data; -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.data.value.InitialPersistentValue; -import net.staticstudios.data.data.value.Value; -import net.staticstudios.data.impl.PersistentValueManager; -import net.staticstudios.data.key.CellKey; -import net.staticstudios.data.util.DeletionStrategy; -import net.staticstudios.data.util.InsertionStrategy; -import net.staticstudios.data.util.ValueUpdate; +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; import net.staticstudios.data.util.ValueUpdateHandler; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.Blocking; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.function.Supplier; +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; + +//todo: keep this as an interface, since we'll allow the data accessor decide what to use. for example are we writing to the real db or the cache. /** - * Represents a value that is stored in a database table. + * A persistent value represents a single cell in a database table. * - * @param The type of data that this value stores. + * @param */ -public class PersistentValue implements Value { - private final String schema; - private final String table; - private final String column; - private final Class dataType; - private final DataHolder holder; - private final String idColumn; - private final DataManager dataManager; - private Supplier defaultValueSupplier; - private DeletionStrategy deletionStrategy; - private InsertionStrategy insertionStrategy; - private int updateInterval = -1; - - private PersistentValue(String schema, String table, String column, String idColumn, Class dataType, DataHolder holder, DataManager dataManager) { - if (!holder.getDataManager().isSupportedType(dataType)) { - throw new IllegalArgumentException("Unsupported data type: " + dataType); - } - - this.schema = schema; - this.table = table; - this.column = column; - this.dataType = dataType; - this.holder = holder; - this.idColumn = idColumn; - this.dataManager = dataManager; - } - - /** - * Create a new {@link PersistentValue} object. - * - * @param holder The holder of this value. - * @param dataType The type of data that this value stores. - * @param column The column in the holder's table that stores this value. - * @param The type of data that this value stores. - * @return The persistent value. - */ - public static PersistentValue of(UniqueData holder, Class dataType, String column) { - PersistentValue pv = new PersistentValue<>(holder.getSchema(), holder.getTable(), column, holder.getRootHolder().getIdentifier().getColumn(), dataType, holder, holder.getDataManager()); - pv.deletionStrategy = DeletionStrategy.CASCADE; - return pv; - } +public interface PersistentValue { - /** - * Create a new {@link PersistentValue} object that stores a value in a different table than its holder. - * By default, this {@link PersistentValue} will have a deletion strategy of {@link DeletionStrategy#NO_ACTION} and - * an insertion strategy of {@link InsertionStrategy#PREFER_EXISTING}. - * - * @param holder The holder of this value. - * @param dataType The type of data that this value stores. - * @param schemaTableColumn The schema.table.column that stores this value. - * @param foreignIdColumn The column in the holder's table that stores the foreign key. - * @param The type of data that this value stores. - * @return The persistent value. - */ - public static PersistentValue foreign(UniqueData holder, Class dataType, String schemaTableColumn, String foreignIdColumn) { - String[] parts = schemaTableColumn.split("\\."); - if (parts.length != 3) { - throw new IllegalArgumentException("Invalid schema.table.column format: " + schemaTableColumn); - } - PersistentValue pv = new PersistentValue<>(parts[0], parts[1], parts[2], foreignIdColumn, dataType, holder, holder.getDataManager()); - pv.deletionStrategy = DeletionStrategy.NO_ACTION; - pv.insertionStrategy = InsertionStrategy.PREFER_EXISTING; - return pv; + static PersistentValue of(UniqueData holder, Class dataType) { + return new ProxyPersistentValue<>(holder, dataType); } - /** - * Create a new {@link PersistentValue} object that stores a value in a different table than its holder. - * By default, this {@link PersistentValue} will have a deletion strategy of {@link DeletionStrategy#NO_ACTION} and - * an insertion strategy of {@link InsertionStrategy#PREFER_EXISTING}. - * - * @param holder The holder of this value. - * @param dataType The type of data that this value stores. - * @param schema The schema that stores the value. - * @param table The table that stores the value. - * @param column The column that stores the value. - * @param foreignIdColumn The column in the holder's table that stores the foreign key. - * @param The type of data that this value stores. - * @return The persistent value. - */ - public static PersistentValue foreign(UniqueData holder, Class dataType, String schema, String table, String column, String foreignIdColumn) { - PersistentValue pv = new PersistentValue<>(schema, table, column, foreignIdColumn, dataType, holder, holder.getDataManager()); - pv.deletionStrategy = DeletionStrategy.NO_ACTION; - pv.insertionStrategy = InsertionStrategy.PREFER_EXISTING; - return pv; - } + String getSchema(); - /** - * Set the initial value for this object - * - * @param value the initial value, or null if there is no initial value - * @return the initial value object - */ - public InitialPersistentValue initial(@Nullable T value) { - return new InitialPersistentValue(this, value); - } + String getTable(); - /** - * Add an update handler to this value. - * Note that update handlers are run asynchronously. - * - * @param updateHandler the update handler - * @return this - */ - @SuppressWarnings("unchecked") - public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { - dataManager.registerValueUpdateHandler(this.getKey(), update -> ThreadUtils.submit(() -> updateHandler.handle((ValueUpdate) update))); - return this; - } + String getColumn(); - /** - * Set the default value for this value. - * - * @param defaultValue the default value, or null if there is no default value - * @return this - */ - public PersistentValue withDefault(@Nullable T defaultValue) { - this.defaultValueSupplier = () -> defaultValue; - return this; - } + PersistentValue onUpdate(ValueUpdateHandler updateHandler); - /** - * Set the default value for this value. - * - * @param defaultValueSupplier the default value supplier, or null if there is no default value - * @return this - */ - public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier) { - this.defaultValueSupplier = defaultValueSupplier; - return this; - } + PersistentValue withDefault(@Nullable T defaultValue); - /** - * Get the default value for this value. - * - * @return the default value, or null if there is no default value - */ - public @Nullable T getDefaultValue() { - return defaultValueSupplier == null ? null : defaultValueSupplier.get(); - } + PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier); - @Override - public CellKey getKey() { - return new CellKey(this); - } + T get(); - public T get() { - return getDataManager().get(this); - } + void set(T value); - public void set(T value) { - PersistentValueManager manager = dataManager.getPersistentValueManager(); - manager.updateCache(this, value); + class ProxyPersistentValue implements PersistentValue { + protected final UniqueData holder; + protected final Class dataType; + private final Deque> updateHandlers = new ConcurrentLinkedDeque<>(); + private @Nullable Supplier<@Nullable T> defaultValueSupplier; + private @Nullable PersistentValue delegate; - Runnable runnable = () -> dataManager.submitAsyncTask(connection -> manager.updateInDatabase(connection, this, value)); - if (updateInterval > 0) { - manager.enqueueRunnable(getKey(), updateInterval, runnable); - } else { - runnable.run(); + public ProxyPersistentValue(UniqueData holder, Class dataType) { + this.holder = holder; + this.dataType = dataType; } - } - - /** - * Set the value of this persistent value. - * This method will block until the value is set in the database. - * - * @param value the value to set - */ - @Blocking - public void setNow(T value) { - PersistentValueManager manager = dataManager.getPersistentValueManager(); - manager.updateCache(this, value); - Runnable runnable = () -> dataManager.submitBlockingTask(connection -> manager.updateInDatabase(connection, this, value)); - - if (updateInterval > 0) { - manager.enqueueRunnable(getKey(), updateInterval, runnable); - } else { - runnable.run(); + public void setDelegate(PersistentValue delegate) { + Preconditions.checkNotNull(delegate, "Delegate cannot be null"); + Preconditions.checkState(this.delegate == null, "Delegate is already set"); + this.delegate = (PersistentValue) delegate; + for (ValueUpdateHandler handler : updateHandlers) { + this.delegate.onUpdate(handler); + } + this.updateHandlers.clear(); + if (this.defaultValueSupplier != null) { + this.delegate.withDefault(this.defaultValueSupplier); + } } - } - - /** - * Get the schema that this value is stored in. - * - * @return the schema - */ - public String getSchema() { - return schema; - } - - /** - * Get the table that this value is stored in. - * - * @return the table - */ - public String getTable() { - return table; - } - /** - * Get the column that this value is stored in. - * - * @return the column - */ - public String getColumn() { - return column; - } - - @Override - public Class getDataType() { - return dataType; - } - - /** - * Get the column that stores the id of this value. - * - * @return the id column - */ - public String getIdColumn() { - return idColumn; - } - - @Override - public DataManager getDataManager() { - return dataManager; - } - - @Override - public DataHolder getHolder() { - return holder; - } - - @Override - public PersistentValue deletionStrategy(DeletionStrategy strategy) { - if (holder.getRootHolder().getSchema().equals(schema) && holder.getRootHolder().getTable().equals(table)) { - throw new IllegalArgumentException("Cannot set deletion strategy for a PersistentValue in the same table as it's holder!"); + @Override + public String getSchema() { + Preconditions.checkState(delegate != null, "Delegate is not set"); + return delegate.getSchema(); } - this.deletionStrategy = strategy; - return this; - } - /** - * Set the insertion strategy for this value. - * - * @param strategy the insertion strategy - * @return this - * @throws IllegalArgumentException if the holder and value are in the same table - */ - public PersistentValue insertionStrategy(InsertionStrategy strategy) { - if (holder.getRootHolder().getSchema().equals(schema) && holder.getRootHolder().getTable().equals(table)) { - throw new IllegalArgumentException("Cannot set deletion strategy for a PersistentValue in the same table as it's holder!"); + @Override + public String getTable() { + Preconditions.checkState(delegate != null, "Delegate is not set"); + return delegate.getTable(); } - this.insertionStrategy = strategy; - return this; - } - @Override - public @NotNull DeletionStrategy getDeletionStrategy() { - return deletionStrategy == null ? DeletionStrategy.NO_ACTION : deletionStrategy; - } - - /** - * Get the insertion strategy for this value. - * - * @return the insertion strategy - */ - public @NotNull InsertionStrategy getInsertionStrategy() { - return insertionStrategy == null ? InsertionStrategy.OVERWRITE_EXISTING : insertionStrategy; - } + @Override + public String getColumn() { + Preconditions.checkState(delegate != null, "Delegate is not set"); + return delegate.getColumn(); + } - /** - * Set the update interval for this value. - * When set to an integer greater than 0, the database will be updated every n milliseconds, provided the value has changed. - * Further, if the value has changed 10 times, only the 10th change will be written to the database. - * - * @param updateInterval the update interval - * @return this - */ - public PersistentValue updateInterval(int updateInterval) { - this.updateInterval = updateInterval; - return this; - } + @Override + public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { + Preconditions.checkNotNull(updateHandler, "Update handler cannot be null"); - /** - * Get the update interval for this value. - * - * @return the update interval - */ - public int getUpdateInterval() { - return updateInterval; - } + if (delegate != null) { + delegate.onUpdate(updateHandler); + } else { + this.updateHandlers.add(updateHandler); + } - @Override - public int hashCode() { - return getKey().hashCode(); - } + return this; + } - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; + @Override + public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier) { + if (delegate != null) { + delegate.withDefault(defaultValueSupplier); + } else { + this.defaultValueSupplier = defaultValueSupplier; + } + return this; } - if (!(obj instanceof PersistentValue other)) { - return false; + @Override + public PersistentValue withDefault(@Nullable T defaultValue) { + return withDefault(() -> defaultValue); } - return getKey().equals(other.getKey()); - } + @Override + public T get() { + if (delegate != null) { + return delegate.get(); + } + throw new UnsupportedOperationException("Not implemented"); + } - @Override - public String toString() { - return "PersistentValue{" + - "schema='" + schema + '\'' + - ", table='" + table + '\'' + - ", column='" + column + '\'' + - '}'; + @Override + public void set(T value) { + if (delegate != null) { + delegate.set(value); + return; + } + throw new UnsupportedOperationException("Not implemented"); + } } } diff --git a/src/main/java/net/staticstudios/data/Relation.java b/src/main/java/net/staticstudios/data/Relation.java new file mode 100644 index 00000000..226a4c60 --- /dev/null +++ b/src/main/java/net/staticstudios/data/Relation.java @@ -0,0 +1,4 @@ +package net.staticstudios.data; + +public interface Relation { +} diff --git a/src/main/java/net/staticstudios/data/UniqueData.java b/src/main/java/net/staticstudios/data/UniqueData.java index e6dde3cc..15a0e04c 100644 --- a/src/main/java/net/staticstudios/data/UniqueData.java +++ b/src/main/java/net/staticstudios/data/UniqueData.java @@ -1,157 +1,30 @@ package net.staticstudios.data; +import net.staticstudios.data.parse.UniqueDataMetadata; +import net.staticstudios.data.util.PrimaryKey; -import com.google.common.base.Preconditions; -import net.staticstudios.data.data.Data; -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.data.collection.SimplePersistentCollection; -import net.staticstudios.data.data.value.Value; -import net.staticstudios.data.key.UniqueIdentifier; -import net.staticstudios.data.util.ReflectionUtils; - -import java.lang.reflect.Field; -import java.util.Objects; +import java.util.List; import java.util.UUID; -/** - * Represents a unique data object that is stored in the database and contains other data objects. - */ -public abstract class UniqueData implements DataHolder { +public abstract class UniqueData implements PrimaryKey { private final DataManager dataManager; - private final String schema; - private final String table; - private final UniqueIdentifier identifier; - - /** - * Create a new unique data object. - * The id column is assumed to be "id". - * See {@link #UniqueData(DataManager, String, String, String, UUID)} if the id column is different. - * - * @param dataManager the data manager responsible for this data object - * @param schema the schema of the table - * @param table the table name - * @param id the id of the data object - */ - protected UniqueData(DataManager dataManager, String schema, String table, UUID id) { - this(dataManager, schema, table, "id", id); - } - /** - * Create a new unique data object. - * - * @param dataManager the data manager responsible for this data object - * @param schema the schema of the table - * @param table the table name - * @param idColumn the name of the column that stores the id - * @param id the id of the data object - */ - protected UniqueData(DataManager dataManager, String schema, String table, String idColumn, UUID id) { - Preconditions.checkArgument(dataManager.get(this.getClass(), id) == null, "Data with id %s already exists", id); + public UniqueData(DataManager dataManager) { this.dataManager = dataManager; - this.schema = schema; - this.table = table; - this.identifier = UniqueIdentifier.of(idColumn, id); - } - - /** - * Get the id of this data object. - * - * @return the id - */ - public UUID getId() { - return identifier.getId(); - } - - /** - * Get the table that this data object is stored in. - * - * @return the table name - */ - public String getTable() { - return table; - } - - /** - * Get the schema that this data object is stored in. - * - * @return the schema name - */ - public String getSchema() { - return schema; } - /** - * Get the unique identifier of this data object. - * - * @return the unique identifier - */ - public UniqueIdentifier getIdentifier() { - return identifier; - } + public abstract UUID getId(); - @Override public DataManager getDataManager() { return dataManager; } @Override - public UniqueData getRootHolder() { - return this; - } - - @Override - public final boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - UniqueData that = (UniqueData) obj; - return Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - UUID id = getId(); - return id != null ? id.hashCode() : 0; + public List getWhereClause() { + return List.of(new ColumnValuePair(getMetadata().idColumn(), getId())); } - @Override - public String toString() { - StringBuilder sb = new StringBuilder(this.getClass().getSimpleName()); - sb.append("{"); - sb.append("id=").append(getId()).append("(").append(getSchema()).append(".").append(getTable()).append(".").append(identifier.getColumn()).append(")"); - - for (Field field : ReflectionUtils.getFields(this.getClass())) { - field.setAccessible(true); - Class fieldClass = field.getType(); - - if (!Data.class.isAssignableFrom(fieldClass)) { - continue; - } - - try { - Data data = (Data) field.get(this); - - if (data instanceof Value value) { - sb.append(", "); - sb.append(field.getName()).append("=").append(value.get()); - } else if (data instanceof SimplePersistentCollection collection) { - sb.append(", "); - sb.append(field.getName()).append("=Collection<").append(collection.getDataType().getSimpleName()).append(">{size=").append(collection.size()).append("}"); - } else if (data instanceof Reference reference) { - sb.append(", "); - UniqueData ref = reference.get(); - if (ref == null) { - sb.append(field.getName()).append("=null"); - } else { - sb.append(field.getName()).append("=").append(ref.getClass().getSimpleName()).append("{id=").append(ref.getId()).append("}"); - } - } - - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - sb.append("}"); - - return sb.toString(); + public final UniqueDataMetadata getMetadata() { + return dataManager.getMetadata(this.getClass()); } } diff --git a/src/main/java/net/staticstudios/data/impl/sqlite/SQLiteDataAccessor.java b/src/main/java/net/staticstudios/data/impl/sqlite/SQLiteDataAccessor.java new file mode 100644 index 00000000..2f4e838e --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/sqlite/SQLiteDataAccessor.java @@ -0,0 +1,128 @@ +package net.staticstudios.data.impl.sqlite; + +import net.staticstudios.data.DataAccessor; +import net.staticstudios.data.PersistentValue; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.SQLBuilder; +import net.staticstudios.data.parse.UniqueDataMetadata; +import net.staticstudios.data.util.PrimaryKey; +import net.staticstudios.data.util.ReflectionUtils; +import org.intellij.lang.annotations.Language; +import org.sqlite.SQLiteConfig; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SQLiteDataAccessor implements DataAccessor { //todo: we need data transformers since sqlite only supports a few types natively + private final SQLBuilder sqlBuilder; + private final String jdbcUrl; + private final SQLiteConfig config = new SQLiteConfig(); + private final ThreadLocal threadConnection = new ThreadLocal<>(); + private final ThreadLocal> threadPreparedStatementCache = new ThreadLocal<>(); + + public SQLiteDataAccessor(SQLBuilder sqlBuilder) { + this.sqlBuilder = sqlBuilder; + String filePath = "static-data-cache.db"; + + File cacheFile = new File(filePath + ".db"); + if (cacheFile.exists()) { + cacheFile.delete(); + } + config.setSharedCache(true); + config.setJournalMode(SQLiteConfig.JournalMode.OFF); + config.setSynchronous(SQLiteConfig.SynchronousMode.OFF); + config.setTempStore(SQLiteConfig.TempStore.MEMORY); + this.jdbcUrl = "jdbc:sqlite:" + filePath; + + //todo: using thread utils delete the file on exit + } + + private Connection getConnection() throws SQLException { + Connection connection = threadConnection.get(); + if (connection == null) { + connection = DriverManager.getConnection(jdbcUrl); + connection.setAutoCommit(false); + threadConnection.set(connection); + } + return connection; + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + Connection connection = getConnection(); + Map preparedStatementCache = threadPreparedStatementCache.get(); + if (preparedStatementCache == null) { + preparedStatementCache = new HashMap<>(); + threadPreparedStatementCache.set(preparedStatementCache); + } + + PreparedStatement preparedStatement = preparedStatementCache.get(sql); + if (preparedStatement == null) { + preparedStatement = connection.prepareStatement(sql); + preparedStatementCache.put(sql, preparedStatement); + } + + return preparedStatement; + } + + @Override + public PersistentValue createPersistentValue(PrimaryKey primaryKey, Class dataType, String schema, String table, String dataColumn) { + return new SQLitePersistentValue<>(this, primaryKey, dataType, schema, table, dataColumn); + } + + //todo: set & get should be here instead of in the pv impl + + public void insert(Connection connection, UniqueData uniqueData) throws SQLException { + insertIntoCache(uniqueData); + } + + @Override + public void insertIntoCache(UniqueData uniqueData) throws SQLException { + UniqueDataMetadata metadata = uniqueData.getMetadata(); + + Map> tables = new HashMap<>(); + tables.computeIfAbsent(metadata.schema() + "." + metadata.table(), k -> new ArrayList<>()).add(new PrimaryKey.ColumnValuePair(metadata.idColumn(), uniqueData.getId())); + for (PersistentValue pv : ReflectionUtils.getFieldInstances(uniqueData, PersistentValue.class)) { + + tables.computeIfAbsent(pv.getSchema() + "." + pv.getTable(), k -> new ArrayList<>()).add(new PrimaryKey.ColumnValuePair(pv.getColumn(), pv.get())); + } + + for (Map.Entry> entry : tables.entrySet()) { + String fullTableName = entry.getKey(); + List columnValuePairs = entry.getValue(); + + StringBuilder sqlBuilder = new StringBuilder("INSERT INTO " + fullTableName + " ("); + for (PrimaryKey.ColumnValuePair pair : columnValuePairs) { + sqlBuilder.append(pair.column()).append(", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") VALUES ("); + sqlBuilder.append("?, ".repeat(columnValuePairs.size())); + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(")"); + + //todo: on conflict : use insertion strategy for this + + @Language("SQL") String sql = sqlBuilder.toString(); + PreparedStatement preparedStatement = prepareStatement(sql); + for (int i = 0; i < columnValuePairs.size(); i++) { + PrimaryKey.ColumnValuePair pair = columnValuePairs.get(i); + preparedStatement.setObject(i + 1, pair.value()); + } + preparedStatement.executeUpdate(); + } + } + + +// StringBuilder sqlBuilder = new StringBuilder("INSERT INTO " + +} + + +//todo: maintain a buffer of what to send the the real db, and then collapse similar prepared statements into one so we can batch them. \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/impl/sqlite/SQLitePersistentValue.java b/src/main/java/net/staticstudios/data/impl/sqlite/SQLitePersistentValue.java new file mode 100644 index 00000000..600aa515 --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/sqlite/SQLitePersistentValue.java @@ -0,0 +1,123 @@ +package net.staticstudios.data.impl.sqlite; + +import com.google.common.base.Supplier; +import net.staticstudios.data.PersistentValue; +import net.staticstudios.data.util.PrimaryKey; +import net.staticstudios.data.util.ValueUpdateHandler; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Nullable; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class SQLitePersistentValue implements PersistentValue { + private final SQLiteDataAccessor dataAccessor; + private final PrimaryKey primaryKey; + private final Class dataType; + private final String schema; + private final String table; + private final String dataColumn; + + //todo: all methods should update the real db + public SQLitePersistentValue(SQLiteDataAccessor dataAccessor, PrimaryKey primaryKey, Class dataType, String schema, String table, String dataColumn) { + this.dataAccessor = dataAccessor; + this.primaryKey = primaryKey; + this.dataType = dataType; + this.schema = schema; + this.table = table; + this.dataColumn = dataColumn; + } + + @Override + public String getSchema() { + return schema; + } + + @Override + public String getTable() { + return table; + } + + @Override + public String getColumn() { + return dataColumn; + } + + @Override + public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { + return null; + } + + @Override + public PersistentValue withDefault(@Nullable T defaultValue) { + return null; + } + + @Override + public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier) { + return null; + } + + @Override + public T get() { + try { + StringBuilder sqlBuilder = new StringBuilder("SELECT " + dataColumn + " FROM " + schema + "_" + table + " WHERE "); + for (PrimaryKey.ColumnValuePair pair : primaryKey.getWhereClause()) { + sqlBuilder.append(pair.column()).append(" = ? AND "); + } + + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + PreparedStatement preparedStatement = dataAccessor.prepareStatement(sql); + + for (int i = 0; i < primaryKey.getWhereClause().size(); i++) { + PrimaryKey.ColumnValuePair pair = primaryKey.getWhereClause().get(i); + preparedStatement.setObject(i + 1, pair.value()); + } + + ResultSet rs = preparedStatement.executeQuery(); + Object rawValue = null; + if (rs.next()) { + rawValue = rs.getObject(dataColumn); + } + if (rawValue == null) { + return null; + } + //todo: deserialize + return (T) rawValue; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void set(T value) { + try { + Object serializedValue = value; //todo: serialize + StringBuilder sqlBuilder = new StringBuilder("INSERT INTO " + schema + "_" + table + " ("); + for (PrimaryKey.ColumnValuePair pair : primaryKey.getWhereClause()) { + sqlBuilder.append(pair.column()).append(", "); + } + sqlBuilder.append(dataColumn).append(") VALUES ("); + sqlBuilder.append("?, ".repeat(primaryKey.getWhereClause().size())); + sqlBuilder.append("?) ON CONFLICT("); + for (PrimaryKey.ColumnValuePair pair : primaryKey.getWhereClause()) { + sqlBuilder.append(pair.column()).append(", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") DO UPDATE SET ").append(dataColumn).append(" = excluded.").append(dataColumn); + @Language("SQL") String sql = sqlBuilder.toString(); + PreparedStatement preparedStatement = dataAccessor.prepareStatement(sql); + int index = 1; + for (PrimaryKey.ColumnValuePair pair : primaryKey.getWhereClause()) { + preparedStatement.setObject(index++, pair.value()); + } + preparedStatement.setObject(index, serializedValue); + + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/net/staticstudios/data/parse/Column.java b/src/main/java/net/staticstudios/data/parse/Column.java new file mode 100644 index 00000000..e6ac5de8 --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/Column.java @@ -0,0 +1,18 @@ +package net.staticstudios.data.parse; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//todo: annotations would break compatability, but they make static analysis easier for meta data parsing and for building sql +@Retention(RetentionPolicy.RUNTIME) +public @interface Column { + String value(); + + String schema() default ""; + + String table() default ""; + + boolean index() default false; //todo: this + + boolean nullable() default false; //todo: this +} diff --git a/src/main/java/net/staticstudios/data/parse/Data.java b/src/main/java/net/staticstudios/data/parse/Data.java new file mode 100644 index 00000000..4582dacc --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/Data.java @@ -0,0 +1,14 @@ +package net.staticstudios.data.parse; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +//todo: annotations would break compatability, but they make static analysis easier for meta data parsing and for building sql +@Retention(RetentionPolicy.RUNTIME) //todo: we should support env variables in here as well. +public @interface Data { + String idColumn(); + + String schema(); + + String table(); +} diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java new file mode 100644 index 00000000..55cf5f9d --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -0,0 +1,110 @@ +package net.staticstudios.data.parse; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.Relation; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.util.ReflectionUtils; +import net.staticstudios.data.util.ValueUtils; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class SQLBuilder { + private final Map schemas; + + public SQLBuilder() { + this.schemas = new HashMap<>(); + } + + public void parse(Class clazz) { + Preconditions.checkNotNull(clazz, "Class cannot be null"); + + Set> visited = walk(clazz); + for (Class visitedClass : visited) { + parseIndividual(visitedClass); + } + } + + public String asSQL() { //todo: create table if not exist then use alter statements + StringBuilder sb = new StringBuilder(); + for (SQLSchema schema : schemas.values()) { + sb.append("CREATE SCHEMA IF NOT EXISTS ").append(schema.getName()).append(";\n"); + for (SQLTable table : schema.getTables()) { + sb.append("CREATE TABLE IF NOT EXISTS ").append(schema.getName()).append(".").append(table.getName()).append(" (\n"); + for (SQLColumn column : table.getColumns()) { + sb.append(" ").append(column.getName()).append(" ?????").append(column.isNullable() ? "" : " NOT NULL"); + if (column.isIndexed()) { + sb.append(" INDEXED"); + } + sb.append(",\n"); + } + sb.setLength(sb.length() - 2); + sb.append("\n);\n"); + } + } + + return sb.toString(); + } + + private Set> walk(Class clazz) { + Preconditions.checkNotNull(clazz, "Class cannot be null"); + Preconditions.checkArgument(UniqueData.class.isAssignableFrom(clazz), "Class " + clazz.getName() + " is not a UniqueData type"); + + Set> visited = new java.util.HashSet<>(); + walk(clazz, visited); + return visited; + } + + private void walk(Class clazz, Set> visited) { + if (visited.contains(clazz)) { + return; + } + visited.add(clazz); + + for (Field field : ReflectionUtils.getFields(clazz)) { + if (Relation.class.isAssignableFrom(field.getType())) { + Class genericType = ReflectionUtils.getGenericType(field); + Preconditions.checkNotNull(genericType, "Generic type for field " + field.getName() + " in class " + clazz.getName() + " is null"); + Preconditions.checkArgument(UniqueData.class.isAssignableFrom(genericType), "Field " + field.getName() + " in class " + clazz.getName() + " is not a UniqueData type"); + Class relatedClass = (Class) genericType; + + walk(relatedClass, visited); + } + } + } + + private void parseIndividual(Class 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)) { + if (!field.isAnnotationPresent(Column.class)) { + continue; + } + + Column column = field.getAnnotation(Column.class); + Preconditions.checkNotNull(column, "Column annotation is null for field " + field.getName() + " in class " + clazz.getName()); + String schemaName = column.schema().isEmpty() ? ValueUtils.parseValue(dataAnnotation.schema()) : ValueUtils.parseValue(column.schema()); + String tableName = column.table().isEmpty() ? ValueUtils.parseValue(dataAnnotation.table()) : ValueUtils.parseValue(column.table()); + String columnName = ValueUtils.parseValue(column.value()); + + SQLSchema schema = schemas.computeIfAbsent(schemaName, SQLSchema::new); + SQLTable table = schema.getTable(tableName); + if (table == null) { + table = new SQLTable(schema, tableName); + schema.addTable(table); + } + + //todo: grab the type of the PV to determine the SQL type + SQLColumn sqlColumn = new SQLColumn(table, columnName, column.nullable(), column.index()); + table.addColumn(sqlColumn); + } + } +} diff --git a/src/main/java/net/staticstudios/data/parse/SQLColumn.java b/src/main/java/net/staticstudios/data/parse/SQLColumn.java new file mode 100644 index 00000000..22ff60c4 --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/SQLColumn.java @@ -0,0 +1,33 @@ +package net.staticstudios.data.parse; + +public class SQLColumn { + private final SQLTable table; + private final String name; + private final boolean nullable; + private final boolean indexed; + + public SQLColumn(SQLTable table, String name, boolean nullable, boolean indexed) { + this.table = table; + this.name = name; + this.nullable = nullable; + this.indexed = indexed; + } + + public SQLTable getTable() { + return table; + } + + public String getName() { + return name; + } + + public boolean isNullable() { + return nullable; + } + + public boolean isIndexed() { + return indexed; + } + + //todo: equals, hashCode, toString methods if needed +} diff --git a/src/main/java/net/staticstudios/data/parse/SQLSchema.java b/src/main/java/net/staticstudios/data/parse/SQLSchema.java new file mode 100644 index 00000000..6714a109 --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/SQLSchema.java @@ -0,0 +1,41 @@ +package net.staticstudios.data.parse; + +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class SQLSchema { + private final String name; + private final Map tables; + + public SQLSchema(String name) { + this.name = name; + this.tables = new HashMap<>(); + } + + public String getName() { + return name; + } + + public Set getTables() { + return new HashSet<>(tables.values()); + } + + public @Nullable SQLTable getTable(String tableName) { + return tables.get(tableName); + } + + public void addTable(SQLTable table) { + if (table.getSchema() != this) { + throw new IllegalArgumentException("Table does not belong to this schema"); + } + if (tables.containsKey(table.getName())) { + throw new IllegalArgumentException("Table with name " + table.getName() + " already exists in schema " + name); + } + + tables.put(table.getName(), table); + } +} diff --git a/src/main/java/net/staticstudios/data/parse/SQLTable.java b/src/main/java/net/staticstudios/data/parse/SQLTable.java new file mode 100644 index 00000000..6de02b36 --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -0,0 +1,47 @@ +package net.staticstudios.data.parse; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class SQLTable { + private final SQLSchema schema; + private final String name; + private final Map columns; + + public SQLTable(SQLSchema schema, String name) { + this.schema = schema; + this.name = name; + this.columns = new HashMap<>(); + } + + public SQLSchema getSchema() { + return schema; + } + + public String getName() { + return name; + } + + public Set getColumns() { + return new HashSet<>(columns.values()); + } + + public @Nullable SQLColumn getColumn(String columnName) { + return columns.get(columnName); + } + + public void addColumn(SQLColumn column) { + if (column.getTable() != this) { + throw new IllegalArgumentException("Column does not belong to this table"); + } + Preconditions.checkNotNull(column, "Column cannot be null"); + SQLColumn existingColumn = columns.get(column.getName()); + if (existingColumn != null && !Objects.equals(existingColumn, column)) { + throw new IllegalArgumentException("Column with name " + column.getName() + " already exists in table " + name + " in schema " + schema.getName() + " and is different from the one being added"); + } + + columns.put(column.getName(), column); + } +} diff --git a/src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java b/src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java new file mode 100644 index 00000000..41d94f6a --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.parse; + +public record UniqueDataMetadata(String schema, String table, String idColumn) { + +} diff --git a/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java b/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java new file mode 100644 index 00000000..d36f3336 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.util; + +public class EnvironmentVariableAccessor { + public String getEnv(String name) { + return System.getenv(name); + } +} diff --git a/src/main/java/net/staticstudios/data/util/FieldInstancePair.java b/src/main/java/net/staticstudios/data/util/FieldInstancePair.java new file mode 100644 index 00000000..28690668 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/FieldInstancePair.java @@ -0,0 +1,8 @@ +package net.staticstudios.data.util; + +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Field; + +public record FieldInstancePair(@NotNull Field field, T instance) { +} diff --git a/src/main/java/net/staticstudios/data/util/PrimaryKey.java b/src/main/java/net/staticstudios/data/util/PrimaryKey.java new file mode 100644 index 00000000..b875f7dc --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/PrimaryKey.java @@ -0,0 +1,11 @@ +package net.staticstudios.data.util; + +import java.util.List; + +public interface PrimaryKey { + + List getWhereClause(); + + record ColumnValuePair(String column, Object value) { + } +} diff --git a/src/main/java/net/staticstudios/data/util/ReflectionUtils.java b/src/main/java/net/staticstudios/data/util/ReflectionUtils.java index c9b1eba9..ce163568 100644 --- a/src/main/java/net/staticstudios/data/util/ReflectionUtils.java +++ b/src/main/java/net/staticstudios/data/util/ReflectionUtils.java @@ -1,5 +1,7 @@ package net.staticstudios.data.util; +import org.jetbrains.annotations.Nullable; + import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -21,4 +23,53 @@ public static List getFields(Class clazz) { return fields; } + + /** + * Get all fields from this class AND its superclasses of a specific type + * + * @param clazz The class to get fields from + * @param fieldType The type of fields to get + * @return A list of fields + */ + public static List getFields(Class clazz, Class fieldType) { + List fields = getFields(clazz); + fields.removeIf(field -> !fieldType.isAssignableFrom(field.getType())); + return fields; + } + + public static List> getFieldInstancePairs(Object instance, Class fieldType) { + List fields = getFields(instance.getClass(), fieldType); + List> instances = new ArrayList<>(); + for (Field field : fields) { + field.setAccessible(true); + try { + T value = fieldType.cast(field.get(instance)); + if (value != null) { + instances.add(new FieldInstancePair<>(field, value)); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + return instances; + } + + public static List getFieldInstances(Object instance, Class fieldType) { + List> pairs = getFieldInstancePairs(instance, fieldType); + List instances = new ArrayList<>(); + for (FieldInstancePair pair : pairs) { + instances.add(pair.instance()); + } + return instances; + } + + + 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) { + return (Class) parameterizedType.getRawType(); + } + return null; + } } diff --git a/src/main/java/net/staticstudios/data/util/UUIDUtils.java b/src/main/java/net/staticstudios/data/util/UUIDUtils.java new file mode 100644 index 00000000..2eacd5c5 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/UUIDUtils.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.util; + +import java.nio.ByteBuffer; +import java.util.UUID; + +public class UUIDUtils { + + public static byte[] toBytes(UUID uuid) { + ByteBuffer bb = ByteBuffer.allocate(16) + .putLong(uuid.getMostSignificantBits()) + .putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } + + public static UUID fromBytes(byte[] bytes) { + ByteBuffer bb = ByteBuffer.wrap(bytes); + long mostSigBits = bb.getLong(); + long leastSigBits = bb.getLong(); + return new UUID(mostSigBits, leastSigBits); + } +} diff --git a/src/main/java/net/staticstudios/data/util/ValueUtils.java b/src/main/java/net/staticstudios/data/util/ValueUtils.java new file mode 100644 index 00000000..f611e81c --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/ValueUtils.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.util; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.VisibleForTesting; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ValueUtils { + private static final Pattern ENVIRONMENT_VARIABLE_PATTERN = Pattern.compile("\\$\\{([a-zA-Z0-9_]+)}"); + @VisibleForTesting + public static EnvironmentVariableAccessor ENVIRONMENT_VARIABLE_ACCESSOR = new EnvironmentVariableAccessor(); + + public static String parseValue(String encoded) { + Preconditions.checkNotNull(encoded, "Encoded value cannot be null"); + Matcher matcher = ENVIRONMENT_VARIABLE_PATTERN.matcher(encoded); + if (matcher.matches()) { + String varName = matcher.group(1); + String value = ENVIRONMENT_VARIABLE_ACCESSOR.getEnv(varName); + Preconditions.checkArgument(value != null, "Environment variable " + varName + " is not set"); + return value; + } else { + return encoded; + } + } +} diff --git a/src/main/java/net/staticstudios/data/CachedValue.java b/src/og/java/net/staticstudios/data/CachedValue.java similarity index 100% rename from src/main/java/net/staticstudios/data/CachedValue.java rename to src/og/java/net/staticstudios/data/CachedValue.java diff --git a/src/og/java/net/staticstudios/data/DataManager.java b/src/og/java/net/staticstudios/data/DataManager.java new file mode 100644 index 00000000..a02b1483 --- /dev/null +++ b/src/og/java/net/staticstudios/data/DataManager.java @@ -0,0 +1,1149 @@ +package net.staticstudios.data; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import net.staticstudios.data.data.Data; +import net.staticstudios.data.data.InitialValue; +import net.staticstudios.data.data.collection.PersistentManyToManyCollection; +import net.staticstudios.data.data.collection.PersistentUniqueDataCollection; +import net.staticstudios.data.data.collection.SimplePersistentCollection; +import net.staticstudios.data.data.value.InitialCachedValue; +import net.staticstudios.data.data.value.InitialPersistentValue; +import net.staticstudios.data.data.value.Value; +import net.staticstudios.data.impl.CachedValueManager; +import net.staticstudios.data.impl.PersistentCollectionManager; +import net.staticstudios.data.impl.PersistentValueManager; +import net.staticstudios.data.impl.pg.PostgresListener; +import net.staticstudios.data.impl.pg.PostgresOperation; +import net.staticstudios.data.key.CellKey; +import net.staticstudios.data.key.DataKey; +import net.staticstudios.data.key.DatabaseKey; +import net.staticstudios.data.primative.Primitives; +import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.data.util.*; +import net.staticstudios.utils.ShutdownStage; +import net.staticstudios.utils.ThreadUtils; +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.Jedis; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +/** + * Manages all data operations. + */ +public class DataManager extends SQLLogger { + private static final Object NULL_MARKER = new Object(); + private final Logger logger = LoggerFactory.getLogger(DataManager.class); + private final Map cache; + private final Multimap> valueUpdateHandlers; + private final Multimap, UUID> uniqueDataIds; + private final Map, Map> uniqueDataCache; + private final Multimap> dummyValueMap; + private final Multimap> dummySimplePersistentCollectionMap; + private final Multimap> dummyPersistentManyToManyCollectionMap; + private final Multimap dummyUniqueDataMap; + private final Map, UniqueData> dummyInstances; + private final Set> loadedDependencies = new HashSet<>(); + private final List> valueSerializers; + private final PostgresListener pgListener; + private final String applicationName; + private final PersistentValueManager persistentValueManager; + private final PersistentCollectionManager persistentCollectionManager; + private final CachedValueManager cachedValueManager; + + private final TaskQueue taskQueue; + + + /** + * Create a new data manager. + * + * @param dataSourceConfig the data source configuration + */ + public DataManager(DataSourceConfig dataSourceConfig) { + this.applicationName = "static_data_manager-" + UUID.randomUUID(); + this.cache = new ConcurrentHashMap<>(); + this.valueUpdateHandlers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); + this.uniqueDataCache = new ConcurrentHashMap<>(); + this.dummyValueMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); + this.dummySimplePersistentCollectionMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); + this.dummyPersistentManyToManyCollectionMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); + this.dummyUniqueDataMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); + this.dummyInstances = new ConcurrentHashMap<>(); + this.uniqueDataIds = Multimaps.synchronizedSetMultimap(HashMultimap.create()); + this.valueSerializers = new CopyOnWriteArrayList<>(); + + pgListener = new PostgresListener(this, dataSourceConfig); + + pgListener.addHandler(notification -> { //Call insert handler first so that we can access the data in the other handlers + if (notification.getOperation() == PostgresOperation.INSERT) { + logger.trace("Handing INSERT operation"); + Collection dummyUniqueData = dummyUniqueDataMap.get(notification.getSchema() + "." + notification.getTable()); + + for (UniqueData dummy : dummyUniqueData) { + String identifierColumn = dummy.getIdentifier().getColumn(); + UUID id = UUID.fromString(notification.getData().newDataValueMap().get(identifierColumn)); + + UniqueData instance = createInstance(dummy.getClass(), id); + addUniqueData(instance); + } + } + }); + + this.persistentValueManager = new PersistentValueManager(this, pgListener); + this.persistentCollectionManager = new PersistentCollectionManager(this, pgListener); + this.cachedValueManager = new CachedValueManager(this, dataSourceConfig); + + pgListener.addHandler(notification -> { //Call delete handler last so that we can access the data in the other handlers + if (notification.getOperation() == PostgresOperation.DELETE) { + logger.trace("Handling DELETE operation"); + Collection dummyUniqueData = dummyUniqueDataMap.get(notification.getSchema() + "." + notification.getTable()); + + for (UniqueData dummy : dummyUniqueData) { + String identifierColumn = dummy.getIdentifier().getColumn(); + UUID id = UUID.fromString(notification.getData().oldDataValueMap().get(identifierColumn)); + removeUniqueData(dummy.getClass(), id); + } + } + }); + + + this.taskQueue = new TaskQueue(dataSourceConfig, applicationName); + + ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, () -> { + cache.clear(); + uniqueDataCache.clear(); + uniqueDataIds.clear(); + }); + } + + /** + * Submit a blocking task to the priority task queue. + * This method is blocking and will wait for the task to complete before returning. + * + * @param task The task to submit + */ + @Blocking + public void submitBlockingTask(ConnectionConsumer task) { + taskQueue.submitTask(task).join(); + } + + public void submitAsyncTask(ConnectionConsumer task) { + taskQueue.submitTask(task) + .exceptionally(e -> { + logger.error("Error submitting async task", e); + return null; + }); + } + + /** + * Submit a blocking task to the priority task queue. + * This method is blocking and will wait for the task to complete before returning. + * + * @param task The task to submit + */ + @Blocking + public void submitBlockingTask(ConnectionJedisConsumer task) { + taskQueue.submitTask(task).join(); + } + + public void submitAsyncTask(ConnectionJedisConsumer task) { + taskQueue.submitTask(task); + } + + public PersistentValueManager getPersistentValueManager() { + return persistentValueManager; + } + + public PersistentCollectionManager getPersistentCollectionManager() { + return persistentCollectionManager; + } + + public CachedValueManager getCachedValueManager() { + return cachedValueManager; + } + + public String getApplicationName() { + return applicationName; + } + + public Collection> getDummyValues(String schemaTable) { + return dummyValueMap.get(schemaTable); + } + + public Collection getDummyUniqueData(String schemaTable) { + return dummyUniqueDataMap.get(schemaTable); + } + + public UniqueData getDummyInstance(Class clazz) { + return dummyInstances.get(clazz); + } + + public Collection> getDummyPersistentCollections(String schemaTable) { + return dummySimplePersistentCollectionMap.get(schemaTable); + } + + public Collection> getDummyPersistentManyToManyCollection(String schemaTable) { + return dummyPersistentManyToManyCollectionMap.get(schemaTable); + } + + public Collection> getAllDummyPersistentManyToManyCollections() { + return new HashSet<>(dummyPersistentManyToManyCollectionMap.values()); + } + + /** + * Load all the data for a given class from the datasource(s) into the cache. + * This method will recursively load all dependencies of the given class. + * Note that calling this method registers a specific data type. + * Without calling this method, the data manager will have no knowledge of the data type. + * This should be called at the start of the application to ensure all data types are registered. + * This method is inherently blocking. + * + * @param clazz The class to load data for + * @param The type of data to load + * @return A list of all the loaded data + */ + @Blocking + public List loadAll(Class clazz) { + logger.debug("Registering: {}", clazz.getName()); + try { + // A dependency is just another UniqueData that is referenced in some way. + // This clazz is also treated as a dependency, these will all be loaded at the same time + Set> dependencies = new HashSet<>(); + extractDependencies(clazz, dependencies); + dependencies.removeIf(loadedDependencies::contains); + + List dummyHolders = new ArrayList<>(); + Multimap dummyDatabaseKeys = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); + Multimap> dummySimplePersistentCollections = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); + Multimap> dummyPersistentManyToManyCollections = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); + Multimap> dummyCachedValues = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); + + + for (Class dependency : dependencies) { + // A data dependency is some data field on one of our dependencies + List> dataDependencies = extractDataDependencies(dependency); + + dummyHolders.add(createInstance(dependency, null)); + + for (Data data : dataDependencies) { + DataKey key = data.getKey(); + if (key instanceof DatabaseKey dbKey) { + dummyDatabaseKeys.put(data.getHolder().getRootHolder(), dbKey); + } + + // This is our first time seeing this value, so we add it to the map for later use + if (data instanceof PersistentValue value) { + dummyValueMap.put(value.getSchema() + "." + value.getTable(), value); + } + + if (data instanceof Reference reference) { + PersistentValue value = reference.getBackingValue(); + dummyValueMap.put(value.getSchema() + "." + value.getTable(), value); + } + + if (data instanceof SimplePersistentCollection collection) { + dummySimplePersistentCollectionMap.put(collection.getSchema() + "." + collection.getTable(), collection); + dummySimplePersistentCollections.put(data.getHolder().getRootHolder(), collection); + } + + if (data instanceof PersistentManyToManyCollection collection) { + dummyPersistentManyToManyCollectionMap.put(collection.getSchema() + "." + collection.getJunctionTable(), collection); + dummyPersistentManyToManyCollections.put(data.getHolder().getRootHolder(), collection); + } + + if (data instanceof CachedValue cachedValue) { + dummyValueMap.put(cachedValue.getSchema() + "." + cachedValue.getTable(), cachedValue); + dummyCachedValues.put(data.getHolder().getRootHolder(), cachedValue); + } + } + } + + submitBlockingTask((connection, jedis) -> { + // Load all the unique data first + for (UniqueData dummyHolder : dummyHolders) { + loadUniqueData(connection, dummyHolder); + } + + //Load PersistentValues + for (UniqueData dummyHolder : dummyHolders) { + Collection keys = dummyDatabaseKeys.get(dummyHolder); + + // Use a multimap so we can easily group CellKeys together + // The actual key (DataKey) for this map is solely used for grouping entries with the same schema, table, data column, and id column + Multimap persistentDataColumns = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new); + + for (DatabaseKey key : keys) { + if (key instanceof CellKey cellKey) { + persistentDataColumns.put(new DataKey(cellKey.getSchema(), cellKey.getTable(), cellKey.getColumn(), cellKey.getIdColumn()), cellKey); + } + } + + for (DataKey key : persistentDataColumns.keySet()) { + persistentValueManager.loadAllFromDatabase(connection, dummyHolder, persistentDataColumns.get(key)); + } + } + + //Load SimplePersistentCollections + for (UniqueData dummyHolder : dummyHolders) { + Collection> dummyCollections = dummySimplePersistentCollections.get(dummyHolder); + + for (SimplePersistentCollection dummyCollection : dummyCollections) { + persistentCollectionManager.loadAllFromDatabase(connection, dummyHolder, dummyCollection); + } + } + + //Load PersistentManyToManyCollections + for (UniqueData dummyHolder : dummyHolders) { + Collection> dummyCollections = dummyPersistentManyToManyCollections.get(dummyHolder); + + for (PersistentManyToManyCollection dummyCollection : dummyCollections) { + persistentCollectionManager.loadJunctionTablesFromDatabase(connection, dummyCollection); + } + } + + //Load CachedValues + for (UniqueData dummyHolder : dummyHolders) { + Collection> dummyValues = dummyCachedValues.get(dummyHolder); + + for (CachedValue dummyValue : dummyValues) { + cachedValueManager.loadAllFromRedis(jedis, dummyValue); + } + } + }); + + loadedDependencies.addAll(dependencies); + } catch (Exception e) { + throw new RuntimeException(e); + } + + //to load data, we must first find a list of dependencies, and recursively grab their dependencies. after we've found all of them, we should load all data, and then we can establish relationships + +// try (Connection connection = getConnection()) { +// +// } catch (SQLException e) { +// throw new RuntimeException(e); +// } + + // All the entries we either just load or were previously loaded due to them being a dependency are now in the cache, so just return them via getAll() + return getAll(clazz); + } + + /** + * Create a new batch insert operation. + * + * @return The batch insert operation + */ + public final BatchInsert batchInsert() { + return new BatchInsert(this); + } + + /** + * Synchronously insert data into the datasource(s) and cache. + * + * @param batch The batch of data to insert + */ + @Blocking + public final void insertBatch(BatchInsert batch, List intermediateActions, List preInsertActions, List postInsertActions) { + submitBlockingTask((connection, jedis) -> { + connection.setAutoCommit(false); + for (InsertContext context : batch.getInsertionContexts()) { + insertIntoDataSource(connection, jedis, context); + } + for (ConnectionConsumer action : intermediateActions) { + action.accept(connection); + } + connection.setAutoCommit(true); + + for (InsertContext context : batch.getInsertionContexts()) { + insertIntoCache(context); + } + + for (Runnable action : preInsertActions) { + try { + action.run(); + } catch (Exception e) { + logger.error("Error running pre-insert action", e); + } + } + + for (Runnable action : postInsertActions) { + try { + action.run(); + } catch (Exception e) { + logger.error("Error running post-insert action", e); + } + } + }); + } + + /** + * Asynchronously insert data into the datasource(s) and cache. + * The data will be instantly inserted into the cache, and then the datasource(s) will be updated asynchronously. + * This method is non-blocking and will return immediately. + * The cached data can be accessed immediately after this method is called. + * + * @param batch The batch of data to insert + */ + public final void insertBatchAsync(BatchInsert batch, List intermediateActions, List preInsertActions, List postInsertActions) { + for (InsertContext context : batch.getInsertionContexts()) { + insertIntoCache(context); + } + for (Runnable action : preInsertActions) { + try { + action.run(); + } catch (Exception e) { + logger.error("Error running pre-insert action", e); + } + } + + submitAsyncTask((connection, jedis) -> { + connection.setAutoCommit(false); + for (InsertContext context : batch.getInsertionContexts()) { + insertIntoDataSource(connection, jedis, context); + } + for (ConnectionConsumer action : intermediateActions) { + action.accept(connection); + } + connection.setAutoCommit(true); + + for (Runnable action : postInsertActions) { + try { + action.run(); + } catch (Exception e) { + logger.error("Error running post-insert action", e); + } + } + }); + } + + /** + * Synchronously insert data into the datasource(s) and cache. + * + * @param holder The root holder of the data to insert + * @param initialData The initial data to insert + */ + @Blocking + public final void insert(UniqueData holder, InitialValue... initialData) { + InsertContext context = buildInsertContext(holder, initialData); + + submitBlockingTask((connection, jedis) -> { + insertIntoDataSource(connection, jedis, context); + insertIntoCache(context); + }); + } + + /** + * Asynchronously insert data into the datasource(s) and cache. + * The data will be instantly inserted into the cache, and then the datasource(s) will be updated asynchronously. + * This method is non-blocking and will return immediately. + * The cached data can be accessed immediately after this method is called. + * + * @param holder The root holder of the data to insert + * @param initialData The initial data to insert + */ + public final void insertAsync(UniqueData holder, InitialValue... initialData) { + InsertContext context = buildInsertContext(holder, initialData); + insertIntoCache(context); + + submitAsyncTask((connection, jedis) -> { + insertIntoDataSource(connection, jedis, context); + }); + } + + public InsertContext buildInsertContext(UniqueData holder, InitialValue... initialData) { + Map, InitialPersistentValue> initialPersistentValues = new HashMap<>(); + Map, InitialCachedValue> initialCachedValues = new HashMap<>(); + + for (Field field : ReflectionUtils.getFields(holder.getClass())) { + field.setAccessible(true); + + if (Data.class.isAssignableFrom(field.getType())) { + try { + Data data = (Data) field.get(holder); + if (data instanceof PersistentValue pv) { + initialPersistentValues.put(pv, new InitialPersistentValue(pv, pv.getDefaultValue())); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + for (InitialValue data : initialData) { + if (data instanceof InitialPersistentValue initial) { + initialPersistentValues.put(initial.getValue(), initial); + } else if (data instanceof InitialCachedValue initial) { + if (initial.getInitialDataValue() != null) { + initialCachedValues.put(initial.getValue(), initial); + } + } else { + throw new IllegalArgumentException("Unsupported initial data type: " + data.getClass()); + } + } + + for (Map.Entry, InitialPersistentValue> initial : initialPersistentValues.entrySet()) { + PersistentValue pv = initial.getKey(); + InitialPersistentValue value = initial.getValue(); + + if (Primitives.isPrimitive(pv.getDataType()) && !Primitives.getPrimitive(pv.getDataType()).isNullable()) { + Preconditions.checkNotNull(value.getInitialDataValue(), "Initial data value cannot be null for primitive type: " + pv.getDataType()); + } + } + + for (Map.Entry, InitialCachedValue> initial : initialCachedValues.entrySet()) { + CachedValue cv = initial.getKey(); + InitialCachedValue value = initial.getValue(); + + if (Primitives.isPrimitive(cv.getDataType()) && !Primitives.getPrimitive(cv.getDataType()).isNullable()) { + Preconditions.checkNotNull(value.getInitialDataValue(), "Initial data value cannot be null for primitive type: " + cv.getDataType()); + } + } + + return new InsertContext(holder, initialPersistentValues, initialCachedValues); + } + + private void insertIntoCache(InsertContext context) { + addUniqueData(context.holder()); + //todo: REFACTOR: similar to deletions, delegate this to the managers + + List initialPersistentDataWrappers = new ArrayList<>(); + for (InitialPersistentValue data : context.initialPersistentValues().values()) { + InsertionStrategy insertionStrategy = data.getValue().getInsertionStrategy(); + + //do not call PersistentValueManager#updateCache since we need to cache both values + //before PersistentCollectionManager#handlePersistentValueCacheUpdated is called, otherwise we get unexpected behavior + boolean updateCache = !cache.containsKey(data.getValue().getKey()) || insertionStrategy == InsertionStrategy.OVERWRITE_EXISTING; + Object oldValue; + + try { + oldValue = get(data.getValue().getKey()); + if (oldValue == NULL_MARKER) { + oldValue = null; + } + } catch (DataDoesNotExistException e) { + oldValue = null; + } + initialPersistentDataWrappers.add(new InitialPersistentDataWrapper(data, updateCache, oldValue)); + + if (updateCache) { + cache(data.getValue().getKey(), data.getValue().getDataType(), data.getInitialDataValue(), Instant.now(), false); + } + } + + for (InitialPersistentDataWrapper wrapper : initialPersistentDataWrappers) { + InitialPersistentValue data = wrapper.data; + boolean updateCache = wrapper.updateCache; + Object oldValue = wrapper.oldValue; + UniqueData pvHolder = data.getValue().getHolder().getRootHolder(); + CellKey idColumn = new CellKey( + pvHolder.getSchema(), + pvHolder.getTable(), + pvHolder.getIdentifier().getColumn(), + pvHolder.getId(), + pvHolder.getIdentifier().getColumn() + ); + + //Alert the collection manager of this change so it can update what it's keeping track of + if (updateCache) { + persistentCollectionManager.handlePersistentValueCacheUpdated( + data.getValue().getSchema(), + data.getValue().getTable(), + data.getValue().getColumn(), + context.holder().getId(), + data.getValue().getIdColumn(), + oldValue, + data.getInitialDataValue() + ); + } + + persistentCollectionManager.handlePersistentValueCacheUpdated( + idColumn.getSchema(), + idColumn.getTable(), + idColumn.getColumn(), + pvHolder.getId(), + idColumn.getIdColumn(), + null, + pvHolder.getId() + ); + } + + for (InitialCachedValue data : context.initialCachedValues().values()) { + cache(data.getValue().getKey(), data.getValue().getDataType(), data.getInitialDataValue(), Instant.now(), false); + } + } + + private void insertIntoDataSource(Connection connection, Jedis jedis, InsertContext context) throws SQLException { + if (context.initialPersistentValues().isEmpty() && context.initialCachedValues().isEmpty()) { + String sql = "INSERT INTO " + context.holder().getSchema() + "." + context.holder().getTable() + " (" + context.holder().getIdentifier().getColumn() + ") VALUES (?)"; + logSQL(sql); + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setObject(1, context.holder().getId()); + statement.executeUpdate(); + } + return; + } + persistentValueManager.insertInDatabase(connection, context.holder(), new ArrayList<>(context.initialPersistentValues().values())); + cachedValueManager.setInRedis(jedis, new ArrayList<>(context.initialCachedValues().values())); + } + + /** + * Delete data from the datasource(s) and cache. + * This method will immediately delete the data from the cache, and then asynchronously delete the data from the datasource(s). + * This method is non-blocking and will return immediately after deleting the data from the cache. + * Data will be recursively deleted based off of the {@link DeletionStrategy} of each member of the data object. + * + * @param holder The root holder of the data to delete + */ + public void delete(UniqueData holder) { + DeleteContext context = buildDeleteContext(holder); + logger.trace("Deleting: {}", context); + deleteFromCache(context); + //todo: i really dislike that update handlers are called when things are deleted. revisit this + + submitAsyncTask((connection, jedis) -> deleteFromDataSource(connection, jedis, context)); + } + + /** + * Synchronously delete data from the datasource(s) and cache. + * Data will be recursively deleted based off of the {@link DeletionStrategy} of each member of the data object. + * This method is inherently blocking. + * + * @param holder The root holder of the data to delete + */ + @Blocking + public void deleteSync(UniqueData holder) { + DeleteContext context = buildDeleteContext(holder); + logger.trace("Deleting: {}", context); + deleteFromCache(context); + + submitBlockingTask((connection, jedis) -> deleteFromDataSource(connection, jedis, context)); + } + + private DeleteContext buildDeleteContext(UniqueData holder) { + Set> toDelete = new HashSet<>(); + Set holders = new HashSet<>(); + extractDataToDelete(holder, holders, toDelete); + Map oldValues = new HashMap<>(); + for (Data data : toDelete) { + if (data instanceof PersistentValue || data instanceof CachedValue) { + try { + Object value = get(data.getKey()); + oldValues.put(data.getKey(), value == NULL_MARKER ? null : value); + } catch (DataDoesNotExistException e) { + // This is fine, it just means the value was null + } + } + } + + return new DeleteContext(holders, toDelete, oldValues); + } + + private void extractDataToDelete(UniqueData holder, Set holders, Set> toDelete) { + if (holders.contains(holder)) { + return; + } + holders.add(holder); + + //Add the root holder's id column to the list of things to delete, just in case the holder is empty + toDelete.add(PersistentValue.of(holder.getRootHolder(), UUID.class, holder.getRootHolder().getIdentifier().getColumn())); + + for (Field field : ReflectionUtils.getFields(holder.getClass())) { + field.setAccessible(true); + + if (Data.class.isAssignableFrom(field.getType())) { + try { + Data data = (Data) field.get(holder); + + //Always delete the backing value since it's in the same table as the holder + if (data instanceof Reference reference) { + toDelete.add(reference.getBackingValue()); + } + + if (data.getDeletionStrategy() == DeletionStrategy.NO_ACTION) { + continue; + } + + if (data instanceof Reference reference) { + UUID id = reference.getForeignId(); + if (id != null) { + UniqueData foreignData = reference.get(); + if (foreignData != null) { + extractDataToDelete(foreignData, holders, toDelete); + } + } + } + + if (data instanceof PersistentUniqueDataCollection collection) { + if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) { + for (UniqueData dataInCollection : collection) { + extractDataToDelete(dataInCollection, holders, toDelete); + } + } + } + + if (data instanceof PersistentManyToManyCollection collection) { + if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) { + for (UniqueData dataInCollection : collection) { + extractDataToDelete(dataInCollection, holders, toDelete); + } + } + } + + toDelete.add(data); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + private void deleteFromCache(DeleteContext context) { + persistentCollectionManager.deleteFromCache(context); + persistentValueManager.deleteFromCache(context); + cachedValueManager.deleteFromCache(context); + + for (UniqueData holder : context.holders()) { + removeUniqueData(holder.getClass(), holder.getId()); + } + } + + @Blocking + private void deleteFromDataSource(Connection connection, Jedis jedis, DeleteContext context) throws SQLException { + for (UniqueData holder : context.holders()) { + String sql = "DELETE FROM " + holder.getSchema() + "." + holder.getTable() + " WHERE " + holder.getIdentifier().getColumn() + " = ?"; + logSQL(sql); + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setObject(1, holder.getId()); + statement.executeUpdate(); + } + } + persistentValueManager.deleteFromDatabase(connection, context); + persistentCollectionManager.deleteFromDatabase(connection, context); + cachedValueManager.deleteFromRedis(jedis, context); + } + + /** + * Get all data of a given type from the cache. + * + * @param clazz The class of data to get + * @param The type of data to get + * @return A list of all the data of the given type in the cache + */ + public List getAll(Class clazz) { + return uniqueDataIds.get(clazz).stream().map(id -> get(clazz, id)).toList(); + } + + private void extractDependencies(Class clazz, @NotNull Set> dependencies) throws Exception { + if (dependencies.contains(clazz)) { + return; + } + + dependencies.add(clazz); + + if (!UniqueData.class.isAssignableFrom(clazz)) { + return; + } + + UniqueData dummy = createInstance(clazz, null); + dummyInstances.put(clazz, dummy); + + for (Field field : ReflectionUtils.getFields(clazz)) { + field.setAccessible(true); + + Class type = field.getType(); + + if (Data.class.isAssignableFrom(type)) { + Data data = (Data) field.get(dummy); + Class dataType = data.getDataType(); + + if (UniqueData.class.isAssignableFrom(dataType)) { + extractDependencies((Class) dataType, dependencies); + } + } + } + } + + private List> extractDataDependencies(Class clazz) throws Exception { + UniqueData dummy = createInstance(clazz, null); + + List> dependencies = new ArrayList<>(); + + for (Field field : ReflectionUtils.getFields(clazz)) { + field.setAccessible(true); + + Class type = field.getType(); + + if (Data.class.isAssignableFrom(type)) { + try { + Data data = (Data) field.get(dummy); + dependencies.add(data); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + dummyUniqueDataMap.put(dummy.getSchema() + "." + dummy.getTable(), dummy); + + return dependencies; + } + + public synchronized void removeFromCacheIf(Predicate predicate) { + Set keysToRemove = cache.keySet().stream().filter(predicate).collect(Collectors.toSet()); + keysToRemove.forEach(cache::remove); + } + + /** + * Dump the internal cache to the log. + */ + public void dump() { + logger.debug("Dumping cache:"); + for (Map.Entry entry : cache.entrySet()) { + logger.debug("{} -> {}", entry.getKey(), entry.getValue().value()); + } + } + + private void loadUniqueData(Connection connection, UniqueData dummyHolder) throws SQLException { + String sql = "SELECT " + dummyHolder.getIdentifier().getColumn() + " FROM " + dummyHolder.getSchema() + "." + dummyHolder.getTable(); + logSQL(sql); + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + ResultSet resultSet = statement.executeQuery(); + while (resultSet.next()) { + UUID id = resultSet.getObject(dummyHolder.getIdentifier().getColumn(), UUID.class); + UniqueData instance = createInstance(dummyHolder.getClass(), id); + addUniqueData(instance); + } + } + + logger.info("Loaded {} instances of {}", uniqueDataIds.get(dummyHolder.getClass()).size(), dummyHolder.getClass().getName()); + } + + /** + * Get a data object from the cache. + * + * @param data The data object to get + * @param The value type stored in the data object + * @return The data object from the cache + * @throws DataDoesNotExistException If the data object does not exist in the cache + */ + public T get(Data data) throws DataDoesNotExistException { + return get(data.getKey()); + } + + /** + * Get a data object from the cache. + * + * @param key The key of the data object to get + * @param The value type stored in the data object + * @return The data object from the cache + * @throws DataDoesNotExistException If the data object does not exist in the cache + */ + @SuppressWarnings("unchecked") + public T get(DataKey key) throws DataDoesNotExistException { + CacheEntry cacheEntry = cache.get(key); + + if (cacheEntry == null) { + throw new DataDoesNotExistException("Data does not exist in cache: " + key); + } + + Object value = cacheEntry.value(); + if (value == NULL_MARKER) { + return null; + } + + return (T) value; + } + + /** + * Get a {@link UniqueData} object from the cache. + * + * @param clazz The class of the data object to get + * @param id The id of the data object to get + * @param The type of data object to get + * @return The data object from the cache, or null if it does not exist + */ + public T get(Class clazz, UUID id) { + if (id == null) { + return null; + } + Map uniqueData = uniqueDataCache.get(clazz); + if (uniqueData != null) { + UniqueData data = uniqueData.get(id); + if (data != null) { + return clazz.cast(data); + } + } + + return null; + } + + public void addUniqueData(UniqueData data) { + logger.trace("Adding unique data to cache: {}({})", data.getClass(), data.getId()); + + CellKey idColumn = new CellKey( + data.getSchema(), + data.getTable(), + data.getIdentifier().getColumn(), + data.getId(), + data.getIdentifier().getColumn() + ); + cache(idColumn, UUID.class, data.getId(), Instant.now(), false); + + uniqueDataIds.put(data.getClass(), data.getId()); + uniqueDataCache.computeIfAbsent(data.getClass(), k -> new ConcurrentHashMap<>()).put(data.getId(), data); + } + + public void removeUniqueData(Class clazz, UUID id) { + try { + logger.trace("Removing unique data from cache: {}({})", clazz, id); + uniqueDataIds.remove(clazz, id); + uniqueDataCache.get(clazz).remove(id); + } catch (DataDoesNotExistException e) { + logger.trace("Data does not exist in cache: {}({})", clazz, id); + } + } + + /** + * Add an object to the cache. Null values are permitted. + * If an entry with the given key already exists in the cache, + * it will only be replaced if this value's instant is newer than the existing entry's instant. + * + * @param key The key to cache the value under + * @param valueDataType The type of the value + * @param value The value to cache + * @param instant The instant at which the value was set + */ + public void cache(DataKey key, Class valueDataType, T value, Instant instant, boolean callUpdateHandlers) { + if (value != null && !valueDataType.isInstance(value)) { + throw new IllegalArgumentException("Value is not of the correct type! Expected: " + valueDataType + ", got: " + value.getClass()); + } + if (Primitives.isPrimitive(valueDataType) && !Primitives.getPrimitive(valueDataType).isNullable()) { + Preconditions.checkNotNull(value, "Value cannot be null for primitive type: " + valueDataType); + } + + CacheEntry existing = cache.get(key); + + if (existing != null && existing.instant().isAfter(instant)) { + logger.trace("Not caching value: {} -> {}, existing entry is newer. {} vs {} (existing)", key, value, instant, existing.instant()); + return; + } else { + logger.trace("Caching value: {} -> {}", key, value); + } + + if (existing != null) { + logger.trace("Caching value to replace existing entry. Difference in instants: {} vs {} (existing)", instant, existing.instant()); + } + + cache.put(key, CacheEntry.of(Objects.requireNonNullElse(value, NULL_MARKER), instant)); + + Collection> updateHandlers = valueUpdateHandlers.get(key); + + Object oldValue = existing == null ? null : existing.value() == NULL_MARKER ? null : existing.value(); + Object newValue = value == NULL_MARKER ? null : value; + + if (Objects.equals(oldValue, newValue)) { + return; + } + + if (callUpdateHandlers) { + + for (ValueUpdateHandler updateHandler : updateHandlers) { + try { + updateHandler.unsafeHandle(oldValue, newValue); + } catch (Exception e) { + logger.error("Error handling value update. Key: {}", key, e); + } + } + } + } + + public int getCacheSize() { + return cache.size(); + } + + public void uncache(DataKey key) { + uncache(key, true); + } + + public void uncache(DataKey key, boolean removeUpdateHandlers) { + Collection> updateHandlers = valueUpdateHandlers.get(key); + CacheEntry existing = cache.get(key); + + for (ValueUpdateHandler updateHandler : updateHandlers) { + try { + updateHandler.unsafeHandle(existing.value(), null); + } catch (Exception e) { + logger.error("Error handling value update. Key: {}", key, e); + } + } + + cache.remove(key); + + if (removeUpdateHandlers) { + valueUpdateHandlers.removeAll(key); + } + } + + public void registerValueUpdateHandler(DataKey key, ValueUpdateHandler handler) { + valueUpdateHandlers.put(key, handler); + } + + public void registerSerializer(ValueSerializer serializer) { + if (Primitives.isPrimitive(serializer.getDeserializedType())) { + throw new IllegalArgumentException("Cannot register a serializer for a primitive type"); + } + + if (!Primitives.isPrimitive(serializer.getSerializedType())) { + throw new IllegalArgumentException("Cannot register a ValueSerializer that serializes to a non-primitive type"); + } + + + for (ValueSerializer v : valueSerializers) { + if (v.getDeserializedType().isAssignableFrom(serializer.getDeserializedType())) { + throw new IllegalArgumentException("A serializer for " + serializer.getDeserializedType() + " is already registered! (" + v.getClass() + ")"); + } + } + + valueSerializers.add(serializer); + } + + public boolean isSupportedType(Class type) { + if (Primitives.isPrimitive(type)) { + return true; + } + + for (ValueSerializer serializer : valueSerializers) { + if (serializer.getDeserializedType().isAssignableFrom(type)) { + return true; + } + } + + return false; + } + + public T deserialize(Class type, Object serialized) { + if (serialized == null) { + return null; + } + if (Primitives.isPrimitive(type)) { + return (T) serialized; + } + + for (ValueSerializer serializer : valueSerializers) { + if (serializer.getDeserializedType().isAssignableFrom(type)) { + return (T) serializer.unsafeDeserialize(serialized); + } + } + + throw new IllegalArgumentException("No serializer found for type: " + type); + } + + public Class getSerializedDataType(Class deserializedType) { + if (Primitives.isPrimitive(deserializedType)) { + return deserializedType; + } + + for (ValueSerializer serializer : valueSerializers) { + if (serializer.getDeserializedType().isAssignableFrom(deserializedType)) { + return serializer.getSerializedType(); + } + } + + throw new IllegalArgumentException("No serializer found for type: " + deserializedType); + } + + public Object serialize(T deserialized) { + if (deserialized == null) { + return null; + } + + if (Primitives.isPrimitive(deserialized.getClass())) { + return deserialized; + } + + for (ValueSerializer serializer : valueSerializers) { + if (serializer.getDeserializedType().isAssignableFrom(deserialized.getClass())) { + return serializer.unsafeSerialize(deserialized); + } + } + + throw new IllegalArgumentException("No serializer found for type: " + deserialized.getClass()); + } + + public Object decode(Class type, String value) { + if (Primitives.isPrimitive(type)) { + return Primitives.decode(type, value); + } + + ValueSerializer serializer = valueSerializers.stream() + .filter(s -> s.getDeserializedType().isAssignableFrom(type)) + .findFirst() + .orElseThrow(); + + Class serializedType = serializer.getSerializedType(); + + return Primitives.decode(serializedType, value); + } + + public UniqueData createInstance(Class clazz, UUID id) { + logger.trace("Creating instance of {} with id {}", clazz, id); + try { + Constructor constructor = clazz.getDeclaredConstructor(DataManager.class, UUID.class); + constructor.setAccessible(true); + return constructor.newInstance(this, id); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + public PostgresListener getPostgresListener() { + return pgListener; + } + + public String getIdColumn(Class clazz) { + if (!dummyInstances.containsKey(clazz)) { + try { + dummyInstances.put(clazz, createInstance(clazz, null)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return dummyInstances.get(clazz).getIdentifier().getColumn(); + } + + /** + * Block the calling thread until all previously enqueued tasks have been completed + */ + @Blocking + public void flushTaskQueue() { + //This will add a task to the queue and block until it's done + submitBlockingTask(connection -> { + //Ignore + }); + } + + private record InitialPersistentDataWrapper(InitialPersistentValue data, boolean updateCache, Object oldValue) { + } +} diff --git a/src/main/java/net/staticstudios/data/PersistentCollection.java b/src/og/java/net/staticstudios/data/PersistentCollection.java similarity index 100% rename from src/main/java/net/staticstudios/data/PersistentCollection.java rename to src/og/java/net/staticstudios/data/PersistentCollection.java diff --git a/src/og/java/net/staticstudios/data/PersistentValue.java b/src/og/java/net/staticstudios/data/PersistentValue.java new file mode 100644 index 00000000..2e51d70e --- /dev/null +++ b/src/og/java/net/staticstudios/data/PersistentValue.java @@ -0,0 +1,343 @@ +package net.staticstudios.data; + +import net.staticstudios.data.data.DataHolder; +import net.staticstudios.data.data.value.InitialPersistentValue; +import net.staticstudios.data.data.value.Value; +import net.staticstudios.data.impl.PersistentValueManager; +import net.staticstudios.data.key.CellKey; +import net.staticstudios.data.util.DeletionStrategy; +import net.staticstudios.data.util.InsertionStrategy; +import net.staticstudios.data.util.ValueUpdate; +import net.staticstudios.data.util.ValueUpdateHandler; +import net.staticstudios.utils.ThreadUtils; +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Represents a value that is stored in a database table. + * + * @param The type of data that this value stores. + */ +public class PersistentValue implements Value { + private final String schema; + private final String table; + private final String column; + private final Class dataType; + private final DataHolder holder; + private final String idColumn; + private final DataManager dataManager; + private Supplier defaultValueSupplier; + private DeletionStrategy deletionStrategy; + private InsertionStrategy insertionStrategy; + private int updateInterval = -1; + + private PersistentValue(String schema, String table, String column, String idColumn, Class dataType, DataHolder holder, DataManager dataManager) { + if (!holder.getDataManager().isSupportedType(dataType)) { + throw new IllegalArgumentException("Unsupported data type: " + dataType); + } + + this.schema = schema; + this.table = table; + this.column = column; + this.dataType = dataType; + this.holder = holder; + this.idColumn = idColumn; + this.dataManager = dataManager; + } + + /** + * Create a new {@link PersistentValue} object. + * + * @param holder The holder of this value. + * @param dataType The type of data that this value stores. + * @param column The column in the holder's table that stores this value. + * @param The type of data that this value stores. + * @return The persistent value. + */ + public static PersistentValue of(UniqueData holder, Class dataType, String column) { + PersistentValue pv = new PersistentValue<>(holder.getSchema(), holder.getTable(), column, holder.getRootHolder().getIdentifier().getColumn(), dataType, holder, holder.getDataManager()); + pv.deletionStrategy = DeletionStrategy.CASCADE; + return pv; + } + + /** + * Create a new {@link PersistentValue} object that stores a value in a different table than its holder. + * By default, this {@link PersistentValue} will have a deletion strategy of {@link DeletionStrategy#NO_ACTION} and + * an insertion strategy of {@link InsertionStrategy#PREFER_EXISTING}. + * + * @param holder The holder of this value. + * @param dataType The type of data that this value stores. + * @param schemaTableColumn The schema.table.column that stores this value. + * @param foreignIdColumn The column in the holder's table that stores the foreign key. + * @param The type of data that this value stores. + * @return The persistent value. + */ + public static PersistentValue foreign(UniqueData holder, Class dataType, String schemaTableColumn, String foreignIdColumn) { + String[] parts = schemaTableColumn.split("\\."); + if (parts.length != 3) { + throw new IllegalArgumentException("Invalid schema.table.column format: " + schemaTableColumn); + } + PersistentValue pv = new PersistentValue<>(parts[0], parts[1], parts[2], foreignIdColumn, dataType, holder, holder.getDataManager()); + pv.deletionStrategy = DeletionStrategy.NO_ACTION; + pv.insertionStrategy = InsertionStrategy.PREFER_EXISTING; + return pv; + } + + /** + * Create a new {@link PersistentValue} object that stores a value in a different table than its holder. + * By default, this {@link PersistentValue} will have a deletion strategy of {@link DeletionStrategy#NO_ACTION} and + * an insertion strategy of {@link InsertionStrategy#PREFER_EXISTING}. + * + * @param holder The holder of this value. + * @param dataType The type of data that this value stores. + * @param schema The schema that stores the value. + * @param table The table that stores the value. + * @param column The column that stores the value. + * @param foreignIdColumn The column in the holder's table that stores the foreign key. + * @param The type of data that this value stores. + * @return The persistent value. + */ + public static PersistentValue foreign(UniqueData holder, Class dataType, String schema, String table, String column, String foreignIdColumn) { + PersistentValue pv = new PersistentValue<>(schema, table, column, foreignIdColumn, dataType, holder, holder.getDataManager()); + pv.deletionStrategy = DeletionStrategy.NO_ACTION; + pv.insertionStrategy = InsertionStrategy.PREFER_EXISTING; + return pv; + } + + /** + * Set the initial value for this object + * + * @param value the initial value, or null if there is no initial value + * @return the initial value object + */ + public InitialPersistentValue initial(@Nullable T value) { + return new InitialPersistentValue(this, value); + } + + /** + * Add an update handler to this value. + * Note that update handlers are run asynchronously. + * + * @param updateHandler the update handler + * @return this + */ + @SuppressWarnings("unchecked") + public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { + dataManager.registerValueUpdateHandler(this.getKey(), update -> ThreadUtils.submit(() -> updateHandler.handle((ValueUpdate) update))); + return this; + } + + /** + * Set the default value for this value. + * + * @param defaultValue the default value, or null if there is no default value + * @return this + */ + public PersistentValue withDefault(@Nullable T defaultValue) { + this.defaultValueSupplier = () -> defaultValue; + return this; + } + + /** + * Set the default value for this value. + * + * @param defaultValueSupplier the default value supplier, or null if there is no default value + * @return this + */ + public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier) { + this.defaultValueSupplier = defaultValueSupplier; + return this; + } + + /** + * Get the default value for this value. + * + * @return the default value, or null if there is no default value + */ + public @Nullable T getDefaultValue() { + return defaultValueSupplier == null ? null : defaultValueSupplier.get(); + } + + @Override + public CellKey getKey() { + return new CellKey(this); + } + + public T get() { + return getDataManager().get(this); + } + + public void set(T value) { + PersistentValueManager manager = dataManager.getPersistentValueManager(); + manager.updateCache(this, value); + + Runnable runnable = () -> dataManager.submitAsyncTask(connection -> manager.updateInDatabase(connection, this, value)); + + if (updateInterval > 0) { + manager.enqueueRunnable(getKey(), updateInterval, runnable); + } else { + runnable.run(); + } + } + + /** + * Set the value of this persistent value. + * This method will block until the value is set in the database. + * + * @param value the value to set + */ + @Blocking + public void setNow(T value) { + PersistentValueManager manager = dataManager.getPersistentValueManager(); + manager.updateCache(this, value); + + Runnable runnable = () -> dataManager.submitBlockingTask(connection -> manager.updateInDatabase(connection, this, value)); + + if (updateInterval > 0) { + manager.enqueueRunnable(getKey(), updateInterval, runnable); + } else { + runnable.run(); + } + } + + /** + * Get the schema that this value is stored in. + * + * @return the schema + */ + public String getSchema() { + return schema; + } + + /** + * Get the table that this value is stored in. + * + * @return the table + */ + public String getTable() { + return table; + } + + /** + * Get the column that this value is stored in. + * + * @return the column + */ + public String getColumn() { + return column; + } + + @Override + public Class getDataType() { + return dataType; + } + + /** + * Get the column that stores the id of this value. + * + * @return the id column + */ + public String getIdColumn() { + return idColumn; + } + + @Override + public DataManager getDataManager() { + return dataManager; + } + + @Override + public DataHolder getHolder() { + return holder; + } + + @Override + public PersistentValue deletionStrategy(DeletionStrategy strategy) { + if (holder.getRootHolder().getSchema().equals(schema) && holder.getRootHolder().getTable().equals(table)) { + throw new IllegalArgumentException("Cannot set deletion strategy for a PersistentValue in the same table as it's holder!"); + } + this.deletionStrategy = strategy; + return this; + } + + /** + * Set the insertion strategy for this value. + * + * @param strategy the insertion strategy + * @return this + * @throws IllegalArgumentException if the holder and value are in the same table + */ + public PersistentValue insertionStrategy(InsertionStrategy strategy) { + if (holder.getRootHolder().getSchema().equals(schema) && holder.getRootHolder().getTable().equals(table)) { + throw new IllegalArgumentException("Cannot set deletion strategy for a PersistentValue in the same table as it's holder!"); + } + this.insertionStrategy = strategy; + return this; + } + + @Override + public @NotNull DeletionStrategy getDeletionStrategy() { + return deletionStrategy == null ? DeletionStrategy.NO_ACTION : deletionStrategy; + } + + /** + * Get the insertion strategy for this value. + * + * @return the insertion strategy + */ + public @NotNull InsertionStrategy getInsertionStrategy() { + return insertionStrategy == null ? InsertionStrategy.OVERWRITE_EXISTING : insertionStrategy; + } + + /** + * Set the update interval for this value. + * When set to an integer greater than 0, the database will be updated every n milliseconds, provided the value has changed. + * Further, if the value has changed 10 times, only the 10th change will be written to the database. + * + * @param updateInterval the update interval + * @return this + */ + public PersistentValue updateInterval(int updateInterval) { + this.updateInterval = updateInterval; + return this; + } + + /** + * Get the update interval for this value. + * + * @return the update interval + */ + public int getUpdateInterval() { + return updateInterval; + } + + @Override + public int hashCode() { + return getKey().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof PersistentValue other)) { + return false; + } + + return getKey().equals(other.getKey()); + } + + @Override + public String toString() { + return "PersistentValue{" + + "schema='" + schema + '\'' + + ", table='" + table + '\'' + + ", column='" + column + '\'' + + '}'; + } +} diff --git a/src/main/java/net/staticstudios/data/Reference.java b/src/og/java/net/staticstudios/data/Reference.java similarity index 100% rename from src/main/java/net/staticstudios/data/Reference.java rename to src/og/java/net/staticstudios/data/Reference.java diff --git a/src/og/java/net/staticstudios/data/UniqueData.java b/src/og/java/net/staticstudios/data/UniqueData.java new file mode 100644 index 00000000..e6dde3cc --- /dev/null +++ b/src/og/java/net/staticstudios/data/UniqueData.java @@ -0,0 +1,157 @@ +package net.staticstudios.data; + + +import com.google.common.base.Preconditions; +import net.staticstudios.data.data.Data; +import net.staticstudios.data.data.DataHolder; +import net.staticstudios.data.data.collection.SimplePersistentCollection; +import net.staticstudios.data.data.value.Value; +import net.staticstudios.data.key.UniqueIdentifier; +import net.staticstudios.data.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.util.Objects; +import java.util.UUID; + +/** + * Represents a unique data object that is stored in the database and contains other data objects. + */ +public abstract class UniqueData implements DataHolder { + private final DataManager dataManager; + private final String schema; + private final String table; + private final UniqueIdentifier identifier; + + /** + * Create a new unique data object. + * The id column is assumed to be "id". + * See {@link #UniqueData(DataManager, String, String, String, UUID)} if the id column is different. + * + * @param dataManager the data manager responsible for this data object + * @param schema the schema of the table + * @param table the table name + * @param id the id of the data object + */ + protected UniqueData(DataManager dataManager, String schema, String table, UUID id) { + this(dataManager, schema, table, "id", id); + } + + /** + * Create a new unique data object. + * + * @param dataManager the data manager responsible for this data object + * @param schema the schema of the table + * @param table the table name + * @param idColumn the name of the column that stores the id + * @param id the id of the data object + */ + protected UniqueData(DataManager dataManager, String schema, String table, String idColumn, UUID id) { + Preconditions.checkArgument(dataManager.get(this.getClass(), id) == null, "Data with id %s already exists", id); + this.dataManager = dataManager; + this.schema = schema; + this.table = table; + this.identifier = UniqueIdentifier.of(idColumn, id); + } + + /** + * Get the id of this data object. + * + * @return the id + */ + public UUID getId() { + return identifier.getId(); + } + + /** + * Get the table that this data object is stored in. + * + * @return the table name + */ + public String getTable() { + return table; + } + + /** + * Get the schema that this data object is stored in. + * + * @return the schema name + */ + public String getSchema() { + return schema; + } + + /** + * Get the unique identifier of this data object. + * + * @return the unique identifier + */ + public UniqueIdentifier getIdentifier() { + return identifier; + } + + @Override + public DataManager getDataManager() { + return dataManager; + } + + @Override + public UniqueData getRootHolder() { + return this; + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + UniqueData that = (UniqueData) obj; + return Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + UUID id = getId(); + return id != null ? id.hashCode() : 0; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(this.getClass().getSimpleName()); + sb.append("{"); + sb.append("id=").append(getId()).append("(").append(getSchema()).append(".").append(getTable()).append(".").append(identifier.getColumn()).append(")"); + + for (Field field : ReflectionUtils.getFields(this.getClass())) { + field.setAccessible(true); + Class fieldClass = field.getType(); + + if (!Data.class.isAssignableFrom(fieldClass)) { + continue; + } + + try { + Data data = (Data) field.get(this); + + if (data instanceof Value value) { + sb.append(", "); + sb.append(field.getName()).append("=").append(value.get()); + } else if (data instanceof SimplePersistentCollection collection) { + sb.append(", "); + sb.append(field.getName()).append("=Collection<").append(collection.getDataType().getSimpleName()).append(">{size=").append(collection.size()).append("}"); + } else if (data instanceof Reference reference) { + sb.append(", "); + UniqueData ref = reference.get(); + if (ref == null) { + sb.append(field.getName()).append("=null"); + } else { + sb.append(field.getName()).append("=").append(ref.getClass().getSimpleName()).append("{id=").append(ref.getId()).append("}"); + } + } + + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + sb.append("}"); + + return sb.toString(); + } +} diff --git a/src/main/java/net/staticstudios/data/ValueSerializer.java b/src/og/java/net/staticstudios/data/ValueSerializer.java similarity index 100% rename from src/main/java/net/staticstudios/data/ValueSerializer.java rename to src/og/java/net/staticstudios/data/ValueSerializer.java diff --git a/src/main/java/net/staticstudios/data/data/Data.java b/src/og/java/net/staticstudios/data/data/Data.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/Data.java rename to src/og/java/net/staticstudios/data/data/Data.java diff --git a/src/main/java/net/staticstudios/data/data/DataHolder.java b/src/og/java/net/staticstudios/data/data/DataHolder.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/DataHolder.java rename to src/og/java/net/staticstudios/data/data/DataHolder.java diff --git a/src/main/java/net/staticstudios/data/data/Deletable.java b/src/og/java/net/staticstudios/data/data/Deletable.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/Deletable.java rename to src/og/java/net/staticstudios/data/data/Deletable.java diff --git a/src/main/java/net/staticstudios/data/data/InitialValue.java b/src/og/java/net/staticstudios/data/data/InitialValue.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/InitialValue.java rename to src/og/java/net/staticstudios/data/data/InitialValue.java diff --git a/src/main/java/net/staticstudios/data/data/collection/CollectionEntry.java b/src/og/java/net/staticstudios/data/data/collection/CollectionEntry.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/collection/CollectionEntry.java rename to src/og/java/net/staticstudios/data/data/collection/CollectionEntry.java diff --git a/src/main/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java b/src/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java rename to src/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java diff --git a/src/main/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java b/src/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java rename to src/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java diff --git a/src/main/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java b/src/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java rename to src/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java diff --git a/src/main/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java b/src/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java rename to src/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java diff --git a/src/main/java/net/staticstudios/data/data/collection/PersistentValueCollection.java b/src/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/collection/PersistentValueCollection.java rename to src/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java diff --git a/src/main/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java b/src/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java rename to src/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java diff --git a/src/main/java/net/staticstudios/data/data/value/InitialCachedValue.java b/src/og/java/net/staticstudios/data/data/value/InitialCachedValue.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/value/InitialCachedValue.java rename to src/og/java/net/staticstudios/data/data/value/InitialCachedValue.java diff --git a/src/main/java/net/staticstudios/data/data/value/InitialPersistentValue.java b/src/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/value/InitialPersistentValue.java rename to src/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java diff --git a/src/main/java/net/staticstudios/data/data/value/Value.java b/src/og/java/net/staticstudios/data/data/value/Value.java similarity index 100% rename from src/main/java/net/staticstudios/data/data/value/Value.java rename to src/og/java/net/staticstudios/data/data/value/Value.java diff --git a/src/main/java/net/staticstudios/data/impl/CachedValueManager.java b/src/og/java/net/staticstudios/data/impl/CachedValueManager.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/CachedValueManager.java rename to src/og/java/net/staticstudios/data/impl/CachedValueManager.java diff --git a/src/main/java/net/staticstudios/data/impl/PersistentCollectionManager.java b/src/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/PersistentCollectionManager.java rename to src/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java diff --git a/src/main/java/net/staticstudios/data/impl/PersistentValueManager.java b/src/og/java/net/staticstudios/data/impl/PersistentValueManager.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/PersistentValueManager.java rename to src/og/java/net/staticstudios/data/impl/PersistentValueManager.java diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresData.java b/src/og/java/net/staticstudios/data/impl/pg/PostgresData.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/pg/PostgresData.java rename to src/og/java/net/staticstudios/data/impl/pg/PostgresData.java diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java b/src/og/java/net/staticstudios/data/impl/pg/PostgresListener.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java rename to src/og/java/net/staticstudios/data/impl/pg/PostgresListener.java diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java b/src/og/java/net/staticstudios/data/impl/pg/PostgresNotification.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java rename to src/og/java/net/staticstudios/data/impl/pg/PostgresNotification.java diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java b/src/og/java/net/staticstudios/data/impl/pg/PostgresOperation.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java rename to src/og/java/net/staticstudios/data/impl/pg/PostgresOperation.java diff --git a/src/main/java/net/staticstudios/data/key/CellKey.java b/src/og/java/net/staticstudios/data/key/CellKey.java similarity index 100% rename from src/main/java/net/staticstudios/data/key/CellKey.java rename to src/og/java/net/staticstudios/data/key/CellKey.java diff --git a/src/main/java/net/staticstudios/data/key/CollectionKey.java b/src/og/java/net/staticstudios/data/key/CollectionKey.java similarity index 100% rename from src/main/java/net/staticstudios/data/key/CollectionKey.java rename to src/og/java/net/staticstudios/data/key/CollectionKey.java diff --git a/src/main/java/net/staticstudios/data/key/DataKey.java b/src/og/java/net/staticstudios/data/key/DataKey.java similarity index 100% rename from src/main/java/net/staticstudios/data/key/DataKey.java rename to src/og/java/net/staticstudios/data/key/DataKey.java diff --git a/src/main/java/net/staticstudios/data/key/DatabaseKey.java b/src/og/java/net/staticstudios/data/key/DatabaseKey.java similarity index 100% rename from src/main/java/net/staticstudios/data/key/DatabaseKey.java rename to src/og/java/net/staticstudios/data/key/DatabaseKey.java diff --git a/src/main/java/net/staticstudios/data/key/RedisKey.java b/src/og/java/net/staticstudios/data/key/RedisKey.java similarity index 100% rename from src/main/java/net/staticstudios/data/key/RedisKey.java rename to src/og/java/net/staticstudios/data/key/RedisKey.java diff --git a/src/main/java/net/staticstudios/data/key/UniqueIdentifier.java b/src/og/java/net/staticstudios/data/key/UniqueIdentifier.java similarity index 100% rename from src/main/java/net/staticstudios/data/key/UniqueIdentifier.java rename to src/og/java/net/staticstudios/data/key/UniqueIdentifier.java diff --git a/src/main/java/net/staticstudios/data/primative/Primitive.java b/src/og/java/net/staticstudios/data/primative/Primitive.java similarity index 100% rename from src/main/java/net/staticstudios/data/primative/Primitive.java rename to src/og/java/net/staticstudios/data/primative/Primitive.java diff --git a/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java b/src/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java similarity index 100% rename from src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java rename to src/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java diff --git a/src/main/java/net/staticstudios/data/primative/Primitives.java b/src/og/java/net/staticstudios/data/primative/Primitives.java similarity index 100% rename from src/main/java/net/staticstudios/data/primative/Primitives.java rename to src/og/java/net/staticstudios/data/primative/Primitives.java diff --git a/src/main/java/net/staticstudios/data/util/BatchInsert.java b/src/og/java/net/staticstudios/data/util/BatchInsert.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/BatchInsert.java rename to src/og/java/net/staticstudios/data/util/BatchInsert.java diff --git a/src/main/java/net/staticstudios/data/util/CacheEntry.java b/src/og/java/net/staticstudios/data/util/CacheEntry.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/CacheEntry.java rename to src/og/java/net/staticstudios/data/util/CacheEntry.java diff --git a/src/og/java/net/staticstudios/data/util/ConnectionConsumer.java b/src/og/java/net/staticstudios/data/util/ConnectionConsumer.java new file mode 100644 index 00000000..9beefca2 --- /dev/null +++ b/src/og/java/net/staticstudios/data/util/ConnectionConsumer.java @@ -0,0 +1,8 @@ +package net.staticstudios.data.util; + +import java.sql.Connection; +import java.sql.SQLException; + +public interface ConnectionConsumer { + void accept(Connection connection) throws SQLException; +} diff --git a/src/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java b/src/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java new file mode 100644 index 00000000..8f6f209c --- /dev/null +++ b/src/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java @@ -0,0 +1,10 @@ +package net.staticstudios.data.util; + +import redis.clients.jedis.Jedis; + +import java.sql.Connection; +import java.sql.SQLException; + +public interface ConnectionJedisConsumer { + void accept(Connection connection, Jedis jedis) throws SQLException; +} diff --git a/src/main/java/net/staticstudios/data/util/DataDoesNotExistException.java b/src/og/java/net/staticstudios/data/util/DataDoesNotExistException.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/DataDoesNotExistException.java rename to src/og/java/net/staticstudios/data/util/DataDoesNotExistException.java diff --git a/src/og/java/net/staticstudios/data/util/DataSourceConfig.java b/src/og/java/net/staticstudios/data/util/DataSourceConfig.java new file mode 100644 index 00000000..ee8adf5a --- /dev/null +++ b/src/og/java/net/staticstudios/data/util/DataSourceConfig.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.util; + +public record DataSourceConfig( + String databaseHost, + int databasePort, + String databaseName, + String databaseUsername, + String databasePassword, + String redisHost, + int redisPort +) { +} diff --git a/src/main/java/net/staticstudios/data/util/DeleteContext.java b/src/og/java/net/staticstudios/data/util/DeleteContext.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/DeleteContext.java rename to src/og/java/net/staticstudios/data/util/DeleteContext.java diff --git a/src/main/java/net/staticstudios/data/util/DeletionStrategy.java b/src/og/java/net/staticstudios/data/util/DeletionStrategy.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/DeletionStrategy.java rename to src/og/java/net/staticstudios/data/util/DeletionStrategy.java diff --git a/src/main/java/net/staticstudios/data/util/InsertContext.java b/src/og/java/net/staticstudios/data/util/InsertContext.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/InsertContext.java rename to src/og/java/net/staticstudios/data/util/InsertContext.java diff --git a/src/main/java/net/staticstudios/data/util/InsertionStrategy.java b/src/og/java/net/staticstudios/data/util/InsertionStrategy.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/InsertionStrategy.java rename to src/og/java/net/staticstudios/data/util/InsertionStrategy.java diff --git a/src/main/java/net/staticstudios/data/util/JunctionTable.java b/src/og/java/net/staticstudios/data/util/JunctionTable.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/JunctionTable.java rename to src/og/java/net/staticstudios/data/util/JunctionTable.java diff --git a/src/main/java/net/staticstudios/data/util/PostgresUtils.java b/src/og/java/net/staticstudios/data/util/PostgresUtils.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/PostgresUtils.java rename to src/og/java/net/staticstudios/data/util/PostgresUtils.java diff --git a/src/og/java/net/staticstudios/data/util/ReflectionUtils.java b/src/og/java/net/staticstudios/data/util/ReflectionUtils.java new file mode 100644 index 00000000..c9b1eba9 --- /dev/null +++ b/src/og/java/net/staticstudios/data/util/ReflectionUtils.java @@ -0,0 +1,24 @@ +package net.staticstudios.data.util; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +public class ReflectionUtils { + + /** + * Get all fields from this class AND its superclasses + * + * @param clazz The class to get fields from + * @return A list of fields + */ + public static List getFields(Class clazz) { + List fields = new ArrayList<>(List.of(clazz.getDeclaredFields())); + + if (clazz.getSuperclass() != null) { + fields.addAll(getFields(clazz.getSuperclass())); + } + + return fields; + } +} diff --git a/src/main/java/net/staticstudios/data/util/SQLLogger.java b/src/og/java/net/staticstudios/data/util/SQLLogger.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/SQLLogger.java rename to src/og/java/net/staticstudios/data/util/SQLLogger.java diff --git a/src/og/java/net/staticstudios/data/util/TaskQueue.java b/src/og/java/net/staticstudios/data/util/TaskQueue.java new file mode 100644 index 00000000..e81d618a --- /dev/null +++ b/src/og/java/net/staticstudios/data/util/TaskQueue.java @@ -0,0 +1,111 @@ +package net.staticstudios.data.util; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.pool.HikariPool; +import net.staticstudios.utils.ShutdownStage; +import net.staticstudios.utils.ThreadUtils; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.sql.Connection; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +public class TaskQueue { + private final BlockingDeque taskQueue = new LinkedBlockingDeque<>(); + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + private final ExecutorService executor; + private final HikariPool connectionPool; + private final JedisPool jedisPool; + + public TaskQueue(DataSourceConfig config, String applicationName) { + HikariConfig poolConfig = new HikariConfig(); + poolConfig.setDataSourceClassName("com.impossibl.postgres.jdbc.PGDataSource"); + poolConfig.addDataSourceProperty("serverName", config.databaseHost()); + poolConfig.addDataSourceProperty("portNumber", config.databasePort()); + poolConfig.addDataSourceProperty("user", config.databaseUsername()); + poolConfig.addDataSourceProperty("password", config.databasePassword()); + poolConfig.addDataSourceProperty("databaseName", config.databaseName()); + poolConfig.addDataSourceProperty("ApplicationName", applicationName); + poolConfig.setLeakDetectionThreshold(10000); + poolConfig.setMaximumPoolSize(1); + + this.connectionPool = new HikariPool(poolConfig); + this.jedisPool = new JedisPool(config.redisHost(), config.redisPort()); + this.jedisPool.setMaxTotal(1); + + executor = Executors.newSingleThreadExecutor(r -> { + Thread thread = new Thread(r); + thread.setName("SQLTaskQueue"); + thread.setDaemon(true); + return thread; + }); + + start(); + + ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, this::shutdown); + } + + public CompletableFuture submitTask(ConnectionConsumer task) { + return submitTask((connection, jedis) -> task.accept(connection)); + } + + public CompletableFuture submitTask(ConnectionJedisConsumer task) { + CompletableFuture future = new CompletableFuture<>(); + taskQueue.addLast((connection, jedis) -> { + try { + task.accept(connection, jedis); + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + return future; + } + + private void start() { + executor.submit(() -> { + while (!(isShutdown.get() && taskQueue.isEmpty())) { + ConnectionJedisConsumer task; + try { + task = taskQueue.takeFirst(); + } catch (InterruptedException e) { + // We're shutting down + break; + } + + try ( + Connection connection = connectionPool.getConnection(); + Jedis jedis = jedisPool.getResource() + ) { + task.accept(connection, jedis); + + if (!connection.getAutoCommit()) { + connection.setAutoCommit(true); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + private void shutdown() { + if (!isShutdown.compareAndSet(false, true)) { + return; + } + executor.shutdown(); + + if (taskQueue.isEmpty()) { + executor.shutdownNow(); + } + + try { + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/src/og/java/net/staticstudios/data/util/ValueUpdate.java b/src/og/java/net/staticstudios/data/util/ValueUpdate.java new file mode 100644 index 00000000..aa715c43 --- /dev/null +++ b/src/og/java/net/staticstudios/data/util/ValueUpdate.java @@ -0,0 +1,6 @@ +package net.staticstudios.data.util; + +import org.jetbrains.annotations.Nullable; + +public record ValueUpdate(@Nullable T oldValue, @Nullable T newValue) { +} diff --git a/src/og/java/net/staticstudios/data/util/ValueUpdateHandler.java b/src/og/java/net/staticstudios/data/util/ValueUpdateHandler.java new file mode 100644 index 00000000..043bbbbd --- /dev/null +++ b/src/og/java/net/staticstudios/data/util/ValueUpdateHandler.java @@ -0,0 +1,11 @@ +package net.staticstudios.data.util; + +public interface ValueUpdateHandler { + + void handle(ValueUpdate update); + + @SuppressWarnings("unchecked") + default void unsafeHandle(Object oldValue, Object newValue) { + handle(new ValueUpdate<>((T) oldValue, (T) newValue)); + } +} diff --git a/src/test/java/net/staticstudios/data/CachedValueTest.java b/src/ogtest/java/net/staticstudios/data/CachedValueTest.java similarity index 95% rename from src/test/java/net/staticstudios/data/CachedValueTest.java rename to src/ogtest/java/net/staticstudios/data/CachedValueTest.java index 0780080c..a53810c6 100644 --- a/src/test/java/net/staticstudios/data/CachedValueTest.java +++ b/src/ogtest/java/net/staticstudios/data/CachedValueTest.java @@ -1,6 +1,6 @@ package net.staticstudios.data; -import net.staticstudios.data.key.RedisKey; +import net.staticstudios.data.primaryKey.RedisKey; import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.mock.cachedvalue.RedditUser; @@ -30,7 +30,7 @@ public void init() { drop schema if exists reddit cascade; create schema if not exists reddit; create table if not exists reddit.users ( - id uuid primary key + id uuid primary primaryKey ); """); } catch (SQLException e) { @@ -104,7 +104,7 @@ public void testExpiringCachedValue() { assertTrue(jedis.exists(user.suspended.getKey().toString())); - //wait for the key to expire + //wait for the primaryKey to expire try { Thread.sleep(4000); } catch (InterruptedException e) { @@ -191,8 +191,8 @@ public void testLoading() { UUID id = UUID.randomUUID(); ids.add(id); statement.executeUpdate("insert into reddit.users (id) values ('" + id + "')"); - RedisKey key = new RedisKey("reddit", "users", "id", id, "status"); - jedis.set(key.toString(), Primitives.encode("Hey, I'm user " + i)); + RedisKey primaryKey = new RedisKey("reddit", "users", "id", id, "status"); + jedis.set(primaryKey.toString(), Primitives.encode("Hey, I'm user " + i)); } } catch (SQLException e) { throw new RuntimeException(e); diff --git a/src/test/java/net/staticstudios/data/DeletionTest.java b/src/ogtest/java/net/staticstudios/data/DeletionTest.java similarity index 97% rename from src/test/java/net/staticstudios/data/DeletionTest.java rename to src/ogtest/java/net/staticstudios/data/DeletionTest.java index b136f222..f243bd3d 100644 --- a/src/test/java/net/staticstudios/data/DeletionTest.java +++ b/src/ogtest/java/net/staticstudios/data/DeletionTest.java @@ -1,7 +1,7 @@ package net.staticstudios.data; import net.staticstudios.data.data.collection.SimplePersistentCollection; -import net.staticstudios.data.key.RedisKey; +import net.staticstudios.data.primaryKey.RedisKey; import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.misc.TestUtils; @@ -24,32 +24,32 @@ public void init() { drop schema if exists minecraft cascade; create schema if not exists minecraft; create table if not exists minecraft.users ( - id uuid primary key, + id uuid primary primaryKey, name text not null ); create table if not exists minecraft.user_meta ( - id uuid primary key, + id uuid primary primaryKey, account_creation timestamp not null ); create table if not exists minecraft.user_stats ( - id uuid primary key + id uuid primary primaryKey ); create table if not exists minecraft.servers ( - id uuid primary key, + id uuid primary primaryKey, name text not null ); create table if not exists minecraft.skins ( - id uuid primary key, + id uuid primary primaryKey, user_id uuid, name text not null ); create table if not exists minecraft.user_servers ( user_id uuid not null, server_id uuid not null, - primary key (user_id, server_id) + primary primaryKey (user_id, server_id) ); create table if not exists minecraft.worlds ( - id uuid primary key, + id uuid primary primaryKey, user_id uuid not null, name text not null ); diff --git a/src/test/java/net/staticstudios/data/InsertionTest.java b/src/ogtest/java/net/staticstudios/data/InsertionTest.java similarity index 96% rename from src/test/java/net/staticstudios/data/InsertionTest.java rename to src/ogtest/java/net/staticstudios/data/InsertionTest.java index ef37441f..57f90afb 100644 --- a/src/test/java/net/staticstudios/data/InsertionTest.java +++ b/src/ogtest/java/net/staticstudios/data/InsertionTest.java @@ -23,11 +23,11 @@ public void init() { drop schema if exists twitch cascade; create schema if not exists twitch; create table if not exists twitch.users ( - id uuid primary key, + id uuid primary primaryKey, name text not null ); create table if not exists twitch.chat_messages ( - id uuid primary key, + id uuid primary primaryKey, sender_id uuid not null references twitch.users(id) on delete set null deferrable initially deferred ); """); diff --git a/src/test/java/net/staticstudios/data/PersistentCollectionTest.java b/src/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java similarity index 99% rename from src/test/java/net/staticstudios/data/PersistentCollectionTest.java rename to src/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java index e62414a8..0704a5bf 100644 --- a/src/test/java/net/staticstudios/data/PersistentCollectionTest.java +++ b/src/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java @@ -30,16 +30,16 @@ public void init() { drop schema if exists facebook cascade; create schema if not exists facebook; create table if not exists facebook.users ( - id uuid primary key + id uuid primary primaryKey ); create table if not exists facebook.posts ( - id uuid primary key, + id uuid primary primaryKey, user_id uuid, description text not null default '', likes int not null default 0 ); create table if not exists facebook.favorite_quotes ( - id uuid primary key, + id uuid primary primaryKey, user_id uuid, quote text not null default '' ); diff --git a/src/ogtest/java/net/staticstudios/data/PersistentValueTest.java b/src/ogtest/java/net/staticstudios/data/PersistentValueTest.java new file mode 100644 index 00000000..0230b51c --- /dev/null +++ b/src/ogtest/java/net/staticstudios/data/PersistentValueTest.java @@ -0,0 +1,354 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.misc.MockEnvironment; +import net.staticstudios.data.mock.persistentvalue.DiscordUser; +import net.staticstudios.data.mock.persistentvalue.DiscordUserSettings; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junitpioneer.jupiter.RetryingTest; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class PersistentValueTest extends DataTest { + + //todo: we need to test what happens when we manually edit the db. do the values get set on insert, update, and delete + //todo: test default values + //todo: test blocking #set calls + + @BeforeEach + public void init() { + try (Statement statement = getConnection().createStatement()) { + statement.executeUpdate(""" + drop schema if exists discord cascade; + create schema if not exists discord; + create table if not exists discord.users ( + id uuid primary primaryKey, + name text not null + ); + create table if not exists discord.user_meta ( + id uuid primary primaryKey, + name_updates_called int not null default 0, + enable_friend_requests_updates_called int not null default 0 + ); + create table if not exists discord.user_settings ( + user_id uuid primary primaryKey, + enable_friend_requests boolean not null default true + ); + """); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + getMockEnvironments().forEach(env -> { + DataManager dataManager = env.dataManager(); + dataManager.loadAll(DiscordUser.class); + dataManager.loadAll(DiscordUserSettings.class); + }); + } + + @RetryingTest(5) + public void testSetPersistentValue() { + MockEnvironment environment = getMockEnvironments().getFirst(); + DataManager dataManager = environment.dataManager(); + + DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); + assertEquals("John Doe", user.getName()); + + waitForDataPropagation(); + + try (Statement statement = getConnection().createStatement()) { + statement.executeUpdate("update discord.users set name = 'Jane Doe' where id = '" + user.getId() + "'"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + waitForDataPropagation(); + + assertEquals("Jane Doe", user.getName()); + + user.setName("User"); + assertEquals("User", user.getName()); + + waitForDataPropagation(); + + try (Statement statement = getConnection().createStatement()) { + ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); + resultSet.next(); + assertEquals("User", resultSet.getString("name")); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @RetryingTest(5) + public void testSetForeignPersistentValue() { + MockEnvironment environment = getMockEnvironments().getFirst(); + DataManager dataManager = environment.dataManager(); + + DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); + assertTrue(user.getEnableFriendRequests()); //defaults to true + + waitForDataPropagation(); + + try (Statement statement = getConnection().createStatement()) { + ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + user.getId() + "'"); + resultSet.next(); + assertTrue(resultSet.getBoolean("enable_friend_requests")); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + user.setEnableFriendRequests(false); + assertFalse(user.getEnableFriendRequests()); + + waitForDataPropagation(); + + try (Statement statement = getConnection().createStatement()) { + ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + user.getId() + "'"); + resultSet.next(); + assertFalse(resultSet.getBoolean("enable_friend_requests")); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + user.setEnableFriendRequests(true); + assertTrue(user.getEnableFriendRequests()); + + waitForDataPropagation(); + + try (Statement statement = getConnection().createStatement()) { + ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + user.getId() + "'"); + resultSet.next(); + assertTrue(resultSet.getBoolean("enable_friend_requests")); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + try (Statement statement = getConnection().createStatement()) { + statement.executeUpdate("update discord.user_settings set enable_friend_requests = false where user_id = '" + user.getId() + "'"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + waitForDataPropagation(); + + assertFalse(user.getEnableFriendRequests()); + } + + @RetryingTest(5) + public void testForeignPersistentValuePerferExistingInsertionStrategy() { + UUID id = UUID.randomUUID(); + try (Statement statement = getConnection().createStatement()) { + statement.executeUpdate("insert into discord.user_settings (user_id, enable_friend_requests) values ('" + id + "', false)"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + MockEnvironment environment = createMockEnvironment(); + DataManager dataManager = environment.dataManager(); + + dataManager.loadAll(DiscordUser.class); + dataManager.loadAll(DiscordUserSettings.class); + + assertNull(dataManager.get(DiscordUser.class, id)); + + DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", id); + assertFalse(user.getEnableFriendRequests()); + + user.setEnableFriendRequests(true); + assertTrue(user.getEnableFriendRequests()); + + waitForDataPropagation(); + + try (Statement statement = getConnection().createStatement()) { + ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + id + "'"); + resultSet.next(); + assertTrue(resultSet.getBoolean("enable_friend_requests")); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @RetryingTest(5) + @Disabled("see todo message. the datamanager no longer receives its own pg notifications, so this will never work until we implement the TODO") + public void testSetForeignPersistentValueAndSeePersistentValueUpdate() { + MockEnvironment environment = getMockEnvironments().getFirst(); + DataManager dataManager = environment.dataManager(); + + DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); + assertTrue(user.getEnableFriendRequests()); //defaults to true + + //todo: the settings should be created as soon as the user is, but this is not the case since they are completely separate entities + // this functionality needs to be added to the data manager. + // maybe instead of having the data manager do all the work, we can let it know somehow that DiscordUserSettings should be created when a DiscordUser is created + // currently it is being created when the postgres notification comes back altering us that the user_settings table has had a new row inserted +// DiscordSettings settings = dataManager.getUniqueData(DiscordSettings.class, user.getId()); + + + waitForDataPropagation(); + + DiscordUserSettings settings = dataManager.get(DiscordUserSettings.class, user.getId()); + + assertTrue(settings.getEnableFriendRequests()); + + user.setEnableFriendRequests(false); + assertFalse(user.getEnableFriendRequests()); + assertFalse(settings.getEnableFriendRequests()); + + settings.setEnableFriendRequests(true); + assertTrue(user.getEnableFriendRequests()); + assertTrue(settings.getEnableFriendRequests()); + + try (Statement statement = getConnection().createStatement()) { + statement.executeUpdate("update discord.user_settings set enable_friend_requests = false where user_id = '" + user.getId() + "'"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + waitForDataPropagation(); + + assertFalse(user.getEnableFriendRequests()); + assertFalse(settings.getEnableFriendRequests()); + } + + @RetryingTest(5) + public void testPersistentValueUpdateHandlers() { + //Note that update handlers are called async + MockEnvironment environment = getMockEnvironments().getFirst(); + DataManager dataManager = environment.dataManager(); + + DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); + assertEquals(0, user.getNameUpdatesCalled()); + assertEquals(0, user.getEnableFriendRequestsUpdatesCalled()); + + user.setName("Jane Doe"); + waitForDataPropagation(); + assertEquals(1, user.getNameUpdatesCalled()); + + user.setEnableFriendRequests(false); + waitForDataPropagation(); + assertEquals(1, user.getEnableFriendRequestsUpdatesCalled()); + + user.setEnableFriendRequests(true); + waitForDataPropagation(); + assertEquals(2, user.getEnableFriendRequestsUpdatesCalled()); + + user.setName("John Doe"); + waitForDataPropagation(); + assertEquals(2, user.getNameUpdatesCalled()); + + waitForDataPropagation(); + + assertEquals(2, user.getNameUpdatesCalled()); + assertEquals(2, user.getEnableFriendRequestsUpdatesCalled()); + + try (Statement statement = getConnection().createStatement()) { + statement.executeUpdate("update discord.users set name = 'Jane Doe' where id = '" + user.getId() + "'"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + try (Statement statement = getConnection().createStatement()) { + statement.executeUpdate("update discord.user_settings set enable_friend_requests = false where user_id = '" + user.getId() + "'"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + waitForDataPropagation(); + + assertEquals(3, user.getNameUpdatesCalled()); + assertEquals(3, user.getEnableFriendRequestsUpdatesCalled()); + } + + @RetryingTest(5) + public void testLoading() { + List ids = new ArrayList<>(); + try (Statement statement = getConnection().createStatement()) { + for (int i = 0; i < 10; i++) { + UUID id = UUID.randomUUID(); + ids.add(id); + statement.executeUpdate("insert into discord.users (id, name) values ('" + id + "', 'User " + i + "')"); + statement.executeUpdate("insert into discord.user_meta (id) values ('" + id + "')"); + statement.executeUpdate("insert into discord.user_settings (user_id) values ('" + id + "')"); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + MockEnvironment environment = createMockEnvironment(); + getMockEnvironments().add(environment); + + DataManager dataManager = environment.dataManager(); + dataManager.loadAll(DiscordUser.class); + + for (UUID id : ids) { + DiscordUser user = dataManager.get(DiscordUser.class, id); + assertEquals("User " + ids.indexOf(id), user.getName()); + } + } + + @RetryingTest(5) + public void testTaskQueue() { + MockEnvironment environment = getMockEnvironments().getFirst(); + DataManager dataManager = environment.dataManager(); + + DiscordUser user = DiscordUser.createSync(dataManager, "0", UUID.randomUUID()); + for (int i = 0; i < 30; i++) { + int name = Integer.parseInt(user.getName()); + user.setName(String.valueOf(name + 1)); + } + + waitForDataPropagation(); + + try (Statement statement = getConnection().createStatement()) { + ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); + resultSet.next(); + assertEquals("30", resultSet.getString("name")); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @RetryingTest(5) + public void testUpdateInterval() { + MockEnvironment environment = getMockEnvironments().getFirst(); + DataManager dataManager = environment.dataManager(); + + DiscordUser user = DiscordUser.createSync(dataManager, "0", UUID.randomUUID()); + user.name.updateInterval(getWaitForDataPropagationTime() * 2); + for (int i = 0; i < 30; i++) { + int name = Integer.parseInt(user.getName()); + user.setName(String.valueOf(name + 1)); + } + + waitForDataPropagation(); + + try (Statement statement = getConnection().createStatement()) { + ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); + resultSet.next(); + assertEquals("0", resultSet.getString("name")); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + waitForDataPropagation(); + waitForDataPropagation(); + + try (Statement statement = getConnection().createStatement()) { + ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); + resultSet.next(); + assertEquals("30", resultSet.getString("name")); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + //todo: test straight up deleting an fpv. ideally a data does not exist exception should be thrown when calling get on a deleted fpv +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/PostgresListenerTest.java b/src/ogtest/java/net/staticstudios/data/PostgresListenerTest.java similarity index 98% rename from src/test/java/net/staticstudios/data/PostgresListenerTest.java rename to src/ogtest/java/net/staticstudios/data/PostgresListenerTest.java index 8ca80952..d052e48a 100644 --- a/src/test/java/net/staticstudios/data/PostgresListenerTest.java +++ b/src/ogtest/java/net/staticstudios/data/PostgresListenerTest.java @@ -23,7 +23,7 @@ public void init() { drop schema if exists test cascade; create schema if not exists test; create table if not exists test.test ( - id uuid primary key, + id uuid primary primaryKey, value int not null ); """); diff --git a/src/test/java/net/staticstudios/data/PrimitivesTest.java b/src/ogtest/java/net/staticstudios/data/PrimitivesTest.java similarity index 96% rename from src/test/java/net/staticstudios/data/PrimitivesTest.java rename to src/ogtest/java/net/staticstudios/data/PrimitivesTest.java index 1a3784de..1560c677 100644 --- a/src/test/java/net/staticstudios/data/PrimitivesTest.java +++ b/src/ogtest/java/net/staticstudios/data/PrimitivesTest.java @@ -24,62 +24,62 @@ public void init() { drop schema if exists primitive cascade; create schema if not exists primitive; create table if not exists primitive.string_test ( - id uuid primary key, + id uuid primary primaryKey, value text ); create table if not exists primitive.character_test ( - id uuid primary key, + id uuid primary primaryKey, value char(1) not null ); create table if not exists primitive.byte_test ( - id uuid primary key, + id uuid primary primaryKey, value smallint not null ); create table if not exists primitive.short_test ( - id uuid primary key, + id uuid primary primaryKey, value smallint not null ); create table if not exists primitive.integer_test ( - id uuid primary key, + id uuid primary primaryKey, value integer not null ); create table if not exists primitive.long_test ( - id uuid primary key, + id uuid primary primaryKey, value bigint not null ); create table if not exists primitive.float_test ( - id uuid primary key, + id uuid primary primaryKey, value real not null ); create table if not exists primitive.double_test ( - id uuid primary key, + id uuid primary primaryKey, value double precision not null ); create table if not exists primitive.boolean_test ( - id uuid primary key, + id uuid primary primaryKey, value boolean not null ); create table if not exists primitive.uuid_test ( - id uuid primary key, + id uuid primary primaryKey, value uuid ); create table if not exists primitive.timestamp_test ( - id uuid primary key, + id uuid primary primaryKey, value timestamp ); create table if not exists primitive.byte_array_test ( - id uuid primary key, + id uuid primary primaryKey, value bytea ); """); diff --git a/src/test/java/net/staticstudios/data/ReferenceTest.java b/src/ogtest/java/net/staticstudios/data/ReferenceTest.java similarity index 96% rename from src/test/java/net/staticstudios/data/ReferenceTest.java rename to src/ogtest/java/net/staticstudios/data/ReferenceTest.java index af73563e..22376ff2 100644 --- a/src/test/java/net/staticstudios/data/ReferenceTest.java +++ b/src/ogtest/java/net/staticstudios/data/ReferenceTest.java @@ -21,15 +21,15 @@ public void init() { drop schema if exists snapchat cascade; create schema if not exists snapchat; create table if not exists snapchat.users ( - id uuid primary key, + id uuid primary primaryKey, favorite_user_id uuid ); create table if not exists snapchat.user_meta ( - id uuid primary key, + id uuid primary primaryKey, update_called integer ); create table if not exists snapchat.user_settings ( - user_id uuid primary key, + user_id uuid primary primaryKey, enable_friend_requests boolean not null ); """); diff --git a/src/ogtest/java/net/staticstudios/data/misc/DataTest.java b/src/ogtest/java/net/staticstudios/data/misc/DataTest.java new file mode 100644 index 00000000..0891cc29 --- /dev/null +++ b/src/ogtest/java/net/staticstudios/data/misc/DataTest.java @@ -0,0 +1,112 @@ +package net.staticstudios.data.misc; + +import com.redis.testcontainers.RedisContainer; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.util.DataSourceConfig; +import net.staticstudios.utils.ThreadUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; +import redis.clients.jedis.Jedis; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +public class DataTest { + public static final int NUM_ENVIRONMENTS = 1; + public static RedisContainer redis; + public static PostgreSQLContainer postgres = new PostgreSQLContainer<>( + "postgres:16.2" + ); + public static DataSourceConfig dataSourceConfig; + private static Connection connection; + private static Jedis jedis; + private List mockEnvironments; + + @BeforeAll + static void initPostgres() throws IOException, SQLException, InterruptedException { + postgres.start(); + redis = new RedisContainer(DockerImageName.parse("redis:6.2.6")); + redis.start(); + + redis.execInContainer("redis-cli", "config", "set", "notify-keyspace-events", "KEA"); + + dataSourceConfig = new DataSourceConfig( + postgres.getHost(), + postgres.getFirstMappedPort(), + postgres.getDatabaseName(), + postgres.getUsername(), + postgres.getPassword(), + redis.getHost(), + redis.getRedisPort() + ); + + connection = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + jedis = new Jedis(redis.getHost(), redis.getRedisPort()); + } + + @AfterAll + public static void cleanup() throws IOException { + postgres.stop(); + redis.stop(); + } + + public static Connection getConnection() { + return connection; + } + + public static Jedis getJedis() { + return jedis; + } + + @BeforeEach + public void setupMockEnvironments() { + mockEnvironments = new LinkedList<>(); + ThreadUtils.setProvider(new MockThreadProvider()); + for (int i = 0; i < NUM_ENVIRONMENTS; i++) { + mockEnvironments.add(createMockEnvironment()); + } + } + + protected MockEnvironment createMockEnvironment() { + DataManager dataManager = new DataManager(dataSourceConfig); + + MockEnvironment mockEnvironment = new MockEnvironment(dataSourceConfig, dataManager); + mockEnvironments.add(mockEnvironment); + return mockEnvironment; + } + + @AfterEach + public void teardownThreadUtils() { + ThreadUtils.shutdown(); + } + + public int getNumEnvironments() { + return mockEnvironments.size(); + } + + public List getMockEnvironments() { + return mockEnvironments; + } + + public int getWaitForDataPropagationTime() { + return 500 + (Objects.equals(System.getenv("GITHUB_ACTIONS"), "true") ? 1000 : 0); + } + + public void waitForDataPropagation() { + try { + Thread.sleep(getWaitForDataPropagationTime()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java b/src/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java new file mode 100644 index 00000000..a5b37ed1 --- /dev/null +++ b/src/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java @@ -0,0 +1,10 @@ +package net.staticstudios.data.misc; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.util.DataSourceConfig; + +public record MockEnvironment( + DataSourceConfig dataSourceConfig, + DataManager dataManager +) { +} diff --git a/src/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java b/src/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java new file mode 100644 index 00000000..b40bff74 --- /dev/null +++ b/src/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java @@ -0,0 +1,125 @@ +package net.staticstudios.data.misc; + +import net.staticstudios.utils.ShutdownStage; +import net.staticstudios.utils.ShutdownTask; +import net.staticstudios.utils.ThreadUtilProvider; +import net.staticstudios.utils.ThreadUtils; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.logging.Logger; + +public class MockThreadProvider implements ThreadUtilProvider { + private final ExecutorService mainThreadExecutorService; + private final List syncOnDisableTasksRunNext = Collections.synchronizedList(new ArrayList<>()); + private final List shutdownTasks = Collections.synchronizedList(new ArrayList<>()); + private final Logger logger = Logger.getLogger(MockThreadProvider.class.getName()); + private ExecutorService executorService; + private boolean isShuttingDown = false; + private boolean doneShuttingDown = false; + + + public MockThreadProvider() { + this.mainThreadExecutorService = Executors.newSingleThreadExecutor(); + this.executorService = Executors.newCachedThreadPool((r) -> new Thread(r, "MockThreadProvider")); + } + + @Override + public void submit(Runnable runnable) { + if (doneShuttingDown) { + throw new IllegalStateException("Cannot submit tasks after shutdown"); + } + executorService.submit(() -> { + try { + runnable.run(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + @Override + public void runSync(Runnable runnable) { + if (isShuttingDown) { + syncOnDisableTasksRunNext.add(runnable); + return; + } + + mainThreadExecutorService.submit(runnable); + } + + @Override + public void onShutdownRunSync(ShutdownStage shutdownStage, Runnable runnable) { + shutdownTasks.add(new ShutdownTask(shutdownStage, () -> { + ThreadUtils.safe(runnable); + return null; + }, true)); + } + + @Override + public void onShutdownRunAsync(ShutdownStage shutdownStage, Supplier> task) { + shutdownTasks.add(new ShutdownTask(shutdownStage, task, false)); + + } + + @Override + public boolean isShuttingDown() { + return isShuttingDown; + } + + public void shutdown() { + isShuttingDown = true; + + executorService.shutdown(); + try { + executorService.awaitTermination(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + Map> tasks = new HashMap<>(); + shutdownTasks.forEach(task -> tasks.computeIfAbsent(task.stage(), k -> new ArrayList<>()).add(task)); + + ShutdownStage.getStages() + .forEach(stage -> { + if (tasks.containsKey(stage)) { + getLogger().info("Running shutdown tasks for stage " + stage); + + List> asyncFutures = new ArrayList<>(); + List syncTasks = new ArrayList<>(); + + tasks.get(stage).forEach(task -> { + if (task.sync()) { + syncTasks.add(() -> task.task().get()); + } else { + asyncFutures.add(task.task().get()); + } + }); + + //Wait for all async tasks to finish + try { + CompletableFuture.allOf(asyncFutures.toArray(new CompletableFuture[0])).get(30, TimeUnit.SECONDS); + } catch (Exception e) { + getLogger().severe("Failed to wait for async tasks to finish during shutdown stage " + stage); + e.printStackTrace(); + } + + syncTasks.forEach(Runnable::run); + + syncOnDisableTasksRunNext.forEach(Runnable::run); + + syncOnDisableTasksRunNext.clear(); + } + }); + + doneShuttingDown = true; + } + + private Logger getLogger() { + return logger; + } +} \ No newline at end of file diff --git a/src/ogtest/java/net/staticstudios/data/misc/TestUtils.java b/src/ogtest/java/net/staticstudios/data/misc/TestUtils.java new file mode 100644 index 00000000..eeb5a95a --- /dev/null +++ b/src/ogtest/java/net/staticstudios/data/misc/TestUtils.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.misc; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class TestUtils { + public static int getResultCount(ResultSet rs) throws SQLException { + if (rs.getType() == ResultSet.TYPE_FORWARD_ONLY) { + int count = rs.getRow(); + while (rs.next()) { + count++; + } + return count; + } + + int currentRow = rs.getRow(); + try { + rs.last(); + int rowCount = rs.getRow(); + rs.absolute(currentRow); + return rowCount; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java b/src/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java rename to src/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java diff --git a/src/test/java/net/staticstudios/data/mock/deletions/MinecraftServer.java b/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/deletions/MinecraftServer.java rename to src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java diff --git a/src/test/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java b/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java rename to src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java diff --git a/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java b/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java rename to src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java diff --git a/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java b/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java rename to src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java diff --git a/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java b/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java rename to src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java diff --git a/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java b/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java rename to src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java diff --git a/src/test/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java b/src/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java rename to src/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java diff --git a/src/test/java/net/staticstudios/data/mock/insertions/TwitchUser.java b/src/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/insertions/TwitchUser.java rename to src/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java diff --git a/src/test/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java b/src/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java rename to src/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java diff --git a/src/test/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java b/src/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java rename to src/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java diff --git a/src/test/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java b/src/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java rename to src/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java diff --git a/src/test/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java b/src/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java rename to src/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java b/src/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java rename to src/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java diff --git a/src/test/java/net/staticstudios/data/mock/reference/SnapchatUser.java b/src/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/reference/SnapchatUser.java rename to src/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java diff --git a/src/test/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java b/src/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java rename to src/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java diff --git a/src/ogtest/resources/log4j.properties b/src/ogtest/resources/log4j.properties new file mode 100644 index 00000000..c87dc64f --- /dev/null +++ b/src/ogtest/resources/log4j.properties @@ -0,0 +1,6 @@ +log4j.rootLogger=INFO, STDOUT +log4j.logger.net.staticstudios=TRACE +log4j.logger.deng=INFO +log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender +log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout +log4j.appender.STDOUT.layout.ConversionPattern=%5p %d [%t] (%F:%L) - %m%n \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index dba1cfe1..3654def4 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -1,354 +1,17 @@ package net.staticstudios.data; import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; -import net.staticstudios.data.mock.persistentvalue.DiscordUser; -import net.staticstudios.data.mock.persistentvalue.DiscordUserSettings; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junitpioneer.jupiter.RetryingTest; +import net.staticstudios.data.mock.MockUser; +import org.junit.jupiter.api.Test; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; public class PersistentValueTest extends DataTest { - //todo: we need to test what happens when we manually edit the db. do the values get set on insert, update, and delete - //todo: test default values - //todo: test blocking #set calls - - @BeforeEach - public void init() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - drop schema if exists discord cascade; - create schema if not exists discord; - create table if not exists discord.users ( - id uuid primary key, - name text not null - ); - create table if not exists discord.user_meta ( - id uuid primary key, - name_updates_called int not null default 0, - enable_friend_requests_updates_called int not null default 0 - ); - create table if not exists discord.user_settings ( - user_id uuid primary key, - enable_friend_requests boolean not null default true - ); - """); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - getMockEnvironments().forEach(env -> { - DataManager dataManager = env.dataManager(); - dataManager.loadAll(DiscordUser.class); - dataManager.loadAll(DiscordUserSettings.class); - }); - } - - @RetryingTest(5) - public void testSetPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); - assertEquals("John Doe", user.getName()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.users set name = 'Jane Doe' where id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals("Jane Doe", user.getName()); - - user.setName("User"); - assertEquals("User", user.getName()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); - resultSet.next(); - assertEquals("User", resultSet.getString("name")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testSetForeignPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); - assertTrue(user.getEnableFriendRequests()); //defaults to true - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + user.getId() + "'"); - resultSet.next(); - assertTrue(resultSet.getBoolean("enable_friend_requests")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - user.setEnableFriendRequests(false); - assertFalse(user.getEnableFriendRequests()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + user.getId() + "'"); - resultSet.next(); - assertFalse(resultSet.getBoolean("enable_friend_requests")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - user.setEnableFriendRequests(true); - assertTrue(user.getEnableFriendRequests()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + user.getId() + "'"); - resultSet.next(); - assertTrue(resultSet.getBoolean("enable_friend_requests")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.user_settings set enable_friend_requests = false where user_id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertFalse(user.getEnableFriendRequests()); - } - - @RetryingTest(5) - public void testForeignPersistentValuePerferExistingInsertionStrategy() { - UUID id = UUID.randomUUID(); - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("insert into discord.user_settings (user_id, enable_friend_requests) values ('" + id + "', false)"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - MockEnvironment environment = createMockEnvironment(); - DataManager dataManager = environment.dataManager(); - - dataManager.loadAll(DiscordUser.class); - dataManager.loadAll(DiscordUserSettings.class); - - assertNull(dataManager.get(DiscordUser.class, id)); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", id); - assertFalse(user.getEnableFriendRequests()); - - user.setEnableFriendRequests(true); - assertTrue(user.getEnableFriendRequests()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + id + "'"); - resultSet.next(); - assertTrue(resultSet.getBoolean("enable_friend_requests")); - } catch (SQLException e) { - throw new RuntimeException(e); - } + @Test + public void test() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + MockUser mockUser = MockUser.create(dataManager, "josh"); } - - @RetryingTest(5) - @Disabled("see todo message. the datamanager no longer receives its own pg notifications, so this will never work until we implement the TODO") - public void testSetForeignPersistentValueAndSeePersistentValueUpdate() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); - assertTrue(user.getEnableFriendRequests()); //defaults to true - - //todo: the settings should be created as soon as the user is, but this is not the case since they are completely separate entities - // this functionality needs to be added to the data manager. - // maybe instead of having the data manager do all the work, we can let it know somehow that DiscordUserSettings should be created when a DiscordUser is created - // currently it is being created when the postgres notification comes back altering us that the user_settings table has had a new row inserted -// DiscordSettings settings = dataManager.getUniqueData(DiscordSettings.class, user.getId()); - - - waitForDataPropagation(); - - DiscordUserSettings settings = dataManager.get(DiscordUserSettings.class, user.getId()); - - assertTrue(settings.getEnableFriendRequests()); - - user.setEnableFriendRequests(false); - assertFalse(user.getEnableFriendRequests()); - assertFalse(settings.getEnableFriendRequests()); - - settings.setEnableFriendRequests(true); - assertTrue(user.getEnableFriendRequests()); - assertTrue(settings.getEnableFriendRequests()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.user_settings set enable_friend_requests = false where user_id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertFalse(user.getEnableFriendRequests()); - assertFalse(settings.getEnableFriendRequests()); - } - - @RetryingTest(5) - public void testPersistentValueUpdateHandlers() { - //Note that update handlers are called async - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); - assertEquals(0, user.getNameUpdatesCalled()); - assertEquals(0, user.getEnableFriendRequestsUpdatesCalled()); - - user.setName("Jane Doe"); - waitForDataPropagation(); - assertEquals(1, user.getNameUpdatesCalled()); - - user.setEnableFriendRequests(false); - waitForDataPropagation(); - assertEquals(1, user.getEnableFriendRequestsUpdatesCalled()); - - user.setEnableFriendRequests(true); - waitForDataPropagation(); - assertEquals(2, user.getEnableFriendRequestsUpdatesCalled()); - - user.setName("John Doe"); - waitForDataPropagation(); - assertEquals(2, user.getNameUpdatesCalled()); - - waitForDataPropagation(); - - assertEquals(2, user.getNameUpdatesCalled()); - assertEquals(2, user.getEnableFriendRequestsUpdatesCalled()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.users set name = 'Jane Doe' where id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.user_settings set enable_friend_requests = false where user_id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(3, user.getNameUpdatesCalled()); - assertEquals(3, user.getEnableFriendRequestsUpdatesCalled()); - } - - @RetryingTest(5) - public void testLoading() { - List ids = new ArrayList<>(); - try (Statement statement = getConnection().createStatement()) { - for (int i = 0; i < 10; i++) { - UUID id = UUID.randomUUID(); - ids.add(id); - statement.executeUpdate("insert into discord.users (id, name) values ('" + id + "', 'User " + i + "')"); - statement.executeUpdate("insert into discord.user_meta (id) values ('" + id + "')"); - statement.executeUpdate("insert into discord.user_settings (user_id) values ('" + id + "')"); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - - MockEnvironment environment = createMockEnvironment(); - getMockEnvironments().add(environment); - - DataManager dataManager = environment.dataManager(); - dataManager.loadAll(DiscordUser.class); - - for (UUID id : ids) { - DiscordUser user = dataManager.get(DiscordUser.class, id); - assertEquals("User " + ids.indexOf(id), user.getName()); - } - } - - @RetryingTest(5) - public void testTaskQueue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "0", UUID.randomUUID()); - for (int i = 0; i < 30; i++) { - int name = Integer.parseInt(user.getName()); - user.setName(String.valueOf(name + 1)); - } - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); - resultSet.next(); - assertEquals("30", resultSet.getString("name")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testUpdateInterval() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "0", UUID.randomUUID()); - user.name.updateInterval(getWaitForDataPropagationTime() * 2); - for (int i = 0; i < 30; i++) { - int name = Integer.parseInt(user.getName()); - user.setName(String.valueOf(name + 1)); - } - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); - resultSet.next(); - assertEquals("0", resultSet.getString("name")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); - resultSet.next(); - assertEquals("30", resultSet.getString("name")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - //todo: test straight up deleting an fpv. ideally a data does not exist exception should be thrown when calling get on a deleted fpv } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java new file mode 100644 index 00000000..212061c4 --- /dev/null +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -0,0 +1,13 @@ +package net.staticstudios.data; + +public class SQLParseTest { + +// @Test +// public void testParse() { +// DataManager dm = new DataManager(); +// dm.extractMetadata(MockUser.class); +// String sql = dm.getSQLBuilder().asSQL(); +// +// System.out.println(sql); +// } +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/ValueParseTest.java b/src/test/java/net/staticstudios/data/ValueParseTest.java new file mode 100644 index 00000000..ddc68a0e --- /dev/null +++ b/src/test/java/net/staticstudios/data/ValueParseTest.java @@ -0,0 +1,32 @@ +package net.staticstudios.data; + +import net.staticstudios.data.util.EnvironmentVariableAccessor; +import net.staticstudios.data.util.ValueUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ValueParseTest { + + @BeforeAll + public static void setup() { + ValueUtils.ENVIRONMENT_VARIABLE_ACCESSOR = new EnvironmentVariableAccessor() { + @Override + public String getEnv(String name) { + return switch (name) { + case "env_var" -> "value_here"; + case "ENV_VAR2" -> "VALUE2_HERE"; + default -> null; + }; + } + }; + } + + @Test + public void testParse() { + assertEquals("value", ValueUtils.parseValue("value")); + assertEquals("value_here", ValueUtils.parseValue("${env_var}")); + assertEquals("VALUE2_HERE", ValueUtils.parseValue("${ENV_VAR2}")); + } +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/MockUser.java new file mode 100644 index 00000000..bceb1698 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/MockUser.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.mock; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.PersistentValue; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.Column; +import net.staticstudios.data.parse.Data; + +import java.util.UUID; + +@Data(schema = "public", table = "users", idColumn = "id") +public class MockUser extends UniqueData { + private final UUID id; + + public @Column(value = "name") PersistentValue name = PersistentValue.of(this, String.class).withDefault("Unknown"); + public @Column(value = "age", nullable = true) PersistentValue age; + + public MockUser(DataManager dataManager, UUID id) { + super(dataManager); + this.id = id; + } + + public static MockUser create(DataManager dataManager, String name) { + MockUser user = new MockUser(dataManager, UUID.randomUUID()); + dataManager.init(user); //todo: can do this in insert + dataManager.insert(user, false); //todo: set the name and age + return user; + } + + @Override + public UUID getId() { + return id; + } +} diff --git a/static-data-cache.db b/static-data-cache.db new file mode 100644 index 00000000..e69de29b From f39a38d93d968f561ce480e057d5df352d51034b Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Thu, 11 Sep 2025 20:57:42 -0400 Subject: [PATCH 02/75] partial v3 impl --- build.gradle | 4 +- .../net/staticstudios/data/DataAccessor.java | 20 +- .../net/staticstudios/data/DataManager.java | 289 ++++++++++------ .../staticstudios/data/PersistentValue.java | 113 +++++-- .../net/staticstudios/data/Reference.java | 53 +++ .../java/net/staticstudios/data/Relation.java | 2 +- .../net/staticstudios/data/UniqueData.java | 28 +- .../java/net/staticstudios/data/Value.java | 7 + .../net/staticstudios/data/delete/Delete.java | 9 + .../data/delete/DeleteStrategy.java | 18 + .../data/impl/data/PersistentValueImpl.java | 218 ++++++++++++ .../data/impl/data/ReferenceImpl.java | 157 +++++++++ .../data/impl/h2/H2DataAccessor.java | 320 ++++++++++++++++++ .../staticstudios/data/impl/h2/H2Trigger.java | 90 +++++ .../data/impl/pg/PostgresData.java | 9 + .../data/impl/pg/PostgresListener.java | 192 +++++++++++ .../data/impl/pg/PostgresNotification.java | 50 +++ .../data/impl/pg/PostgresOperation.java | 7 + .../data/impl/sqlite/SQLiteDataAccessor.java | 128 ------- .../impl/sqlite/SQLitePersistentValue.java | 123 ------- .../net/staticstudios/data/insert/Insert.java | 9 + .../data/insert/InsertContext.java | 93 +++++ .../staticstudios/data/insert/InsertMode.java | 16 + .../data/insert/InsertStrategy.java | 12 + .../net/staticstudios/data/parse/Column.java | 2 +- .../net/staticstudios/data/parse/Data.java | 2 - .../staticstudios/data/parse/SQLBuilder.java | 185 +++++++--- .../staticstudios/data/parse/SQLColumn.java | 37 +- .../staticstudios/data/parse/SQLTable.java | 9 +- .../data/parse/UniqueDataMetadata.java | 7 +- .../data/util/ColumnMetadata.java | 5 + .../data/util/ColumnValuePair.java | 47 +++ .../data/util/ColumnValuePairs.java | 65 ++++ .../data/util/ForeignColumn.java | 26 ++ .../net/staticstudios/data/util/IdColumn.java | 9 + .../net/staticstudios/data/util/OneToOne.java | 19 ++ .../data/util/PersistentValueMetadata.java | 58 ++++ .../staticstudios/data/util/PrimaryKey.java | 11 - .../data/util/ReflectionUtils.java | 6 +- .../net/staticstudios/data/util/SQLUtils.java | 28 ++ .../staticstudios/data/util/SQlStatement.java | 21 ++ .../staticstudios/data/util/StringUtils.java | 9 + .../data/util/ValueUpdateHandler.java | 10 +- .../ValueUpdateHandlerNonStaticException.java | 7 + .../data/util/ValueUpdateHandlerWrapper.java | 48 +++ .../staticstudios/data/util/ValueUtils.java | 11 + .../data/PersistentValueTest.java | 104 +++++- .../net/staticstudios/data/SQLParseTest.java | 20 +- .../net/staticstudios/data/mock/MockUser.java | 60 +++- .../data/mock/MockUserSettings.java | 27 ++ static-data-cache.db | 0 51 files changed, 2304 insertions(+), 496 deletions(-) create mode 100644 src/main/java/net/staticstudios/data/Reference.java create mode 100644 src/main/java/net/staticstudios/data/Value.java create mode 100644 src/main/java/net/staticstudios/data/delete/Delete.java create mode 100644 src/main/java/net/staticstudios/data/delete/DeleteStrategy.java create mode 100644 src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java create mode 100644 src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java create mode 100644 src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java create mode 100644 src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java create mode 100644 src/main/java/net/staticstudios/data/impl/pg/PostgresData.java create mode 100644 src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java create mode 100644 src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java create mode 100644 src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java delete mode 100644 src/main/java/net/staticstudios/data/impl/sqlite/SQLiteDataAccessor.java delete mode 100644 src/main/java/net/staticstudios/data/impl/sqlite/SQLitePersistentValue.java create mode 100644 src/main/java/net/staticstudios/data/insert/Insert.java create mode 100644 src/main/java/net/staticstudios/data/insert/InsertContext.java create mode 100644 src/main/java/net/staticstudios/data/insert/InsertMode.java create mode 100644 src/main/java/net/staticstudios/data/insert/InsertStrategy.java create mode 100644 src/main/java/net/staticstudios/data/util/ColumnMetadata.java create mode 100644 src/main/java/net/staticstudios/data/util/ColumnValuePair.java create mode 100644 src/main/java/net/staticstudios/data/util/ColumnValuePairs.java create mode 100644 src/main/java/net/staticstudios/data/util/ForeignColumn.java create mode 100644 src/main/java/net/staticstudios/data/util/IdColumn.java create mode 100644 src/main/java/net/staticstudios/data/util/OneToOne.java create mode 100644 src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java delete mode 100644 src/main/java/net/staticstudios/data/util/PrimaryKey.java create mode 100644 src/main/java/net/staticstudios/data/util/SQLUtils.java create mode 100644 src/main/java/net/staticstudios/data/util/SQlStatement.java create mode 100644 src/main/java/net/staticstudios/data/util/StringUtils.java create mode 100644 src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java create mode 100644 src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/MockUserSettings.java delete mode 100644 static-data-cache.db diff --git a/build.gradle b/build.gradle index a2b794cb..0d58bc39 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'net.staticstudios' -version = '2.0.10-SNAPSHOT' +version = '3.0.0-SNAPSHOT' repositories { mavenCentral() @@ -24,7 +24,7 @@ dependencies { implementation 'redis.clients:jedis:5.1.2' implementation 'net.staticstudios:static-utils:1.0.1' implementation 'com.h2database:h2:2.3.232' - implementation 'org.xerial:sqlite-jdbc:3.45.1.0' +// implementation 'org.xerial:sqlite-jdbc:3.45.1.0' implementation 'org.jetbrains:annotations:24.0.1' // testImplementation(platform('org.junit:junit-bom:5.10.3')) diff --git a/src/main/java/net/staticstudios/data/DataAccessor.java b/src/main/java/net/staticstudios/data/DataAccessor.java index 6066c0a7..e4c56cca 100644 --- a/src/main/java/net/staticstudios/data/DataAccessor.java +++ b/src/main/java/net/staticstudios/data/DataAccessor.java @@ -1,15 +1,25 @@ package net.staticstudios.data; -import net.staticstudios.data.util.PrimaryKey; +import net.staticstudios.data.insert.InsertContext; +import net.staticstudios.data.insert.InsertMode; import org.intellij.lang.annotations.Language; -import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; +import java.util.List; public interface DataAccessor { - PreparedStatement prepareStatement(@Language("SQL") String sql) throws SQLException; +// PreparedStatement prepareStatement(@Language("SQL") String sql) throws SQLException; - PersistentValue createPersistentValue(PrimaryKey primaryKey, Class dataType, String schema, String table, String dataColumn); +// PersistentValue createPersistentValue(PrimaryKey primaryKey, Class dataType, String schema, String table, String dataColumn); - void insertIntoCache(UniqueData uniqueData) throws SQLException; + ResultSet executeQuery(@Language("SQL") String sql, List values) throws SQLException; + + void executeUpdate(@Language("SQL") String sql, List values) throws SQLException; + + void insert(InsertContext insertContext, InsertMode insertMode) throws SQLException; + + void runDDL(@Language("SQL") String sql) throws SQLException; + + void postDDL() throws SQLException; } diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 3fd9681e..9328d0fa 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -1,39 +1,60 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.impl.sqlite.SQLiteDataAccessor; -import net.staticstudios.data.parse.Column; +import com.google.common.collect.MapMaker; +import net.staticstudios.data.impl.data.PersistentValueImpl; +import net.staticstudios.data.impl.data.ReferenceImpl; +import net.staticstudios.data.impl.h2.H2DataAccessor; +import net.staticstudios.data.impl.pg.PostgresListener; +import net.staticstudios.data.insert.InsertContext; +import net.staticstudios.data.insert.InsertMode; import net.staticstudios.data.parse.Data; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.parse.UniqueDataMetadata; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Blocking; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.sql.Connection; -import java.sql.PreparedStatement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.sql.SQLException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; public class DataManager { private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private final String applicationName; private final DataAccessor dataAccessor; private final SQLBuilder sqlBuilder; private final TaskQueue taskQueue; private final ConcurrentHashMap, UniqueDataMetadata> uniqueDataMetadataMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap, Map> uniqueDataInstanceCache = new ConcurrentHashMap<>(); //todo: weak reference map + private final ConcurrentHashMap, List>>> updateHandlers = new ConcurrentHashMap<>(); + private final PostgresListener postgresListener; + //todo: use the class as a key since we will need to pass the instance as a param on each update + private final Set registeredUpdateHandlersForColumns = Collections.synchronizedSet(new HashSet<>()); public DataManager(DataSourceConfig dataSourceConfig) { - String applicationName = "static_data_manager_v3-" + UUID.randomUUID(); - sqlBuilder = new SQLBuilder(); - dataAccessor = new SQLiteDataAccessor(sqlBuilder); + applicationName = "static_data_manager_v3-" + UUID.randomUUID(); + postgresListener = new PostgresListener(this, dataSourceConfig); + sqlBuilder = new SQLBuilder(this); this.taskQueue = new TaskQueue(dataSourceConfig, applicationName); + dataAccessor = new H2DataAccessor(this, postgresListener, taskQueue); //todo: when we parse UniqueData objects we should build an internal map, and then when we are done auto create the sql if the tables dont exist //todo: this will be extremely useful for building the internal cache tables + + //todo: when we reconnect to postgres, refresh the internal cache from the source + + //todo: support for CachedValues + } + + public String getApplicationName() { + return applicationName; } public DataAccessor getDataAccessor() { @@ -44,14 +65,102 @@ public SQLBuilder getSQLBuilder() { return sqlBuilder; } + public InsertContext createInsertContext() { + return new InsertContext(this); + } + + public void addUpdateHandler(String schema, String table, String column, ValueUpdateHandlerWrapper handler) { + String key = schema + "." + table + "." + column; + updateHandlers.computeIfAbsent(key, k -> new ConcurrentHashMap<>()) + .computeIfAbsent(handler.getHolderClass(), k -> new CopyOnWriteArrayList<>()) + .add(handler); + } + + //todo: when a row is updated, provide the entire row to this method (so we can grab id cols). this is the responsibility of the data accessor impl + @ApiStatus.Internal + public void callUpdateHandlers(List columnNames, String schema, String table, String column, Object[] oldSerializedValues, Object[] newSerializedValues) { + //todo: submit to somewhere for where to run these, configured during setup. default to thread utils + Map, List>> handlersForColumn = updateHandlers.get(schema + "." + table + "." + column); + if (handlersForColumn == null) { + return; + } + + int columnIndex = columnNames.indexOf(column); + Preconditions.checkArgument(columnIndex != -1, "Column %s not found in provided column names %s", column, columnNames); + + for (Map.Entry, List>> entry : handlersForColumn.entrySet()) { + Class holderClass = entry.getKey(); + UniqueDataMetadata metadata = getMetadata(holderClass); + List> handlers = entry.getValue(); + 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(), oldSerializedValues[i]); + found = true; + break; + } + } + if (!found) { + throw new IllegalArgumentException("Not all ID columns were provided for UniqueData class " + holderClass.getName() + ". Required: " + metadata.idColumns() + ", Provided: " + columnNames); + } + } + UniqueData instance = get(holderClass, idColumns); + for (ValueUpdateHandlerWrapper wrapper : handlers) { + Class dataType = wrapper.getDataType(); + Object deserializedOldValue = oldSerializedValues[columnIndex]; //todo: these + Object deserializedNewValue = newSerializedValues[columnIndex]; + + wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue); + } + } + } + + @ApiStatus.Internal + public void registerUpdateHandler(PersistentValueMetadata metadata, Collection> handlers) { + if (registeredUpdateHandlersForColumns.add(metadata)) { + for (ValueUpdateHandlerWrapper handler : handlers) { + addUpdateHandler(metadata.getSchema(), metadata.getTable(), metadata.getColumn(), handler); + } + } + } + + public List> getUpdateHandlers(String schema, String table, String column, Class holderClass) { + String key = schema + "." + table + "." + column; + if (updateHandlers.containsKey(key) && updateHandlers.get(key).containsKey(holderClass)) { + return updateHandlers.get(key).get(holderClass); + } + return Collections.emptyList(); + } + @SafeVarargs public final void load(Class... classes) { for (Class clazz : classes) { extractMetadata(clazz); } + List defs = new ArrayList<>(); + for (Class clazz : classes) { + defs.addAll(sqlBuilder.parse(clazz)); + } + + for (String def : defs) { + try { + dataAccessor.runDDL(def); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + try { + dataAccessor.postDDL(); + } catch (SQLException e) { + throw new RuntimeException(e); + } - //todo: create source tables if not exists - //todo tell cache accessor to create its cache tables + //todo: the sql builder needs to be altered to spit out the sql for the just walked class + //todo: then we need to create those tables in the cache and source, and then finally load all of that data into the cache + //todo: also, stare listening to events before we start grabbing data, queue them, and then process them after the initial load is done } public void extractMetadata(Class clazz) { @@ -59,9 +168,24 @@ public void extractMetadata(Class clazz) { Data dataAnnotation = clazz.getAnnotation(Data.class); Preconditions.checkNotNull(dataAnnotation, "UniqueData class %s is missing @Data annotation", clazz.getName()); - sqlBuilder.parse(clazz); - UniqueDataMetadata metadata = new UniqueDataMetadata(ValueUtils.parseValue(dataAnnotation.schema()), ValueUtils.parseValue(dataAnnotation.table()), ValueUtils.parseValue(dataAnnotation.idColumn())); + List idColumns = new ArrayList<>(); + for (Field field : ReflectionUtils.getFields(clazz, PersistentValue.class)) { + IdColumn idColumnAnnotation = field.getAnnotation(IdColumn.class); + if (idColumnAnnotation == null) { + continue; + } + idColumns.add(new ColumnMetadata(ValueUtils.parseValue(idColumnAnnotation.name()), ReflectionUtils.getGenericType(field), false, false, ValueUtils.parseValue(dataAnnotation.table()), ValueUtils.parseValue(dataAnnotation.schema()))); + } + Preconditions.checkArgument(!idColumns.isEmpty(), "UniqueData class %s must have at least one @IdColumn annotated PersistentValue field", clazz.getName()); + UniqueDataMetadata metadata = new UniqueDataMetadata(clazz, ValueUtils.parseValue(dataAnnotation.schema()), ValueUtils.parseValue(dataAnnotation.table()), idColumns); uniqueDataMetadataMap.put(clazz, metadata); + + for (Field field : ReflectionUtils.getFields(clazz, Relation.class)) { + Class dependencyClass = Objects.requireNonNull(ReflectionUtils.getGenericType(field)).asSubclass(UniqueData.class); + if (!uniqueDataMetadataMap.containsKey(dependencyClass)) { + extractMetadata(dependencyClass); + } + } } public UniqueDataMetadata getMetadata(Class clazz) { @@ -70,41 +194,62 @@ public UniqueDataMetadata getMetadata(Class clazz) { return metadata; } - public void init(UniqueData uniqueData) { - Data dataAnnotation = uniqueData.getClass().getAnnotation(Data.class); - Preconditions.checkNotNull(dataAnnotation, "UniqueData class %s is missing @Data annotation", uniqueData.getClass().getName()); - for (FieldInstancePair<@Nullable PersistentValue> pair : ReflectionUtils.getFieldInstancePairs(uniqueData, PersistentValue.class)) { - Column columnAnnotation = pair.field().getAnnotation(Column.class); - Preconditions.checkNotNull(columnAnnotation, "PersistentValue field %s is missing @Column annotation", pair.field().getName()); - - String columnName = ValueUtils.parseValue(columnAnnotation.value()); - String tableName = ValueUtils.parseValue(columnAnnotation.table().isEmpty() ? dataAnnotation.table() : columnAnnotation.table()); - String schemaName = ValueUtils.parseValue(columnAnnotation.schema().isEmpty() ? dataAnnotation.schema() : columnAnnotation.schema()); - - //todo: the primary key gets a bit more complicated when we are dealing with a foreign key. this needs to be handled, and a new ForeignKey created which properly maps my id column to the foreign key column. - // for the time being, all id columns are just "id" - - PersistentValue newPv = dataAccessor.createPersistentValue( - uniqueData, - ReflectionUtils.getGenericType(pair.field()), - schemaName, - tableName, - columnName - ); - - logger.debug("Initialized PersistentValue field {}.{} -> {}.{}.{}", uniqueData.getClass().getSimpleName(), pair.field().getName(), schemaName, tableName, columnName); - - if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { - proxyPv.setDelegate(newPv); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(uniqueData, newPv); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); + @SuppressWarnings("unchecked") + public T get(Class clazz, ColumnValuePair... idColumnValues) { + ColumnValuePairs idColumns = new ColumnValuePairs(idColumnValues); + UniqueDataMetadata metadata = getMetadata(clazz); + Preconditions.checkNotNull(metadata, "UniqueData class %s has not been parsed yet", clazz.getName()); + boolean hasAllIdColumns = true; + for (ColumnMetadata idColumn : metadata.idColumns()) { + boolean found = false; + for (ColumnValuePair providedIdColumn : idColumns) { + if (idColumn.name().equals(providedIdColumn.column())) { + found = true; + break; } } + if (!found) { + hasAllIdColumns = false; + break; + } + } + + for (ColumnValuePair providedIdColumn : idColumns) { + Preconditions.checkNotNull(providedIdColumn.value(), "ID column value for column %s in UniqueData class %s cannot be null", providedIdColumn.column(), clazz.getName()); } + + Preconditions.checkArgument(hasAllIdColumns, "Not all @IdColumn columns were provided for UniqueData class %s. Required: %s, Provided: %s", clazz.getName(), metadata.idColumns(), idColumns); + + if (uniqueDataInstanceCache.containsKey(clazz) && uniqueDataInstanceCache.get(clazz).containsKey(idColumns)) { + logger.trace("Cache hit for UniqueData class {} with ID columns {}", clazz.getName(), idColumns); + return (T) uniqueDataInstanceCache.get(clazz).get(idColumns); + } + + T instance; + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + instance = constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + instance.setDataManager(this); + instance.setIdColumns(idColumns); + + Data dataAnnotation = clazz.getAnnotation(Data.class); + Preconditions.checkNotNull(dataAnnotation, "UniqueData class %s is missing @Data annotation", clazz.getName()); + String schema = ValueUtils.parseValue(dataAnnotation.schema()); + String table = ValueUtils.parseValue(dataAnnotation.table()); + PersistentValueImpl.delegate(schema, table, instance); + ReferenceImpl.delegate(instance); + + uniqueDataInstanceCache.computeIfAbsent(clazz, k -> new MapMaker().weakValues().makeMap()) + .put(idColumns, instance); + + logger.trace("Cache miss for UniqueData class {} with ID columns {}. Created new instance.", clazz.getName(), idColumns); + + return instance; } /** @@ -138,60 +283,14 @@ public void submitAsyncTask(ConnectionJedisConsumer task) { taskQueue.submitTask(task); } - public void insert(UniqueData uniqueData, boolean async) { - ConnectionConsumer task = (connection) -> { - boolean autoCommit = connection.getAutoCommit(); - connection.setAutoCommit(false); - insertIntoSource(connection, uniqueData); - connection.commit(); - connection.setAutoCommit(autoCommit); - }; - - if (async) { - submitAsyncTask(task); - } else { - submitBlockingTask(task); - } - + public void insert(InsertContext insertContext, InsertMode insertMode) { + //todo: process default values for any schemas involved. + // note: defaults should be applied by the db, not us. try { - dataAccessor.insertIntoCache(uniqueData); + insertContext.markInserted(); + dataAccessor.insert(insertContext, insertMode); } catch (SQLException e) { throw new RuntimeException(e); } } - - private void insertIntoSource(Connection connection, UniqueData uniqueData) throws SQLException { //todo: async handling and such - UniqueDataMetadata metadata = uniqueData.getMetadata(); - - Map> tables = new HashMap<>(); - tables.computeIfAbsent(metadata.schema() + "." + metadata.table(), k -> new ArrayList<>()).add(new PrimaryKey.ColumnValuePair(metadata.idColumn(), uniqueData.getId())); - for (PersistentValue pv : ReflectionUtils.getFieldInstances(uniqueData, PersistentValue.class)) { - tables.computeIfAbsent(pv.getSchema() + "." + pv.getTable(), k -> new ArrayList<>()).add(new PrimaryKey.ColumnValuePair(pv.getColumn(), pv.get())); - } - - for (Map.Entry> entry : tables.entrySet()) { - String fullTableName = entry.getKey(); - List columnValuePairs = entry.getValue(); - - StringBuilder sqlBuilder = new StringBuilder("INSERT INTO " + fullTableName + " ("); - for (PrimaryKey.ColumnValuePair pair : columnValuePairs) { - sqlBuilder.append(pair.column()).append(", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(") VALUES ("); - sqlBuilder.append("?, ".repeat(columnValuePairs.size())); - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(")"); - - //todo: on conflict : use insertion strategy for this - - String sql = sqlBuilder.toString(); - PreparedStatement preparedStatement = connection.prepareStatement(sql); - for (int i = 0; i < columnValuePairs.size(); i++) { - PrimaryKey.ColumnValuePair pair = columnValuePairs.get(i); - preparedStatement.setObject(i + 1, pair.value()); - } - preparedStatement.executeUpdate(); - } - } } diff --git a/src/main/java/net/staticstudios/data/PersistentValue.java b/src/main/java/net/staticstudios/data/PersistentValue.java index 0135c485..614db40e 100644 --- a/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/src/main/java/net/staticstudios/data/PersistentValue.java @@ -2,95 +2,126 @@ import com.google.common.base.Preconditions; import com.google.common.base.Supplier; +import net.staticstudios.data.util.ColumnMetadata; +import net.staticstudios.data.util.PersistentValueMetadata; import net.staticstudios.data.util.ValueUpdateHandler; +import net.staticstudios.data.util.ValueUpdateHandlerWrapper; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; -import java.util.Deque; -import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; -//todo: keep this as an interface, since we'll allow the data accessor decide what to use. for example are we writing to the real db or the cache. +//todo: keep this as an interface, since we'll allow the data accessor decide what to use. for example are we writing to the DB or the cache. /** * A persistent value represents a single cell in a database table. * * @param */ -public interface PersistentValue { +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 + + //todo: insert strategy, deletion strategy, update interval, update handling static PersistentValue of(UniqueData holder, Class dataType) { return new ProxyPersistentValue<>(holder, dataType); } - String getSchema(); + UniqueData getHolder(); - String getTable(); + Class getDataType(); - String getColumn(); + @ApiStatus.Internal + Map getIdColumnLinks(); - PersistentValue onUpdate(ValueUpdateHandler updateHandler); + PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler); PersistentValue withDefault(@Nullable T defaultValue); PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier); - T get(); - void set(T value); +// PersistentValue updateInterval(long intervalMillis); class ProxyPersistentValue implements PersistentValue { protected final UniqueData holder; protected final Class dataType; - private final Deque> updateHandlers = new ConcurrentLinkedDeque<>(); + private final List> updateHandlers = new ArrayList<>(); + //todo: here's how update handlers should be stored + // we store them globally on the data manager. we have a map of > + // additionally, for every field of type PV, we store the update handlers once - after the first unique data object is created. we will set them right before setting the delegate. + // this also means that after weve set the delegate, we cannot add any more update handlers. + // i think it would be useful to expose a method on the DM publicly to add an update handler to a specific column private @Nullable Supplier<@Nullable T> defaultValueSupplier; private @Nullable PersistentValue delegate; - + private Map idColumnLinks = Collections.emptyMap(); + private long updateIntervalMillis = -1; public ProxyPersistentValue(UniqueData holder, Class dataType) { this.holder = holder; this.dataType = dataType; } - public void setDelegate(PersistentValue delegate) { + public void setDelegate(ColumnMetadata columnMetadata, PersistentValue delegate) { Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); - this.delegate = (PersistentValue) delegate; - for (ValueUpdateHandler handler : updateHandlers) { - this.delegate.onUpdate(handler); - } - this.updateHandlers.clear(); + this.delegate = delegate; +// for (ValueUpdateHandler handler : updateHandlers) { +// this.delegate.onUpdate(handler); +// } +// this.updateHandlers.clear(); if (this.defaultValueSupplier != null) { this.delegate.withDefault(this.defaultValueSupplier); } - } - @Override - public String getSchema() { - Preconditions.checkState(delegate != null, "Delegate is not set"); - return delegate.getSchema(); + PersistentValueMetadata metadata = new PersistentValueMetadata( + holder.getClass(), + columnMetadata.schema(), + columnMetadata.table(), + columnMetadata.name(), + dataType + ); + + holder.getDataManager().registerUpdateHandler(metadata, updateHandlers); } @Override - public String getTable() { - Preconditions.checkState(delegate != null, "Delegate is not set"); - return delegate.getTable(); + public UniqueData getHolder() { + return holder; } @Override - public String getColumn() { - Preconditions.checkState(delegate != null, "Delegate is not set"); - return delegate.getColumn(); + public Class getDataType() { + return dataType; } @Override - public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { - Preconditions.checkNotNull(updateHandler, "Update handler cannot be null"); + public Map getIdColumnLinks() { + return idColumnLinks; + } +// +// @Override +// public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { +// Preconditions.checkNotNull(updateHandler, "Update handler cannot be null"); +// +// if (delegate != null) { +// delegate.onUpdate(updateHandler); +// } else { +// this.updateHandlers.add(updateHandler); +// } +// +// return this; +// } - if (delegate != null) { - delegate.onUpdate(updateHandler); - } else { - this.updateHandlers.add(updateHandler); - } + @Override + public PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler) { + Preconditions.checkArgument(delegate == null, "Cannot dynamically add an update handler after the holder has been initialized!"); + ValueUpdateHandlerWrapper wrapper = new ValueUpdateHandlerWrapper<>(updateHandler, dataType, holderClass); + this.updateHandlers.add(wrapper); return this; } @@ -104,6 +135,16 @@ public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultVal return this; } +// @Override +// public PersistentValue updateInterval(long intervalMillis) { +// if (delegate != null) { +// delegate.updateInterval(intervalMillis); +// return this; +// } +// this.updateIntervalMillis = intervalMillis; +// return this; +// } + @Override public PersistentValue withDefault(@Nullable T defaultValue) { return withDefault(() -> defaultValue); diff --git a/src/main/java/net/staticstudios/data/Reference.java b/src/main/java/net/staticstudios/data/Reference.java new file mode 100644 index 00000000..3f7f290b --- /dev/null +++ b/src/main/java/net/staticstudios/data/Reference.java @@ -0,0 +1,53 @@ +package net.staticstudios.data; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.Nullable; + +public interface Reference extends Relation { + + UniqueData getHolder(); + + Class getReferenceType(); + + T get(); + + void set(T value); + + class ProxyReference implements Reference { + private final UniqueData holder; + private final Class referenceType; + private @Nullable Reference delegate; + + public ProxyReference(UniqueData holder, Class referenceType) { + this.holder = holder; + this.referenceType = referenceType; + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getReferenceType() { + return referenceType; + } + + @Override + public T get() { + Preconditions.checkState(delegate != null, "Reference has not been initialized yet"); + return delegate.get(); + } + + @Override + public void set(T value) { + Preconditions.checkState(delegate != null, "Reference has not been initialized yet"); + delegate.set(value); + } + + public void setDelegate(Reference delegate) { + Preconditions.checkState(this.delegate == null, "Delegate has already been set"); + this.delegate = delegate; + } + } +} diff --git a/src/main/java/net/staticstudios/data/Relation.java b/src/main/java/net/staticstudios/data/Relation.java index 226a4c60..57b98b3f 100644 --- a/src/main/java/net/staticstudios/data/Relation.java +++ b/src/main/java/net/staticstudios/data/Relation.java @@ -1,4 +1,4 @@ package net.staticstudios.data; -public interface Relation { +public interface Relation { } diff --git a/src/main/java/net/staticstudios/data/UniqueData.java b/src/main/java/net/staticstudios/data/UniqueData.java index 15a0e04c..6bc1f1e9 100644 --- a/src/main/java/net/staticstudios/data/UniqueData.java +++ b/src/main/java/net/staticstudios/data/UniqueData.java @@ -1,30 +1,36 @@ package net.staticstudios.data; import net.staticstudios.data.parse.UniqueDataMetadata; -import net.staticstudios.data.util.PrimaryKey; +import net.staticstudios.data.util.ColumnValuePairs; +import org.jetbrains.annotations.ApiStatus; -import java.util.List; -import java.util.UUID; +public abstract class UniqueData { + private ColumnValuePairs idColumns; + private DataManager dataManager; -public abstract class UniqueData implements PrimaryKey { - private final DataManager dataManager; - - public UniqueData(DataManager dataManager) { + //todo: when an update is done to an id column, we need to handle it here. + //todo: when this row is deleted from the database, we should mark this with a deleted flag and throw an error if any operations are attempted on it. more specifically, any pvs referencing this object should throw an error if this has been deleted. + @ApiStatus.Internal + protected final void setDataManager(DataManager dataManager) { this.dataManager = dataManager; } - public abstract UUID getId(); + @ApiStatus.Internal + protected final synchronized void setIdColumns(ColumnValuePairs idColumns) { + this.idColumns = idColumns; + } public DataManager getDataManager() { return dataManager; } - @Override - public List getWhereClause() { - return List.of(new ColumnValuePair(getMetadata().idColumn(), getId())); + public synchronized ColumnValuePairs getIdColumns() { + return idColumns; } public final UniqueDataMetadata getMetadata() { return dataManager.getMetadata(this.getClass()); } + + //todo: toString, equals, hashcode - all based on the id columns and class type } diff --git a/src/main/java/net/staticstudios/data/Value.java b/src/main/java/net/staticstudios/data/Value.java new file mode 100644 index 00000000..cd400ac3 --- /dev/null +++ b/src/main/java/net/staticstudios/data/Value.java @@ -0,0 +1,7 @@ +package net.staticstudios.data; + +public interface Value { + T get(); + + void set(T value); +} diff --git a/src/main/java/net/staticstudios/data/delete/Delete.java b/src/main/java/net/staticstudios/data/delete/Delete.java new file mode 100644 index 00000000..29506bdd --- /dev/null +++ b/src/main/java/net/staticstudios/data/delete/Delete.java @@ -0,0 +1,9 @@ +package net.staticstudios.data.delete; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Delete { + DeleteStrategy value() default DeleteStrategy.NO_ACTION; +} diff --git a/src/main/java/net/staticstudios/data/delete/DeleteStrategy.java b/src/main/java/net/staticstudios/data/delete/DeleteStrategy.java new file mode 100644 index 00000000..ad0018a0 --- /dev/null +++ b/src/main/java/net/staticstudios/data/delete/DeleteStrategy.java @@ -0,0 +1,18 @@ +package net.staticstudios.data.delete; + +public enum DeleteStrategy { + /** + * When the parent holder is deleted, delete this data as well. + */ + CASCADE, + /** + * Do nothing when the parent holder is deleted. + */ + NO_ACTION, + /** + * This is only for use in PersistentCollections created via + * {@link PersistentCollection#oneToMany} or + * {@link PersistentCollection#manyToMany} + */ + UNLINK //todo: this +} diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java new file mode 100644 index 00000000..c871d1f3 --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -0,0 +1,218 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import net.staticstudios.data.DataAccessor; +import net.staticstudios.data.PersistentValue; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.Column; +import net.staticstudios.data.util.*; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Nullable; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +public class PersistentValueImpl implements PersistentValue { + private final DataAccessor dataAccessor; + private final UniqueData holder; + private final Class dataType; + private final String schema; + private final String table; + private final String column; + // private final Deque> updateHandlers = new ConcurrentLinkedDeque<>(); + private Map idColumnLinks; + private @Nullable Supplier<@Nullable T> defaultValueSupplier; + + private PersistentValueImpl(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, Map idColumnLinks) { + this.dataAccessor = dataAccessor; + this.holder = holder; + this.dataType = dataType; + this.schema = schema; + this.table = table; + this.column = column; + this.idColumnLinks = idColumnLinks; + } + + public static void createAndDelegate(ProxyPersistentValue proxy, ColumnMetadata columnMetadata) { + PersistentValueImpl delegate = new PersistentValueImpl<>( + proxy.getHolder().getDataManager().getDataAccessor(), + proxy.getHolder(), + proxy.getDataType(), + columnMetadata.schema(), + columnMetadata.table(), + columnMetadata.name(), + proxy.getIdColumnLinks() + ); + + proxy.setDelegate(columnMetadata, delegate); + } + + public static PersistentValueImpl create(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, Map idColumnLinks) { + return new PersistentValueImpl<>(dataAccessor, holder, dataType, schema, table, column, idColumnLinks); + } + + public static void delegate(String schema, String table, T instance) { + for (FieldInstancePair<@Nullable PersistentValue> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentValue.class)) { + IdColumn idColumn = pair.field().getAnnotation(IdColumn.class); + Column columnAnnotation = pair.field().getAnnotation(Column.class); + ForeignColumn foreignColumn = pair.field().getAnnotation(ForeignColumn.class); + ColumnMetadata columnMetadata = null; + Map idColumnLinks = Collections.emptyMap(); + if (idColumn != null) { + Preconditions.checkArgument(columnAnnotation == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @Column", pair.field().getName()); + Preconditions.checkArgument(foreignColumn == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @ForeignColumn", pair.field().getName()); + columnMetadata = new ColumnMetadata(ValueUtils.parseValue(idColumn.name()), ReflectionUtils.getGenericType(pair.field()), false, false, table, schema); + } else if (columnAnnotation != null) { + columnMetadata = new ColumnMetadata(ValueUtils.parseValue(columnAnnotation.name()), ReflectionUtils.getGenericType(pair.field()), columnAnnotation.nullable(), columnAnnotation.index(), + columnAnnotation.table().isEmpty() ? table : ValueUtils.parseValue(columnAnnotation.table()), + columnAnnotation.schema().isEmpty() ? schema : ValueUtils.parseValue(columnAnnotation.schema())); + } else if (foreignColumn != null) { + columnMetadata = new ColumnMetadata(ValueUtils.parseValue(foreignColumn.name()), ReflectionUtils.getGenericType(pair.field()), foreignColumn.nullable(), foreignColumn.index(), + foreignColumn.table().isEmpty() ? table : ValueUtils.parseValue(foreignColumn.table()), + foreignColumn.schema().isEmpty() ? schema : ValueUtils.parseValue(foreignColumn.schema())); + idColumnLinks = new HashMap<>(); + List links = StringUtils.parseCommaSeperatedList(foreignColumn.link()); + for (String link : links) { + String[] parts = link.split("="); + Preconditions.checkArgument(parts.length == 2, "ForeignColumn link must be in the format localColumn=foreignColumn, got: %s", link); + idColumnLinks.put(ValueUtils.parseValue(parts[0]), ValueUtils.parseValue(parts[1])); + } + } + Preconditions.checkNotNull(columnMetadata, "PersistentValue field %s is missing @Column annotation", pair.field().getName()); + + //todo: the primary key gets a bit more complicated when we are dealing with a foreign key. this needs to be handled, and a new ForeignKey created which properly maps my id column to the foreign key column. + //todo: update: what??? + + if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { + PersistentValueImpl.createAndDelegate(proxyPv, columnMetadata); + } else { + pair.field().setAccessible(true); + try { + pair.field().set(instance, PersistentValueImpl.create(instance.getDataManager().getDataAccessor(), instance, pair.field().getType(), columnMetadata.schema(), columnMetadata.table(), columnMetadata.name(), idColumnLinks)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getDataType() { + return dataType; + } + +// @Override +// public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { +// updateHandlers.add(updateHandler); +// return this; +// } + + + @Override + public PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler) { + throw new UnsupportedOperationException("Dynamically adding update handlers is not supported"); + } + + @Override + public PersistentValue withDefault(@Nullable T defaultValue) { + return withDefault(() -> defaultValue); + } + + @Override + public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier) { + this.defaultValueSupplier = defaultValueSupplier; + return this; + } + + @Override + public Map getIdColumnLinks() { + return idColumnLinks; + } + + @Override + public T get() { + StringBuilder sqlBuilder = new StringBuilder().append("SELECT \"").append(column).append("\" FROM \"").append(schema).append("\".\"").append(table).append("\" WHERE "); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append("\"").append(name).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())) { + Object serializedValue = null; + if (rs.next()) { + serializedValue = rs.getObject(column); + } + if (serializedValue != null) { + T deserialized = (T) serializedValue; //todo: this + return deserialized; + } + if (defaultValueSupplier != null) { + return defaultValueSupplier.get(); + } + return null; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public void set(T value) { + //todo: whenever we set an id column of something, we need to tell the datamanager to update any tracked instance of uniquedata with that id. + T oldValue = get(); + StringBuilder sqlBuilder; + if (idColumnLinks.isEmpty()) { + sqlBuilder = new StringBuilder().append("UPDATE \"").append(schema).append("\".\"").append(table).append("\" SET \"").append(column).append("\" = ? WHERE "); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append("\"").append(name).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + } else { // we're dealing with a foreign key + sqlBuilder = new StringBuilder().append("MERGE INTO \"").append(schema).append("\".\"").append(table).append("\" target USING (VALUES (?"); + sqlBuilder.append(", ?".repeat(holder.getIdColumns().getPairs().length)); + sqlBuilder.append(")) AS source (\"").append(column).append("\""); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append(", \"").append(name).append("\""); + } + sqlBuilder.append(") ON "); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append("target.\"").append(name).append("\" = source.\"").append(name).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + sqlBuilder.append(" WHEN MATCHED THEN UPDATE SET \"").append(column).append("\" = source.\"").append(column).append("\" WHEN NOT MATCHED THEN INSERT (\"").append(column).append("\""); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append(", \"").append(name).append("\""); + } + sqlBuilder.append(") VALUES (source.\"").append(column).append("\""); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append(", source.\"").append(name).append("\""); + } + sqlBuilder.append(")"); + } + @Language("SQL") String sql = sqlBuilder.toString(); + List values = new ArrayList<>(1 + holder.getIdColumns().getPairs().length); + values.add(value); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + values.add(columnValuePair.value()); + } + try { + dataAccessor.executeUpdate(sql, values); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + //todo: support set with SetMode, or operationMode (SYNC vs ASYNC) +} diff --git a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java new file mode 100644 index 00000000..3a334da6 --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -0,0 +1,157 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.DataAccessor; +import net.staticstudios.data.Reference; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.UniqueDataMetadata; +import net.staticstudios.data.util.*; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Nullable; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ReferenceImpl implements Reference { + private final UniqueDataMetadata referenceMetadata; + private final UniqueData holder; + private final Class type; + private final Map link; + + public ReferenceImpl(UniqueData holder, Class type, UniqueDataMetadata referenceMetadata, Map link) { + this.holder = holder; + this.type = type; + this.referenceMetadata = referenceMetadata; + this.link = link; + } + + public static void createAndDelegate(Reference.ProxyReference proxy, Map link) { + ReferenceImpl delegate = new ReferenceImpl<>( + proxy.getHolder(), + proxy.getReferenceType(), + proxy.getHolder().getDataManager().getMetadata(proxy.getReferenceType()), + link + ); + proxy.setDelegate(delegate); + } + + public static ReferenceImpl create(UniqueData holder, Class type, UniqueDataMetadata referenceMetadata, Map link) { + return new ReferenceImpl<>(holder, type, referenceMetadata, link); + } + + public static void delegate(T instance) { + for (FieldInstancePair<@Nullable Reference> pair : ReflectionUtils.getFieldInstancePairs(instance, Reference.class)) { + OneToOne oneToOneAnnotation = pair.field().getAnnotation(OneToOne.class); + Preconditions.checkNotNull(oneToOneAnnotation, "Field %s in class %s is missing @OneToOne annotation".formatted(pair.field().getName(), instance.getClass().getName())); + Class referencedClass = ReflectionUtils.getGenericType(pair.field()); + Preconditions.checkNotNull(referencedClass, "Field %s in class %s is not parameterized".formatted(pair.field().getName(), instance.getClass().getName())); + UniqueDataMetadata referenceMetadata = instance.getDataManager().getMetadata((Class) referencedClass); + Map link = new HashMap<>(); + for (String l : StringUtils.parseCommaSeperatedList(oneToOneAnnotation.link())) { + String[] split = l.split("="); + Preconditions.checkArgument(split.length == 2, "Invalid link format in @OneToOne annotation on field %s in class %s".formatted(pair.field().getName(), instance.getClass().getName())); + link.put(ValueUtils.parseValue(split[0].trim()), ValueUtils.parseValue(split[1].trim())); + } + + if (pair.instance() instanceof Reference.ProxyReference proxyRef) { + createAndDelegate(proxyRef, link); + } else { + pair.field().setAccessible(true); + try { + pair.field().set(instance, create(instance, (Class) referencedClass, referenceMetadata, link)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getReferenceType() { + return type; + } + + @Override + public T get() { + ColumnValuePair[] idColumns = new ColumnValuePair[link.size()]; + int i = 0; + UniqueDataMetadata holderMetadata = holder.getMetadata(); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + for (Map.Entry entry : link.entrySet()) { + String myColumn = entry.getKey(); + String theirColumn = entry.getValue(); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT \"").append(myColumn).append("\" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.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())) { + if (rs.next()) { + if (rs.getObject(myColumn) == null) { + return null; + } + idColumns[i++] = new ColumnValuePair(theirColumn, rs.getObject(myColumn)); + } else { + return null; + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + return holder.getDataManager().get(type, idColumns); + } + + @Override + public void set(T value) { + //todo: set local columns to the value's id columns + UniqueDataMetadata holderMetadata = holder.getMetadata(); + List values = new ArrayList<>(); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("UPDATE \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" SET "); + for (Map.Entry entry : link.entrySet()) { + String myColumn = entry.getKey(); + String theirColumn = entry.getValue(); + Object theirValue = null; + for (ColumnValuePair columnValuePair : value.getIdColumns()) { + if (columnValuePair.column().equals(theirColumn)) { + theirValue = columnValuePair.value(); + break; + } + } + Preconditions.checkNotNull(theirValue, "Could not find value for column %s in referenced object of type %s".formatted(theirColumn, type.getName())); + + sqlBuilder.append("\"").append(myColumn).append("\" = ?, "); + values.add(theirValue); + } + + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" WHERE "); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + values.add(columnValuePair.value()); + } + + + try { + holder.getDataManager().getDataAccessor().executeUpdate(sqlBuilder.toString(), values); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java new file mode 100644 index 00000000..0217de7c --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -0,0 +1,320 @@ +package net.staticstudios.data.impl.h2; + +import com.impossibl.postgres.api.jdbc.PGConnection; +import net.staticstudios.data.DataAccessor; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.impl.pg.PostgresListener; +import net.staticstudios.data.insert.InsertContext; +import net.staticstudios.data.insert.InsertMode; +import net.staticstudios.data.util.ColumnMetadata; +import net.staticstudios.data.util.SQlStatement; +import net.staticstudios.data.util.TaskQueue; +import org.intellij.lang.annotations.Language; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.*; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * This data accessor uses a write-behind caching strategy with an in-memory H2 database to optimize read and write operations. + */ +public class H2DataAccessor implements DataAccessor { + private static final Logger logger = LoggerFactory.getLogger(H2DataAccessor.class); + private final TaskQueue taskQueue; + private final String jdbcUrl; + private final ThreadLocal threadConnection = new ThreadLocal<>(); + private final ThreadLocal> threadPreparedStatementCache = new ThreadLocal<>(); + private final Set knownTables = new HashSet<>(); + private final DataManager dataManager; + private final PostgresListener postgresListener; + + public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener, TaskQueue taskQueue) { + this.taskQueue = taskQueue; + this.postgresListener = postgresListener; + this.jdbcUrl = "jdbc:h2:mem:static-data-cache;DB_CLOSE_DELAY=-1;LOCK_MODE=0;CACHE_SIZE=65536"; + this.dataManager = dataManager; + + postgresListener.addHandler(notification -> { + switch (notification.getOperation()) { //todo: update our cache + case UPDATE -> { + } + case INSERT -> { + } + case DELETE -> { + } + } + }); + } + + public synchronized void sync(String schema, String table) throws SQLException { + //todo: when we resync we should clear the task queue and steal the connection + //todo: if possible, id like to pause everything else until we are done syncing + dataManager.submitBlockingTask(realDbConnection -> { + Path tmpFile = Paths.get(System.getProperty("java.io.tmpdir"), schema + "_" + table + ".csv"); + String absolutePath = tmpFile.toAbsolutePath().toString(); + + List columns = getColumnsInTable(schema, table); + + StringBuilder sqlBuilder = new StringBuilder("COPY (SELECT "); + for (String column : columns) { + sqlBuilder.append("\"").append(column).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(schema).append("\".\"").append(table).append("\") TO STDOUT WITH CSV HEADER"); + @Language("SQL") String copySql = sqlBuilder.toString(); + + @Language("SQL") String truncateSql = "TRUNCATE TABLE \"" + schema + "\".\"" + table + "\""; + StringBuilder insertSqlBuilder = new StringBuilder("INSERT INTO \"").append(schema).append("\".\"").append(table).append("\" ("); + for (String column : columns) { + insertSqlBuilder.append("\"").append(column).append("\", "); + } + insertSqlBuilder.setLength(insertSqlBuilder.length() - 2); + insertSqlBuilder.append(") SELECT * FROM CSVREAD('").append(absolutePath).append("')"); + String insertSql = insertSqlBuilder.toString(); + PGConnection pgConnection = realDbConnection.unwrap(PGConnection.class); + FileOutputStream fileOutputStream; + try { + fileOutputStream = new FileOutputStream(absolutePath); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + logger.debug("[DB] {}", copySql); + pgConnection.copyTo(copySql, fileOutputStream); + Connection h2Connection = getConnection(); + boolean autoCommit = h2Connection.getAutoCommit(); + try ( + Statement h2Statement = h2Connection.createStatement() + ) { + h2Connection.setAutoCommit(false); + logger.trace("[H2] {}", truncateSql); + h2Statement.execute(truncateSql); + logger.trace("[H2] {}", insertSql); + h2Statement.execute(insertSql); + } finally { + if (autoCommit) { + h2Connection.setAutoCommit(true); + } + } + }); + + //todo: start listening to changes from pg + // then log them + // load appropriate data into h2 + // process logs + // then continue as normal + } + + private Connection getConnection() throws SQLException { + Connection connection = threadConnection.get(); + if (connection == null) { + connection = DriverManager.getConnection(jdbcUrl); + connection.setAutoCommit(false); + threadConnection.set(connection); + } + return connection; + } + + public PreparedStatement prepareStatement(@Language("SQL") String sql) throws SQLException { + Connection connection = getConnection(); + Map preparedStatementCache = threadPreparedStatementCache.get(); + if (preparedStatementCache == null) { + preparedStatementCache = new HashMap<>(); + threadPreparedStatementCache.set(preparedStatementCache); + } + + PreparedStatement preparedStatement = preparedStatementCache.get(sql); + if (preparedStatement == null) { + preparedStatement = connection.prepareStatement(sql); + preparedStatementCache.put(sql, preparedStatement); + } + + return preparedStatement; + } + + @Override + public void insert(InsertContext insertContext, InsertMode insertMode) throws SQLException { + Connection connection = getConnection(); + boolean autoCommit = connection.getAutoCommit(); + try { + connection.setAutoCommit(false); + List sqlStatements = new ArrayList<>(); + Map>> columnsByTable = new HashMap<>(); + for (Map.Entry entry : insertContext.getEntries().entrySet()) { + ColumnMetadata column = entry.getKey(); + columnsByTable.computeIfAbsent(column.schema(), k -> new HashMap<>()) + .computeIfAbsent(column.table(), k -> new ArrayList<>()) + .add(column); + } + + for (Map.Entry>> schemaEntry : columnsByTable.entrySet()) { + String schema = schemaEntry.getKey(); + for (Map.Entry> tableEntry : schemaEntry.getValue().entrySet()) { + String table = tableEntry.getKey(); + List columns = tableEntry.getValue(); + + StringBuilder sqlBuilder = new StringBuilder("INSERT INTO \""); + sqlBuilder.append(schema).append("\".\"").append(table).append("\" ("); + for (ColumnMetadata column : columns) { + sqlBuilder.append("\"").append(column.name()).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") VALUES ("); + sqlBuilder.append("?, ".repeat(columns.size())); + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(")"); + + String sql = sqlBuilder.toString(); + List values = new ArrayList<>(); + try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + for (int i = 0; i < columns.size(); i++) { + ColumnMetadata column = columns.get(i); + Object value = insertContext.getEntries().get(column); + preparedStatement.setObject(i + 1, value); + values.add(value); + } + logger.debug("[H2] {}", sql); + sqlStatements.add(new SQlStatement(sql, values)); + preparedStatement.executeUpdate(); + } + } + } + + CompletableFuture future = taskQueue.submitTask(realConnection -> { + boolean realAutoCommit = realConnection.getAutoCommit(); + realConnection.setAutoCommit(false); + try { + for (SQlStatement statement : sqlStatements) { + try (PreparedStatement preparedStatement = realConnection.prepareStatement(statement.getSql())) { + List values = statement.getValues(); + for (int i = 0; i < values.size(); i++) { + preparedStatement.setObject(i + 1, values.get(i)); + } + logger.debug("[DB] {}", statement.getSql()); + preparedStatement.executeUpdate(); + } + } + } finally { + if (realAutoCommit) { + realConnection.setAutoCommit(true); + } + } + }); + + if (insertMode == InsertMode.SYNC) { + try { + future.join(); + } catch (CompletionException e) { + connection.rollback(); + } + } + } finally { + if (autoCommit) { + connection.setAutoCommit(true); + } + } + } + + @Override + public ResultSet executeQuery(@Language("SQL") String sql, List values) throws SQLException { + PreparedStatement cachePreparedStatement = prepareStatement(sql); + for (int i = 0; i < values.size(); i++) { + cachePreparedStatement.setObject(i + 1, values.get(i)); + } + logger.debug("[H2] {}", sql); + return cachePreparedStatement.executeQuery(); + } + + @Override + public void executeUpdate(@Language("SQL") String sql, List values) throws SQLException { + PreparedStatement cachePreparedStatement = prepareStatement(sql); + for (int i = 0; i < values.size(); i++) { + cachePreparedStatement.setObject(i + 1, values.get(i)); + } + logger.debug("[H2] {}", sql); + cachePreparedStatement.executeUpdate(); + + taskQueue.submitTask(connection -> { + PreparedStatement realPreparedStatement = connection.prepareStatement(sql); + for (int i = 0; i < values.size(); i++) { + realPreparedStatement.setObject(i + 1, values.get(i)); + } + logger.debug("[DB] {}}", sql); + realPreparedStatement.executeUpdate(); + }); + } + + @Override + public void runDDL(String sql) { + taskQueue.submitTask(connection -> { + logger.debug("[DB] {}", sql); + connection.createStatement().execute(sql); + try (Statement statement = getConnection().createStatement()) { + logger.trace("[H2] {}", sql); + statement.execute(sql); + } + }).join(); + + } + + @Override + public void postDDL() throws SQLException { + updateKnownTables(); + } + + private synchronized void updateKnownTables() throws SQLException { + Set currentTables = new HashSet<>(); + Connection connection = getConnection(); + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery( + "SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA NOT IN ('INFORMATION_SCHEMA', 'SYSTEM_LOBS') AND TABLE_TYPE='BASE TABLE'")) { + while (rs.next()) { + String schema = rs.getString("TABLE_SCHEMA"); + String table = rs.getString("TABLE_NAME"); + currentTables.add(schema + "." + table); + + if (!knownTables.contains(schema + "." + table)) { + logger.trace("Discovered new table {}.{}", schema, table); + UUID randomId = UUID.randomUUID(); + @Language("SQL") String sql = "CREATE TRIGGER IF NOT EXISTS \"trg_%s_%s\" AFTER INSERT, UPDATE, DELETE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; + + try (Statement createTrigger = connection.createStatement()) { + H2Trigger.registerDataManager(randomId, dataManager); + createTrigger.execute(sql.formatted(table, randomId.toString().replace('-', '_'), schema, table, H2Trigger.class.getName())); + } + + dataManager.submitBlockingTask(realDbConnection -> postgresListener.ensureTableHasTrigger(realDbConnection, schema, table)); + sync(schema, table); + } + } + } + knownTables.clear(); + knownTables.addAll(currentTables); + } + + private List getColumnsInTable(String schema, String table) throws SQLException { + List columns = new ArrayList<>(); + try (PreparedStatement ps = getConnection().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); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + columns.add(rs.getString("COLUMN_NAME")); + } + } + } + return columns; + } +} + + +//todo: maintain a buffer of what to send the the real db, and then collapse similar prepared statements into one so we can batch them. have a configurable interval to flush, but by default this will be 0ms + diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java b/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java new file mode 100644 index 00000000..ba107eed --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java @@ -0,0 +1,90 @@ +package net.staticstudios.data.impl.h2; + +import net.staticstudios.data.DataManager; +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 H2Trigger implements Trigger { + private static final Map dataManagerMap = new ConcurrentHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(H2Trigger.class); + private final List columnNames = new ArrayList<>(); + private DataManager dataManager; + 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 table name since it might be a copy for an internal table (very odd behavior i must say h2) + this.dataManager = dataManagerMap.get(dataManagerId); + this.schema = schemaName; + } + + @Override + public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException { + //todo: when were loading our initial data, we should ignore all triggers. + 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 column names: {}, new column names: {}", columnNames, columns); + columnNames.clear(); + columnNames.addAll(columns); + } + + + if (oldRow == null && newRow != null) { + logger.trace("Insert detected: newRow={}", (Object) newRow); + handleInsert(newRow); + } else 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 handleInsert(Object[] 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)); + } + } + + for (String changedColumn : changedColumns) { + dataManager.callUpdateHandlers(columnNames, schema, table, changedColumn, oldRow, newRow); + } + } + + private void handleDelete(Object[] oldRow) { + } +} diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresData.java b/src/main/java/net/staticstudios/data/impl/pg/PostgresData.java new file mode 100644 index 00000000..09950eee --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/pg/PostgresData.java @@ -0,0 +1,9 @@ +package net.staticstudios.data.impl.pg; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +public record PostgresData(@SerializedName("new") Map newDataValueMap, + @SerializedName("old") Map oldDataValueMap) { +} diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java b/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java new file mode 100644 index 00000000..7557c581 --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java @@ -0,0 +1,192 @@ +package net.staticstudios.data.impl.pg; + +import com.google.gson.Gson; +import com.impossibl.postgres.api.jdbc.PGConnection; +import com.impossibl.postgres.api.jdbc.PGNotificationListener; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.util.DataSourceConfig; +import net.staticstudios.utils.ShutdownStage; +import net.staticstudios.utils.ThreadUtils; +import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class PostgresListener { + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSX"); + public static String CREATE_DATA_NOTIFY_FUNCTION = """ + create or replace function propagate_data_update_v3() returns trigger as $$ + declare + notification text; + begin + notification := to_char(current_timestamp AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.FF6"Z"') || ',' || tg_table_schema || ',' || TG_TABLE_NAME || ',' || TG_OP || ',' || current_setting('application_name') || ',' || + json_build_object( + 'old', (case when TG_OP = 'INSERT' then '{}' else row_to_json(OLD) end), + 'new', (case when TG_OP = 'DELETE' then '{}' else row_to_json(NEW) end) + )::text; + + perform pg_notify('data_notification_v3', notification); + + return new; + end; + $$ language plpgsql; + """; + public static String CREATE_TRIGGER = """ + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_trigger + WHERE tgname = 'propagate_data_update_v3_trigger' + AND tgrelid = '%s'::regclass + ) THEN + CREATE TRIGGER propagate_data_update_v3_trigger + AFTER INSERT OR UPDATE OR DELETE ON %s + FOR EACH ROW EXECUTE PROCEDURE propagate_data_update_v3(); + END IF; + END; + $$ + """; + private final Logger logger = LoggerFactory.getLogger(PostgresListener.class); + private final Set tablesTriggered = Collections.synchronizedSet(new HashSet<>()); + private final ConcurrentLinkedDeque> notificationHandlers = new ConcurrentLinkedDeque<>(); + private final Gson gson = new Gson(); + private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + public @VisibleForTesting PGConnection pgConnection; + + public PostgresListener(DataManager dataManager, DataSourceConfig ds) { + try { + Class.forName("com.impossibl.postgres.jdbc.PGDriver"); + + setPgConnection(dataManager, ds); + + scheduledExecutorService.scheduleAtFixedRate(() -> { + if (ThreadUtils.isShuttingDown()) { + return; + } + try { + if (pgConnection.isClosed()) { + logger.warn("Connection closed, re-establishing connection"); + try { + setPgConnection(dataManager, ds); + } catch (SQLException e) { + logger.error("Error re-establishing connection", e); + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }, 1, 1, TimeUnit.SECONDS); + + } catch (SQLException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + logger.debug("Notification listener started"); + + ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, () -> { + try { + scheduledExecutorService.shutdownNow(); + pgConnection.close(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + private void setPgConnection(DataManager dataManager, DataSourceConfig ds) throws SQLException { + this.pgConnection = DriverManager.getConnection("jdbc:pgsql://" + ds.databaseHost() + ":" + ds.databasePort() + "/" + ds.databaseName(), ds.databaseUsername(), ds.databasePassword()).unwrap(PGConnection.class); + + try (Statement statement = pgConnection.createStatement()) { + logger.trace("Creating data_notify function"); + statement.execute(CREATE_DATA_NOTIFY_FUNCTION); + } + + pgConnection.addNotificationListener("data_notification_v3", new PGNotificationListener() { + @Override + public void notification(int processId, String channelName, String payload) { + logger.trace("Received notification. PID: {}, Channel: {}, Payload: {}", processId, channelName, payload); + String[] parts = payload.split(",", 6); + String encodedTimestamp = parts[0]; + String schema = parts[1]; + String table = parts[2]; + String encodedOperation = parts[3]; + String applicationName = parts[4]; + String encodedData = parts[5]; + + //Filter out notifications from this application (data manager session) + if (dataManager.getApplicationName().equals(applicationName)) { + logger.trace("Ignoring notification from this session"); + return; + } + + PostgresData data = gson.fromJson(encodedData, PostgresData.class); + + OffsetDateTime offsetDateTime = OffsetDateTime.parse(encodedTimestamp, DATE_TIME_FORMATTER); + + PostgresNotification notification = new PostgresNotification( + offsetDateTime.toInstant(), + schema, + table, + PostgresOperation.valueOf(encodedOperation), + data + ); + + for (Consumer handler : notificationHandlers) { + try { + handler.accept(notification); + } catch (Exception e) { + logger.error("Error handling notification", e); + } + } + } + }); + + try (Statement statement = pgConnection.createStatement()) { + statement.execute("LISTEN data_notification_v3"); + } + } + + public void addHandler(Consumer handler) { + notificationHandlers.add(handler); + } + + + /** + * Whenever we see a new table, make sure the trigger is added to it + * + * @param connection the connection to the database + * @param schema the schema of the table + * @param table the table to ensure has the trigger + */ + public void ensureTableHasTrigger(Connection connection, String schema, String table) { + String schemaTable = schema + "." + table; + if (tablesTriggered.contains(schemaTable)) { + return; + } + + String sql = CREATE_TRIGGER.formatted(schemaTable, schemaTable); + logger.debug("Adding propagate_data_update_trigger to table: {}", schemaTable); + + try (Statement statement = connection.createStatement()) { + statement.execute(sql); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + tablesTriggered.add(schemaTable); + } +} \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java b/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java new file mode 100644 index 00000000..0571efc9 --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java @@ -0,0 +1,50 @@ +package net.staticstudios.data.impl.pg; + +import java.time.Instant; + +public class PostgresNotification { + private final Instant instant; + private final String schema; + private final String table; + private final PostgresOperation operation; + private final PostgresData data; + + public PostgresNotification(Instant instant, String schema, String table, PostgresOperation operation, PostgresData data) { + this.instant = instant; + this.schema = schema; + this.table = table; + this.operation = operation; + this.data = data; + } + + public Instant getInstant() { + return instant; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public PostgresOperation getOperation() { + return operation; + } + + public PostgresData getData() { + return data; + } + + @Override + public String toString() { + return "PostgresNotification{" + + "instant=" + instant + + ", schema='" + schema + '\'' + + ", table='" + table + '\'' + + ", operation=" + operation + + ", data=" + data + + '}'; + } +} diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java b/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java new file mode 100644 index 00000000..c45ffe95 --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.impl.pg; + +public enum PostgresOperation { + INSERT, + UPDATE, + DELETE +} diff --git a/src/main/java/net/staticstudios/data/impl/sqlite/SQLiteDataAccessor.java b/src/main/java/net/staticstudios/data/impl/sqlite/SQLiteDataAccessor.java deleted file mode 100644 index 2f4e838e..00000000 --- a/src/main/java/net/staticstudios/data/impl/sqlite/SQLiteDataAccessor.java +++ /dev/null @@ -1,128 +0,0 @@ -package net.staticstudios.data.impl.sqlite; - -import net.staticstudios.data.DataAccessor; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.parse.SQLBuilder; -import net.staticstudios.data.parse.UniqueDataMetadata; -import net.staticstudios.data.util.PrimaryKey; -import net.staticstudios.data.util.ReflectionUtils; -import org.intellij.lang.annotations.Language; -import org.sqlite.SQLiteConfig; - -import java.io.File; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class SQLiteDataAccessor implements DataAccessor { //todo: we need data transformers since sqlite only supports a few types natively - private final SQLBuilder sqlBuilder; - private final String jdbcUrl; - private final SQLiteConfig config = new SQLiteConfig(); - private final ThreadLocal threadConnection = new ThreadLocal<>(); - private final ThreadLocal> threadPreparedStatementCache = new ThreadLocal<>(); - - public SQLiteDataAccessor(SQLBuilder sqlBuilder) { - this.sqlBuilder = sqlBuilder; - String filePath = "static-data-cache.db"; - - File cacheFile = new File(filePath + ".db"); - if (cacheFile.exists()) { - cacheFile.delete(); - } - config.setSharedCache(true); - config.setJournalMode(SQLiteConfig.JournalMode.OFF); - config.setSynchronous(SQLiteConfig.SynchronousMode.OFF); - config.setTempStore(SQLiteConfig.TempStore.MEMORY); - this.jdbcUrl = "jdbc:sqlite:" + filePath; - - //todo: using thread utils delete the file on exit - } - - private Connection getConnection() throws SQLException { - Connection connection = threadConnection.get(); - if (connection == null) { - connection = DriverManager.getConnection(jdbcUrl); - connection.setAutoCommit(false); - threadConnection.set(connection); - } - return connection; - } - - @Override - public PreparedStatement prepareStatement(String sql) throws SQLException { - Connection connection = getConnection(); - Map preparedStatementCache = threadPreparedStatementCache.get(); - if (preparedStatementCache == null) { - preparedStatementCache = new HashMap<>(); - threadPreparedStatementCache.set(preparedStatementCache); - } - - PreparedStatement preparedStatement = preparedStatementCache.get(sql); - if (preparedStatement == null) { - preparedStatement = connection.prepareStatement(sql); - preparedStatementCache.put(sql, preparedStatement); - } - - return preparedStatement; - } - - @Override - public PersistentValue createPersistentValue(PrimaryKey primaryKey, Class dataType, String schema, String table, String dataColumn) { - return new SQLitePersistentValue<>(this, primaryKey, dataType, schema, table, dataColumn); - } - - //todo: set & get should be here instead of in the pv impl - - public void insert(Connection connection, UniqueData uniqueData) throws SQLException { - insertIntoCache(uniqueData); - } - - @Override - public void insertIntoCache(UniqueData uniqueData) throws SQLException { - UniqueDataMetadata metadata = uniqueData.getMetadata(); - - Map> tables = new HashMap<>(); - tables.computeIfAbsent(metadata.schema() + "." + metadata.table(), k -> new ArrayList<>()).add(new PrimaryKey.ColumnValuePair(metadata.idColumn(), uniqueData.getId())); - for (PersistentValue pv : ReflectionUtils.getFieldInstances(uniqueData, PersistentValue.class)) { - - tables.computeIfAbsent(pv.getSchema() + "." + pv.getTable(), k -> new ArrayList<>()).add(new PrimaryKey.ColumnValuePair(pv.getColumn(), pv.get())); - } - - for (Map.Entry> entry : tables.entrySet()) { - String fullTableName = entry.getKey(); - List columnValuePairs = entry.getValue(); - - StringBuilder sqlBuilder = new StringBuilder("INSERT INTO " + fullTableName + " ("); - for (PrimaryKey.ColumnValuePair pair : columnValuePairs) { - sqlBuilder.append(pair.column()).append(", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(") VALUES ("); - sqlBuilder.append("?, ".repeat(columnValuePairs.size())); - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(")"); - - //todo: on conflict : use insertion strategy for this - - @Language("SQL") String sql = sqlBuilder.toString(); - PreparedStatement preparedStatement = prepareStatement(sql); - for (int i = 0; i < columnValuePairs.size(); i++) { - PrimaryKey.ColumnValuePair pair = columnValuePairs.get(i); - preparedStatement.setObject(i + 1, pair.value()); - } - preparedStatement.executeUpdate(); - } - } - - -// StringBuilder sqlBuilder = new StringBuilder("INSERT INTO " + -} - - -//todo: maintain a buffer of what to send the the real db, and then collapse similar prepared statements into one so we can batch them. \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/impl/sqlite/SQLitePersistentValue.java b/src/main/java/net/staticstudios/data/impl/sqlite/SQLitePersistentValue.java deleted file mode 100644 index 600aa515..00000000 --- a/src/main/java/net/staticstudios/data/impl/sqlite/SQLitePersistentValue.java +++ /dev/null @@ -1,123 +0,0 @@ -package net.staticstudios.data.impl.sqlite; - -import com.google.common.base.Supplier; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.util.PrimaryKey; -import net.staticstudios.data.util.ValueUpdateHandler; -import org.intellij.lang.annotations.Language; -import org.jetbrains.annotations.Nullable; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -public class SQLitePersistentValue implements PersistentValue { - private final SQLiteDataAccessor dataAccessor; - private final PrimaryKey primaryKey; - private final Class dataType; - private final String schema; - private final String table; - private final String dataColumn; - - //todo: all methods should update the real db - public SQLitePersistentValue(SQLiteDataAccessor dataAccessor, PrimaryKey primaryKey, Class dataType, String schema, String table, String dataColumn) { - this.dataAccessor = dataAccessor; - this.primaryKey = primaryKey; - this.dataType = dataType; - this.schema = schema; - this.table = table; - this.dataColumn = dataColumn; - } - - @Override - public String getSchema() { - return schema; - } - - @Override - public String getTable() { - return table; - } - - @Override - public String getColumn() { - return dataColumn; - } - - @Override - public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { - return null; - } - - @Override - public PersistentValue withDefault(@Nullable T defaultValue) { - return null; - } - - @Override - public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier) { - return null; - } - - @Override - public T get() { - try { - StringBuilder sqlBuilder = new StringBuilder("SELECT " + dataColumn + " FROM " + schema + "_" + table + " WHERE "); - for (PrimaryKey.ColumnValuePair pair : primaryKey.getWhereClause()) { - sqlBuilder.append(pair.column()).append(" = ? AND "); - } - - sqlBuilder.setLength(sqlBuilder.length() - 5); - @Language("SQL") String sql = sqlBuilder.toString(); - PreparedStatement preparedStatement = dataAccessor.prepareStatement(sql); - - for (int i = 0; i < primaryKey.getWhereClause().size(); i++) { - PrimaryKey.ColumnValuePair pair = primaryKey.getWhereClause().get(i); - preparedStatement.setObject(i + 1, pair.value()); - } - - ResultSet rs = preparedStatement.executeQuery(); - Object rawValue = null; - if (rs.next()) { - rawValue = rs.getObject(dataColumn); - } - if (rawValue == null) { - return null; - } - //todo: deserialize - return (T) rawValue; - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @Override - public void set(T value) { - try { - Object serializedValue = value; //todo: serialize - StringBuilder sqlBuilder = new StringBuilder("INSERT INTO " + schema + "_" + table + " ("); - for (PrimaryKey.ColumnValuePair pair : primaryKey.getWhereClause()) { - sqlBuilder.append(pair.column()).append(", "); - } - sqlBuilder.append(dataColumn).append(") VALUES ("); - sqlBuilder.append("?, ".repeat(primaryKey.getWhereClause().size())); - sqlBuilder.append("?) ON CONFLICT("); - for (PrimaryKey.ColumnValuePair pair : primaryKey.getWhereClause()) { - sqlBuilder.append(pair.column()).append(", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(") DO UPDATE SET ").append(dataColumn).append(" = excluded.").append(dataColumn); - @Language("SQL") String sql = sqlBuilder.toString(); - PreparedStatement preparedStatement = dataAccessor.prepareStatement(sql); - int index = 1; - for (PrimaryKey.ColumnValuePair pair : primaryKey.getWhereClause()) { - preparedStatement.setObject(index++, pair.value()); - } - preparedStatement.setObject(index, serializedValue); - - preparedStatement.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } -} diff --git a/src/main/java/net/staticstudios/data/insert/Insert.java b/src/main/java/net/staticstudios/data/insert/Insert.java new file mode 100644 index 00000000..ff168f4d --- /dev/null +++ b/src/main/java/net/staticstudios/data/insert/Insert.java @@ -0,0 +1,9 @@ +package net.staticstudios.data.insert; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Insert { + InsertStrategy value() default InsertStrategy.OVERWRITE_EXISTING; +} diff --git a/src/main/java/net/staticstudios/data/insert/InsertContext.java b/src/main/java/net/staticstudios/data/insert/InsertContext.java new file mode 100644 index 00000000..4018c57c --- /dev/null +++ b/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -0,0 +1,93 @@ +package net.staticstudios.data.insert; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.SQLColumn; +import net.staticstudios.data.parse.SQLSchema; +import net.staticstudios.data.parse.SQLTable; +import net.staticstudios.data.parse.UniqueDataMetadata; +import net.staticstudios.data.util.ColumnMetadata; +import net.staticstudios.data.util.ColumnValuePair; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +public class InsertContext { //todo: insert strategy, on a per pv level. + private final AtomicBoolean inserted = new AtomicBoolean(false); + private final DataManager dataManager; + private final Map entries = new HashMap<>(); + + public InsertContext(DataManager dataManager) { + this.dataManager = dataManager; + } + + public InsertContext set(Class holderClass, String column, Object value) { + UniqueDataMetadata metadata = dataManager.getMetadata(holderClass); + Preconditions.checkNotNull(metadata, "Metadata not found for class: " + holderClass.getName()); + set(metadata.schema(), metadata.table(), column, value); + return this; + } + + public InsertContext set(String schema, String table, String column, @Nullable Object value) { + Preconditions.checkState(!inserted.get(), "Cannot modify InsertContext after it has been inserted"); + SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(schema); + Preconditions.checkNotNull(sqlSchema, "Schema not found: " + schema); + SQLTable sqlTable = sqlSchema.getTable(table); + Preconditions.checkNotNull(sqlTable, "Table not found: " + table); + SQLColumn sqlColumn = sqlTable.getColumn(column); + Preconditions.checkNotNull(sqlColumn, "Column not found: " + column + " in table: " + table + " schema: " + schema); + Preconditions.checkArgument(value != null || sqlColumn.isNullable(), "Column " + column + " in table " + table + " schema " + schema + " cannot be null"); + Preconditions.checkArgument(sqlColumn.getType().isInstance(value), "Value type mismatch for column " + column + " in table " + table + " schema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); + + ColumnMetadata columnMetadata = new ColumnMetadata(column, sqlColumn.getType(), sqlColumn.isNullable(), sqlColumn.isIndexed(), table, schema); + entries.put(columnMetadata, value); + return this; + }//todo: when inserting validate all id column values are present + + public Map getEntries() { + return entries; + } + + public void markInserted() { + inserted.set(true); + } + + public InsertContext insert(InsertMode insertMode) { + dataManager.insert(this, insertMode); + return this; + } + + /** + * Retrieves an instance of the specified UniqueData class based on the ID columns set in this InsertContext. + * + * @param holderClass The class of the UniqueData to retrieve. + * @param The type of UniqueData. + * @return An instance of the specified UniqueData class. + */ + public T get(Class holderClass) { + UniqueDataMetadata metadata = dataManager.getMetadata(holderClass); + Preconditions.checkNotNull(metadata, "Metadata not found for class: " + holderClass.getName()); + SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(metadata.schema()); + Preconditions.checkNotNull(sqlSchema, "Schema not found: " + metadata.schema()); + SQLTable sqlTable = sqlSchema.getTable(metadata.table()); + Preconditions.checkNotNull(sqlTable, "Table not found: " + metadata.table()); + boolean insertedAllIdColumns = true; + for (ColumnMetadata idColumn : metadata.idColumns()) { + if (!entries.containsKey(idColumn)) { + insertedAllIdColumns = false; + break; + } + } + + Preconditions.checkState(insertedAllIdColumns, "The requested class was not inserted. Class: " + holderClass.getName() + " is missing one or more ID column values. Required ID columns: " + metadata.idColumns()); + ColumnValuePair[] idColumnValues = new ColumnValuePair[metadata.idColumns().size()]; + for (int i = 0; i < metadata.idColumns().size(); i++) { + idColumnValues[i] = new ColumnValuePair(metadata.idColumns().get(i).name(), entries.get(metadata.idColumns().get(i))); + } + return dataManager.get(holderClass, idColumnValues); + } +} diff --git a/src/main/java/net/staticstudios/data/insert/InsertMode.java b/src/main/java/net/staticstudios/data/insert/InsertMode.java new file mode 100644 index 00000000..806ef393 --- /dev/null +++ b/src/main/java/net/staticstudios/data/insert/InsertMode.java @@ -0,0 +1,16 @@ +package net.staticstudios.data.insert; + +public enum InsertMode { + /** + * Insert into the cache and database synchronously. + * If either fails, neither will be updated. + * This is inherently blocking. + */ + SYNC, + /** + * Immediately inserts into the cache. + * If the cached insert fails, then the database will not be updated. + * If the database update fails however, the cache will NOT be reverted. + */ + ASYNC +} diff --git a/src/main/java/net/staticstudios/data/insert/InsertStrategy.java b/src/main/java/net/staticstudios/data/insert/InsertStrategy.java new file mode 100644 index 00000000..aaed7536 --- /dev/null +++ b/src/main/java/net/staticstudios/data/insert/InsertStrategy.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.insert; + +public enum InsertStrategy { + /** + * Overwrite existing data with new data. + */ + OVERWRITE_EXISTING, + /** + * Do not overwrite existing data, only insert if no data exists. + */ + PREFER_EXISTING, +} diff --git a/src/main/java/net/staticstudios/data/parse/Column.java b/src/main/java/net/staticstudios/data/parse/Column.java index e6ac5de8..f2b6976f 100644 --- a/src/main/java/net/staticstudios/data/parse/Column.java +++ b/src/main/java/net/staticstudios/data/parse/Column.java @@ -6,7 +6,7 @@ //todo: annotations would break compatability, but they make static analysis easier for meta data parsing and for building sql @Retention(RetentionPolicy.RUNTIME) public @interface Column { - String value(); + String name(); String schema() default ""; diff --git a/src/main/java/net/staticstudios/data/parse/Data.java b/src/main/java/net/staticstudios/data/parse/Data.java index 4582dacc..52f2c024 100644 --- a/src/main/java/net/staticstudios/data/parse/Data.java +++ b/src/main/java/net/staticstudios/data/parse/Data.java @@ -6,8 +6,6 @@ //todo: annotations would break compatability, but they make static analysis easier for meta data parsing and for building sql @Retention(RetentionPolicy.RUNTIME) //todo: we should support env variables in here as well. public @interface Data { - String idColumn(); - String schema(); String table(); diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index 55cf5f9d..d1da6d7d 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -1,58 +1,112 @@ package net.staticstudios.data.parse; import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; import net.staticstudios.data.Relation; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.util.ReflectionUtils; -import net.staticstudios.data.util.ValueUtils; +import net.staticstudios.data.util.*; +import org.jetbrains.annotations.Nullable; import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; +import java.util.*; public class SQLBuilder { - private final Map schemas; + public static final String INDENT = " "; + private final Map parsedSchemas; + private final DataManager dataManager; - public SQLBuilder() { - this.schemas = new HashMap<>(); + public SQLBuilder(DataManager dataManager) { + this.dataManager = dataManager; + this.parsedSchemas = new HashMap<>(); } - public void parse(Class clazz) { + public List parse(Class clazz) { Preconditions.checkNotNull(clazz, "Class cannot be null"); Set> visited = walk(clazz); + Map schemas = new HashMap<>(); for (Class visitedClass : visited) { - parseIndividual(visitedClass); + parseIndividual(visitedClass, schemas); } + + + for (SQLSchema newSchema : schemas.values()) { + if (!this.parsedSchemas.containsKey(newSchema.getName())) { + this.parsedSchemas.put(newSchema.getName(), newSchema); + continue; + } + SQLSchema existingSchema = this.parsedSchemas.get(newSchema.getName()); + for (SQLTable newTable : newSchema.getTables()) { + SQLTable existingTable = existingSchema.getTable(newTable.getName()); + if (existingTable == null) { + existingSchema.addTable(newTable); + continue; + } + for (SQLColumn newColumn : newTable.getColumns()) { + SQLColumn existingColumn = existingTable.getColumn(newColumn.getName()); + if (existingColumn != null) { + Preconditions.checkState(existingColumn.equals(newColumn), "Column " + newColumn.getName() + " in table " + newTable.getName() + " has conflicting definitions! Existing: " + existingColumn + ", New: " + newColumn); + continue; + } + existingTable.addColumn(newColumn); + } + } + } + + return getDefs(schemas.values()); + } + + public @Nullable SQLSchema getSchema(String name) { + return parsedSchemas.get(name); } - public String asSQL() { //todo: create table if not exist then use alter statements - StringBuilder sb = new StringBuilder(); - for (SQLSchema schema : schemas.values()) { - sb.append("CREATE SCHEMA IF NOT EXISTS ").append(schema.getName()).append(";\n"); + private List getDefs(Collection schemas) { + List statements = new ArrayList<>(); + for (SQLSchema schema : schemas) { + statements.add("CREATE SCHEMA IF NOT EXISTS \"" + schema.getName() + "\";"); + StringBuilder sb; for (SQLTable table : schema.getTables()) { - sb.append("CREATE TABLE IF NOT EXISTS ").append(schema.getName()).append(".").append(table.getName()).append(" (\n"); - for (SQLColumn column : table.getColumns()) { - sb.append(" ").append(column.getName()).append(" ?????").append(column.isNullable() ? "" : " NOT NULL"); - if (column.isIndexed()) { - sb.append(" INDEXED"); - } - sb.append(",\n"); +// if (metadata.table().equals(table.getName()) && metadata.schema().equals(schema.getName())) { + sb = new StringBuilder(); + sb.append("CREATE TABLE IF NOT EXISTS \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" (\n"); + for (ColumnMetadata idColumn : table.getIdColumns()) { + sb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append(SQLUtils.getSqlType(idColumn.type())).append(",\n"); + } + sb.append(INDENT).append("PRIMARY KEY ("); + for (ColumnMetadata idColumn : table.getIdColumns()) { + sb.append("\"").append(idColumn.name()).append("\", "); } sb.setLength(sb.length() - 2); - sb.append("\n);\n"); + sb.append(")\n"); + sb.append(");"); + statements.add(sb.toString()); +// } + if (!table.getColumns().isEmpty()) { + for (SQLColumn column : table.getColumns()) { + sb = new StringBuilder(); + sb.append("ALTER TABLE \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ").append("ADD COLUMN IF NOT EXISTS ").append("\"").append(column.getName()).append("\" ").append(SQLUtils.getSqlType(column.getType())); +// if (!column.isNullable()) { +// sb.append(" NOT NULL"); //todo: not valid in h2 +// } + if (column.isIndexed()) { +// sb.append(" INDEXED"); + //todo: this is not valid sql, need to create index separately + } + sb.append(";"); + statements.add(sb.toString()); + } + } } } - return sb.toString(); + return statements; } private Set> walk(Class clazz) { Preconditions.checkNotNull(clazz, "Class cannot be null"); Preconditions.checkArgument(UniqueData.class.isAssignableFrom(clazz), "Class " + clazz.getName() + " is not a UniqueData type"); - Set> visited = new java.util.HashSet<>(); + Set> visited = new HashSet<>(); walk(clazz, visited); return visited; } @@ -63,19 +117,16 @@ private void walk(Class clazz, Set genericType = ReflectionUtils.getGenericType(field); - Preconditions.checkNotNull(genericType, "Generic type for field " + field.getName() + " in class " + clazz.getName() + " is null"); - Preconditions.checkArgument(UniqueData.class.isAssignableFrom(genericType), "Field " + field.getName() + " in class " + clazz.getName() + " is not a UniqueData type"); - Class relatedClass = (Class) genericType; - - walk(relatedClass, visited); + Class related = Objects.requireNonNull(ReflectionUtils.getGenericType(field)).asSubclass(UniqueData.class); + walk(related, visited); } } } - private void parseIndividual(Class clazz) { + private void parseIndividual(Class clazz, Map schemas) { + UniqueDataMetadata metadata = dataManager.getMetadata(clazz); if (!clazz.isAnnotationPresent(Data.class)) { throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @Data"); } @@ -83,27 +134,77 @@ private void parseIndividual(Class clazz) { Data dataAnnotation = clazz.getAnnotation(Data.class); Preconditions.checkNotNull(dataAnnotation, "Data annotation is null for class " + clazz.getName()); - for (Field field : ReflectionUtils.getFields(clazz)) { - if (!field.isAnnotationPresent(Column.class)) { + IdColumn idColumn = field.getAnnotation(IdColumn.class); + Column columnAnnotation = field.getAnnotation(Column.class); +// net.staticstudios.data.relation.Relation.OneToOne oneToOne = field.getAnnotation(net.staticstudios.data.relation.Relation.OneToOne.class); + ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); + //todo: when parsing a OneToOne relation, in the link if there is a column in our table that we have no already created, then we need to create it. note that the type should be the same as the referenced column type. + //todo: add COLUMN REFERENCES bla bla bla for foreign keys + ColumnMetadata columnMetadata = null; + if (idColumn != null) { + Preconditions.checkArgument(columnAnnotation == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @Column", field.getName()); + columnMetadata = new ColumnMetadata(ValueUtils.parseValue(idColumn.name()), ReflectionUtils.getGenericType(field), false, false, ValueUtils.parseValue(dataAnnotation.table()), ValueUtils.parseValue(dataAnnotation.schema())); + } else if (columnAnnotation != null) { + columnMetadata = new ColumnMetadata(ValueUtils.parseValue(columnAnnotation.name()), ReflectionUtils.getGenericType(field), columnAnnotation.nullable(), columnAnnotation.index(), ValueUtils.parseValue(columnAnnotation.table()), ValueUtils.parseValue(columnAnnotation.schema())); + } else if (foreignColumn != null) { + columnMetadata = new ColumnMetadata(ValueUtils.parseValue(foreignColumn.name()), ReflectionUtils.getGenericType(field), foreignColumn.nullable(), foreignColumn.index(), ValueUtils.parseValue(foreignColumn.table()), ValueUtils.parseValue(foreignColumn.schema())); + } + if (columnMetadata == null) { continue; } - Column column = field.getAnnotation(Column.class); - Preconditions.checkNotNull(column, "Column annotation is null for field " + field.getName() + " in class " + clazz.getName()); - String schemaName = column.schema().isEmpty() ? ValueUtils.parseValue(dataAnnotation.schema()) : ValueUtils.parseValue(column.schema()); - String tableName = column.table().isEmpty() ? ValueUtils.parseValue(dataAnnotation.table()) : ValueUtils.parseValue(column.table()); - String columnName = ValueUtils.parseValue(column.value()); + String dataSchema = ValueUtils.parseValue(dataAnnotation.schema()); + String dataTable = ValueUtils.parseValue(dataAnnotation.table()); + + String schemaName = columnMetadata.schema().isEmpty() ? dataSchema : columnMetadata.schema(); + String tableName = columnMetadata.table().isEmpty() ? dataTable : columnMetadata.table(); + String columnName = columnMetadata.name(); + + if (foreignColumn != null) { + Preconditions.checkArgument(!(schemaName.equals(dataSchema) && tableName.equals(dataTable)), "ForeignColumn field %s in class %s cannot reference its own table", field.getName(), clazz.getName()); + } SQLSchema schema = schemas.computeIfAbsent(schemaName, SQLSchema::new); SQLTable table = schema.getTable(tableName); if (table == null) { - table = new SQLTable(schema, tableName); + List idColumns = metadata.idColumns(); + + if (foreignColumn != null) { + idColumns = new ArrayList<>(); + List links = StringUtils.parseCommaSeperatedList(foreignColumn.link()); + for (String link : links) { + String[] parts = link.split("="); + Preconditions.checkArgument(parts.length == 2, "Invalid link format in OneToOne annotation on field %s in class %s. Expected format: localColumn=foreignColumn", field.getName(), clazz.getName()); + String localColumn = ValueUtils.parseValue(parts[0].trim()); + String otherColumn = ValueUtils.parseValue(parts[1].trim()); + + ColumnMetadata found = null; + for (ColumnMetadata idCol : metadata.idColumns()) { + if (idCol.name().equals(localColumn)) { + found = idCol; + break; + } + } + Preconditions.checkNotNull(found, "Link column %s in OneToOne annotation on field %s in class %s is not an ID column", localColumn, field.getName(), clazz.getName()); + + idColumns.add(new ColumnMetadata(otherColumn, found.type(), false, false, tableName, schemaName)); + } + } + + table = new SQLTable(schema, tableName, idColumns); schema.addTable(table); } - //todo: grab the type of the PV to determine the SQL type - SQLColumn sqlColumn = new SQLColumn(table, columnName, column.nullable(), column.index()); + Class type = ReflectionUtils.getGenericType(field); //todo: handle custom types to sql types + SQLColumn sqlColumn = new SQLColumn(table, type, columnName, columnMetadata.nullable(), columnMetadata.indexed()); + + SQLColumn existingColumn = table.getColumn(columnName); + if (existingColumn != null) { + Preconditions.checkState(existingColumn.equals(sqlColumn), "Column " + columnName + " in table " + tableName + " has conflicting definitions! Existing: " + existingColumn + ", New: " + sqlColumn); + continue; + } + table.addColumn(sqlColumn); } } diff --git a/src/main/java/net/staticstudios/data/parse/SQLColumn.java b/src/main/java/net/staticstudios/data/parse/SQLColumn.java index 22ff60c4..8f7b7fd2 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLColumn.java +++ b/src/main/java/net/staticstudios/data/parse/SQLColumn.java @@ -1,13 +1,17 @@ package net.staticstudios.data.parse; +import java.util.Objects; + public class SQLColumn { private final SQLTable table; + private final Class type; private final String name; private final boolean nullable; private final boolean indexed; - public SQLColumn(SQLTable table, String name, boolean nullable, boolean indexed) { + public SQLColumn(SQLTable table, Class type, String name, boolean nullable, boolean indexed) { this.table = table; + this.type = type; this.name = name; this.nullable = nullable; this.indexed = indexed; @@ -17,6 +21,10 @@ public SQLTable getTable() { return table; } + public Class getType() { + return type; + } + public String getName() { return name; } @@ -29,5 +37,30 @@ public boolean isIndexed() { return indexed; } - //todo: equals, hashCode, toString methods if needed + + @Override + public int hashCode() { + return Objects.hash(table, type, name, nullable, indexed); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + SQLColumn other = (SQLColumn) obj; + return nullable == other.nullable && + indexed == other.indexed && + Objects.equals(type, other.type) && + Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "SQLColumn{" + + ", type=" + type + + ", name='" + name + '\'' + + ", nullable=" + nullable + + ", indexed=" + indexed + + '}'; + } } diff --git a/src/main/java/net/staticstudios/data/parse/SQLTable.java b/src/main/java/net/staticstudios/data/parse/SQLTable.java index 6de02b36..9f006670 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLTable.java +++ b/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -1,6 +1,7 @@ package net.staticstudios.data.parse; import com.google.common.base.Preconditions; +import net.staticstudios.data.util.ColumnMetadata; import org.jetbrains.annotations.Nullable; import java.util.*; @@ -8,11 +9,13 @@ public class SQLTable { private final SQLSchema schema; private final String name; + private final List idColumns; private final Map columns; - public SQLTable(SQLSchema schema, String name) { + public SQLTable(SQLSchema schema, String name, List idColumns) { this.schema = schema; this.name = name; + this.idColumns = idColumns; this.columns = new HashMap<>(); } @@ -32,6 +35,10 @@ public Set getColumns() { return columns.get(columnName); } + public List getIdColumns() { + return idColumns; + } + public void addColumn(SQLColumn column) { if (column.getTable() != this) { throw new IllegalArgumentException("Column does not belong to this table"); diff --git a/src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java b/src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java index 41d94f6a..35d4b5e2 100644 --- a/src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java +++ b/src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java @@ -1,5 +1,10 @@ package net.staticstudios.data.parse; -public record UniqueDataMetadata(String schema, String table, String idColumn) { +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.util.ColumnMetadata; +import java.util.List; + +public record UniqueDataMetadata(Class clazz, String schema, String table, + List idColumns) { } diff --git a/src/main/java/net/staticstudios/data/util/ColumnMetadata.java b/src/main/java/net/staticstudios/data/util/ColumnMetadata.java new file mode 100644 index 00000000..8b95dd50 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/ColumnMetadata.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.util; + +public record ColumnMetadata(String name, Class type, boolean nullable, boolean indexed, String table, + String schema) { +} diff --git a/src/main/java/net/staticstudios/data/util/ColumnValuePair.java b/src/main/java/net/staticstudios/data/util/ColumnValuePair.java new file mode 100644 index 00000000..e7abda9e --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/ColumnValuePair.java @@ -0,0 +1,47 @@ +package net.staticstudios.data.util; + +import java.util.Objects; + +public final class ColumnValuePair { + private final String column; + private final Object value; + + public ColumnValuePair(String column, Object value) { + this.column = column; + this.value = value; + } + + public static ColumnValuePair of(String column, Object value) { + return new ColumnValuePair(column, value); + } + + public String column() { + return column; + } + + public Object value() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (ColumnValuePair) obj; + return Objects.equals(this.column, that.column) && + Objects.equals(this.value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(column, value); + } + + @Override + public String toString() { + return "ColumnValuePair[" + + "column=" + column + ", " + + "value=" + value + ']'; + } + +} diff --git a/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java b/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java new file mode 100644 index 00000000..a1602107 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java @@ -0,0 +1,65 @@ +package net.staticstudios.data.util; + +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 { + 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]); + } + + public ColumnValuePair[] getPairs() { + return pairs; + } + + public Stream stream() { + return Arrays.stream(pairs); + } + + @Override + public java.util.@NotNull Iterator iterator() { + return new java.util.Iterator<>() { + private int index = 0; + + @Override + public boolean hasNext() { + return index < pairs.length; + } + + @Override + public ColumnValuePair next() { + return pairs[index++]; + } + }; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ColumnValuePairs other)) return false; + if (this.pairs.length != other.pairs.length) return false; + for (int i = 0; i < this.pairs.length; i++) { + if (!this.pairs[i].equals(other.pairs[i])) return false; + } + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(pairs); + } + + @Override + public String toString() { + return Arrays.toString(pairs); + } +} diff --git a/src/main/java/net/staticstudios/data/util/ForeignColumn.java b/src/main/java/net/staticstudios/data/util/ForeignColumn.java new file mode 100644 index 00000000..d079001e --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/ForeignColumn.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.delete.DeleteStrategy; +import net.staticstudios.data.insert.InsertStrategy; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ForeignColumn { + String name(); + + String table() default ""; + + String schema() default ""; + + boolean nullable() default false; + + boolean index() default false; + + String link(); + + InsertStrategy insertStrategy() default InsertStrategy.OVERWRITE_EXISTING; + + DeleteStrategy deleteStrategy() default DeleteStrategy.NO_ACTION; +} diff --git a/src/main/java/net/staticstudios/data/util/IdColumn.java b/src/main/java/net/staticstudios/data/util/IdColumn.java new file mode 100644 index 00000000..737b3d15 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/IdColumn.java @@ -0,0 +1,9 @@ +package net.staticstudios.data.util; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface IdColumn { + String name(); +} diff --git a/src/main/java/net/staticstudios/data/util/OneToOne.java b/src/main/java/net/staticstudios/data/util/OneToOne.java new file mode 100644 index 00000000..8c0ab67e --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/OneToOne.java @@ -0,0 +1,19 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.delete.DeleteStrategy; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface OneToOne { + /** + * How should this relation be linked? + * Format "localColumn=foreignColumn" + * + * @return The link format + */ + String link(); + + DeleteStrategy deleteStrategy() default DeleteStrategy.NO_ACTION; +} diff --git a/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java b/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java new file mode 100644 index 00000000..e47bf656 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java @@ -0,0 +1,58 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +import java.util.Objects; + +public class PersistentValueMetadata { + private final Class holderClass; + private final String schema; + private final String table; + private final String column; + private final Class dataType; + + public PersistentValueMetadata(Class holderClass, String schema, String table, String column, Class dataType) { + this.holderClass = holderClass; + this.schema = schema; + this.table = table; + this.column = column; + this.dataType = dataType; + } + + public Class getHolderClass() { + return holderClass; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getColumn() { + return column; + } + + public Class getDataType() { + return dataType; + } + + @Override + public int hashCode() { + return Objects.hash(holderClass, schema, table, column, dataType); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + PersistentValueMetadata that = (PersistentValueMetadata) obj; + return holderClass.equals(that.holderClass) && + schema.equals(that.schema) && + table.equals(that.table) && + column.equals(that.column) && + dataType.equals(that.dataType); + } +} diff --git a/src/main/java/net/staticstudios/data/util/PrimaryKey.java b/src/main/java/net/staticstudios/data/util/PrimaryKey.java deleted file mode 100644 index b875f7dc..00000000 --- a/src/main/java/net/staticstudios/data/util/PrimaryKey.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.staticstudios.data.util; - -import java.util.List; - -public interface PrimaryKey { - - List getWhereClause(); - - record ColumnValuePair(String column, Object value) { - } -} diff --git a/src/main/java/net/staticstudios/data/util/ReflectionUtils.java b/src/main/java/net/staticstudios/data/util/ReflectionUtils.java index ce163568..a240c978 100644 --- a/src/main/java/net/staticstudios/data/util/ReflectionUtils.java +++ b/src/main/java/net/staticstudios/data/util/ReflectionUtils.java @@ -44,9 +44,7 @@ public static List> getFieldInstancePairs(Object instan field.setAccessible(true); try { T value = fieldType.cast(field.get(instance)); - if (value != null) { - instances.add(new FieldInstancePair<>(field, value)); - } + instances.add(new FieldInstancePair<>(field, value)); } catch (IllegalAccessException e) { throw new RuntimeException(e); } @@ -68,7 +66,7 @@ public static List getFieldInstances(Object instance, Class fieldType) if (field.getGenericType() instanceof Class) { return (Class) field.getGenericType(); } else if (field.getGenericType() instanceof java.lang.reflect.ParameterizedType parameterizedType) { - return (Class) parameterizedType.getRawType(); + return (Class) parameterizedType.getActualTypeArguments()[0]; } return null; } diff --git a/src/main/java/net/staticstudios/data/util/SQLUtils.java b/src/main/java/net/staticstudios/data/util/SQLUtils.java new file mode 100644 index 00000000..039f82f3 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/SQLUtils.java @@ -0,0 +1,28 @@ +package net.staticstudios.data.util; + +public class SQLUtils { + public static String getSqlType(Class clazz) { + if (clazz.equals(String.class)) { + return "TEXT"; + } + if (clazz.equals(Integer.class) || clazz.equals(int.class)) { + return "INTEGER"; + } + if (clazz.equals(Long.class) || clazz.equals(long.class)) { + return "BIGINT"; + } + if (clazz.equals(Boolean.class) || clazz.equals(boolean.class)) { + return "BOOLEAN"; + } + if (clazz.equals(Double.class) || clazz.equals(double.class)) { + return "DOUBLE PRECISION"; + } + if (clazz.equals(Float.class) || clazz.equals(float.class)) { + return "REAL"; + } + if (clazz.equals(java.util.UUID.class)) { + return "UUID"; + } + throw new IllegalArgumentException("Unsupported class type: " + clazz.getName()); + } +} diff --git a/src/main/java/net/staticstudios/data/util/SQlStatement.java b/src/main/java/net/staticstudios/data/util/SQlStatement.java new file mode 100644 index 00000000..1de8162e --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/SQlStatement.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.util; + +import java.util.List; + +public class SQlStatement { + private final String sql; + private final List values; + + public SQlStatement(String sql, List values) { + this.sql = sql; + this.values = values; + } + + public String getSql() { + return sql; + } + + public List getValues() { + return values; + } +} diff --git a/src/main/java/net/staticstudios/data/util/StringUtils.java b/src/main/java/net/staticstudios/data/util/StringUtils.java new file mode 100644 index 00000000..d10dbaf3 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/StringUtils.java @@ -0,0 +1,9 @@ +package net.staticstudios.data.util; + +import java.util.List; + +public class StringUtils { + public static List parseCommaSeperatedList(String input) { + return List.of(input.split(",")); + } +} diff --git a/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java b/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java index 043bbbbd..80ff9cb2 100644 --- a/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java +++ b/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java @@ -1,11 +1,13 @@ package net.staticstudios.data.util; -public interface ValueUpdateHandler { +import net.staticstudios.data.UniqueData; - void handle(ValueUpdate update); +public interface ValueUpdateHandler { + + void handle(U holder, ValueUpdate update); @SuppressWarnings("unchecked") - default void unsafeHandle(Object oldValue, Object newValue) { - handle(new ValueUpdate<>((T) oldValue, (T) newValue)); + default void unsafeHandle(UniqueData holder, Object oldValue, Object newValue) { + handle((U) holder, new ValueUpdate<>((T) oldValue, (T) newValue)); } } diff --git a/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java b/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java new file mode 100644 index 00000000..71d73db9 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.util; + +public class ValueUpdateHandlerNonStaticException extends RuntimeException { + public ValueUpdateHandlerNonStaticException(String message) { + super(message); + } +} diff --git a/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java b/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java new file mode 100644 index 00000000..0913c248 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java @@ -0,0 +1,48 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +import java.util.Objects; + +public class ValueUpdateHandlerWrapper { + private final ValueUpdateHandler handler; + private final Class dataType; + private final Class holderClass; + + public ValueUpdateHandlerWrapper(ValueUpdateHandler handler, Class dataType, Class holderClass) { + if (handler.getClass().getDeclaredFields().length > 0) { + throw new ValueUpdateHandlerNonStaticException("Value update handler must not capture any variables! It must act as a static function."); + // 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; + } + + public Class getDataType() { + return dataType; + } + + public Class getHolderClass() { + return holderClass; + } + + public void unsafeHandle(UniqueData holder, Object oldValue, Object newValue) { + handler.unsafeHandle(holder, oldValue, newValue); + } + + @Override + public int hashCode() { + return Objects.hash(handler, dataType, holderClass); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + ValueUpdateHandlerWrapper that = (ValueUpdateHandlerWrapper) obj; + return dataType.equals(that.dataType) && holderClass.equals(that.holderClass) && handler.equals(that.handler); + } +} diff --git a/src/main/java/net/staticstudios/data/util/ValueUtils.java b/src/main/java/net/staticstudios/data/util/ValueUtils.java index f611e81c..832c1905 100644 --- a/src/main/java/net/staticstudios/data/util/ValueUtils.java +++ b/src/main/java/net/staticstudios/data/util/ValueUtils.java @@ -3,6 +3,8 @@ import com.google.common.base.Preconditions; import org.jetbrains.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -23,4 +25,13 @@ public static String parseValue(String encoded) { return encoded; } } + + public static List parseCommaSeperatedList(String encoded) { + List strings = new ArrayList<>(); + for (String s : StringUtils.parseCommaSeperatedList(encoded)) { + strings.add(parseValue(s)); + } + + return strings; + } } diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 3654def4..8893abc6 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -2,16 +2,118 @@ import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.mock.MockUser; +import net.staticstudios.data.mock.MockUserSettings; +import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; public class PersistentValueTest extends DataTest { + @Test + public void testReadData() throws SQLException { + List userIds = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + userIds.add(UUID.randomUUID()); + } + + try (PreparedStatement ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS public.users (id UUID PRIMARY KEY, name TEXT, age INT)")) { + ps.execute(); + } + + Connection connection = getConnection(); + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO public.users (id, name) VALUES (?, ?)")) { + for (UUID id : userIds) { + ps.setObject(1, id); + ps.setString(2, "user " + id); + ps.addBatch(); + } + ps.executeBatch(); + } + + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + for (UUID id : userIds) { + MockUser user = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); + assertEquals("user " + id, user.name.get()); + assertNull(user.age.get()); + } + + waitForDataPropagation(); + } + @Test public void test() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); - MockUser mockUser = MockUser.create(dataManager, "josh"); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUser.create(dataManager, id, "test user"); + assertEquals("test user", mockUser.name.get()); + mockUser.name.set("updated name"); + assertEquals("updated name", mockUser.name.get()); + + assertNull(mockUser.age.get()); + mockUser.age.set(25); + assertEquals(25, mockUser.age.get()); + + mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); + mockUser = null; // remove strong reference + System.gc(); + mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss + + assertNull(mockUser.favoriteColor.get()); + mockUser.favoriteColor.set("blue"); + assertEquals("blue", mockUser.favoriteColor.get()); + +// long start; +// int count = 10_000; +// for (int j = 0; j < 5; j++) { +// start = System.currentTimeMillis(); +// for (int i = 0; i < count; i++) { +// mockUser.name.set("name " + i); +// } +// +// System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " updates"); +// } +// for (int j = 0; j < 5; j++) { +// start = System.currentTimeMillis(); +// for (int i = 0; i < count; i++) { +// mockUser.name.get(); +// } +// System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " gets"); +// } + + waitForDataPropagation(); + + MockUserSettings settings = MockUserSettings.create(dataManager, UUID.randomUUID()); + assertNull(mockUser.settings.get()); + mockUser.settings.set(settings); + assertEquals(settings, mockUser.settings.get()); + } + + @Test + public void testUpdateHandlerRegistration() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + assertEquals(0, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); + MockUser mockUser = MockUser.create(dataManager, id, "test user"); + //first instance was created, handler should be registered + assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); + mockUser = null; // remove strong reference + System.gc(); + mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss + //the handler for this pv should not have been registered again + assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); + + mockUser.name.set("new name"); } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index 212061c4..2097e9c0 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -1,13 +1,15 @@ package net.staticstudios.data; -public class SQLParseTest { +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.mock.MockUser; +import org.junit.jupiter.api.Test; -// @Test -// public void testParse() { -// DataManager dm = new DataManager(); -// dm.extractMetadata(MockUser.class); -// String sql = dm.getSQLBuilder().asSQL(); -// -// System.out.println(sql); -// } +public class SQLParseTest extends DataTest { + + @Test + public void testParse() { + DataManager dm = getMockEnvironments().getFirst().dataManager(); + dm.extractMetadata(MockUser.class); + dm.getSQLBuilder().parse(MockUser.class).forEach(System.out::println); + } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/MockUser.java index bceb1698..82ce4ef9 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/MockUser.java @@ -2,33 +2,59 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.PersistentValue; +import net.staticstudios.data.Reference; import net.staticstudios.data.UniqueData; +import net.staticstudios.data.insert.InsertMode; import net.staticstudios.data.parse.Column; import net.staticstudios.data.parse.Data; +import net.staticstudios.data.util.ForeignColumn; +import net.staticstudios.data.util.IdColumn; +import net.staticstudios.data.util.OneToOne; import java.util.UUID; -@Data(schema = "public", table = "users", idColumn = "id") +@Data(schema = "public", table = "users") public class MockUser extends UniqueData { - private final UUID id; + //todo: @OneToMany, @ManyToMany, @ManyToOne - public @Column(value = "name") PersistentValue name = PersistentValue.of(this, String.class).withDefault("Unknown"); - public @Column(value = "age", nullable = true) PersistentValue age; - public MockUser(DataManager dataManager, UUID id) { - super(dataManager); - this.id = id; - } + @IdColumn(name = "id") + public PersistentValue id = PersistentValue.of(this, UUID.class); + @Column(name = "settings_id") + public PersistentValue settingsId = PersistentValue.of(this, UUID.class); + @Column(name = "age", nullable = true) + public PersistentValue age; + @ForeignColumn(name = "fav_color", table = "user_preferences", nullable = true, link = "id=user_id") + public PersistentValue favoriteColor; + @OneToOne(link = "settings_id=user_id") + public Reference settings; + @Column(name = "name", index = true) + public PersistentValue name = PersistentValue.of(this, String.class) + .onUpdate(MockUser.class, (user, update) -> { +// System.out.println("User " + user.id.get() + " changed name from " + update.oldValue() + " to " + update.newValue()); + }) + .withDefault("Unknown"); - public static MockUser create(DataManager dataManager, String name) { - MockUser user = new MockUser(dataManager, UUID.randomUUID()); - dataManager.init(user); //todo: can do this in insert - dataManager.insert(user, false); //todo: set the name and age - return user; - } + public static MockUser create(DataManager dataManager, UUID id, String name) { + return dataManager.createInsertContext() + .set(MockUser.class, "id", id) //todo: i dislike that we lose type safety + .set(MockUser.class, "name", name) + //todo: add support for unique constraints and test them. + //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. + .insert(InsertMode.SYNC) + .get(MockUser.class); + + //TODO: generate the following patterns at compile time from the @Data annotation. - @Override - public UUID getId() { - return id; + /* MockUser.builder(datamanager) + * .id(id) + * .name(name) + * .insert(InsertMode.SYNC); //returns the inserted object + */ + /* MockUser.builder(datamanager) + * .id(id) + * .name(name) + * .insert(insertContext); //add the object to an existing insert context, and return void. + */ } } diff --git a/src/test/java/net/staticstudios/data/mock/MockUserSettings.java b/src/test/java/net/staticstudios/data/mock/MockUserSettings.java new file mode 100644 index 00000000..f7882f86 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/MockUserSettings.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.mock; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.PersistentValue; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.insert.InsertMode; +import net.staticstudios.data.parse.Column; +import net.staticstudios.data.parse.Data; +import net.staticstudios.data.util.IdColumn; + +import java.util.UUID; + +@Data(schema = "public", table = "user_settings") +public class MockUserSettings extends UniqueData { + @IdColumn(name = "user_id") + public PersistentValue id; + @Column(name = "font_size") + public PersistentValue fontSide; + + public static MockUserSettings create(DataManager dataManager, UUID id) { + return dataManager.createInsertContext() + .set(MockUserSettings.class, "user_id", id) + //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. + .insert(InsertMode.SYNC) + .get(MockUserSettings.class); + } +} diff --git a/static-data-cache.db b/static-data-cache.db deleted file mode 100644 index e69de29b..00000000 From 92d85010dbdc2615eb243c93e3f19333be111d3f Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Fri, 12 Sep 2025 11:11:34 -0400 Subject: [PATCH 03/75] start on annotation processing --- annotations/build.gradle | 19 ++ .../java/net/staticstudios/data}/Column.java | 2 +- .../java/net/staticstudios/data}/Data.java | 2 +- .../staticstudios/data}/DeleteStrategy.java | 4 +- .../staticstudios/data}/ForeignColumn.java | 5 +- .../net/staticstudios/data}/IdColumn.java | 2 +- .../net/staticstudios/data}/InsertMode.java | 2 +- .../staticstudios/data}/InsertStrategy.java | 2 +- .../net/staticstudios/data}/OneToOne.java | 3 +- build.gradle | 8 + .../net/staticstudios/data/CachedValue.java | 0 .../net/staticstudios/data/DataManager.java | 1 - .../data/PersistentCollection.java | 0 .../staticstudios/data/PersistentValue.java | 0 .../net/staticstudios/data/Reference.java | 0 .../net/staticstudios/data/UniqueData.java | 0 .../staticstudios/data/ValueSerializer.java | 0 .../net/staticstudios/data/data/Data.java | 0 .../staticstudios/data/data/DataHolder.java | 0 .../staticstudios/data/data/Deletable.java | 0 .../staticstudios/data/data/InitialValue.java | 0 .../data/data/collection/CollectionEntry.java | 0 .../collection/CollectionEntryIdentifier.java | 0 .../PersistentCollectionChangeHandler.java | 0 .../PersistentManyToManyCollection.java | 0 .../PersistentUniqueDataCollection.java | 0 .../collection/PersistentValueCollection.java | 0 .../SimplePersistentCollection.java | 0 .../data/data/value/InitialCachedValue.java | 0 .../data/value/InitialPersistentValue.java | 0 .../staticstudios/data/data/value/Value.java | 0 .../data/impl/CachedValueManager.java | 0 .../impl/PersistentCollectionManager.java | 1 - .../data/impl/PersistentValueManager.java | 0 .../data/impl/pg/PostgresData.java | 0 .../data/impl/pg/PostgresListener.java | 0 .../data/impl/pg/PostgresNotification.java | 0 .../data/impl/pg/PostgresOperation.java | 0 .../net/staticstudios/data/key/CellKey.java | 0 .../staticstudios/data/key/CollectionKey.java | 0 .../net/staticstudios/data/key/DataKey.java | 0 .../staticstudios/data/key/DatabaseKey.java | 0 .../net/staticstudios/data/key/RedisKey.java | 0 .../data/key/UniqueIdentifier.java | 0 .../data/primative/Primitive.java | 0 .../data/primative/PrimitiveBuilder.java | 0 .../data/primative/Primitives.java | 0 .../staticstudios/data/util/BatchInsert.java | 0 .../staticstudios/data/util/CacheEntry.java | 0 .../data/util/ConnectionConsumer.java | 0 .../data/util/ConnectionJedisConsumer.java | 0 .../data/util/DataDoesNotExistException.java | 0 .../data/util/DataSourceConfig.java | 0 .../data/util/DeleteContext.java | 0 .../data/util/DeletionStrategy.java | 0 .../data/util/InsertContext.java | 0 .../data/util/InsertionStrategy.java | 0 .../data/util/JunctionTable.java | 0 .../data/util/PostgresUtils.java | 0 .../data/util/ReflectionUtils.java | 0 .../staticstudios/data/util/SQLLogger.java | 0 .../staticstudios/data/util/TaskQueue.java | 0 .../staticstudios/data/util/ValueUpdate.java | 0 .../data/util/ValueUpdateHandler.java | 0 .../staticstudios/data/CachedValueTest.java | 0 .../net/staticstudios/data/DeletionTest.java | 0 .../net/staticstudios/data/InsertionTest.java | 0 .../data/PersistentCollectionTest.java | 0 .../data/PersistentValueTest.java | 0 .../data/PostgresListenerTest.java | 0 .../staticstudios/data/PrimitivesTest.java | 0 .../net/staticstudios/data/ReferenceTest.java | 0 .../net/staticstudios/data/misc/DataTest.java | 0 .../data/misc/MockEnvironment.java | 0 .../data/misc/MockThreadProvider.java | 0 .../staticstudios/data/misc/TestUtils.java | 0 .../data/mock/cachedvalue/RedditUser.java | 0 .../data/mock/deletions/MinecraftServer.java | 0 .../data/mock/deletions/MinecraftSkin.java | 0 .../deletions/MinecraftUserStatistics.java | 0 ...ecraftUserWithCascadeDeletionStrategy.java | 0 ...craftUserWithNoActionDeletionStrategy.java | 0 ...necraftUserWithUnlinkDeletionStrategy.java | 0 .../mock/insertions/TwitchChatMessage.java | 0 .../data/mock/insertions/TwitchUser.java | 0 .../persistentcollection/FacebookPost.java | 0 .../persistentcollection/FacebookUser.java | 0 .../mock/persistentvalue/DiscordUser.java | 0 .../persistentvalue/DiscordUserSettings.java | 0 .../primative/BooleanPrimitiveTestObject.java | 0 .../ByteArrayPrimitiveTestObject.java | 0 .../primative/BytePrimitiveTestObject.java | 0 .../CharacterPrimitiveTestObject.java | 0 .../primative/DoublePrimitiveTestObject.java | 0 .../primative/FloatPrimitiveTestObject.java | 0 .../primative/IntegerPrimitiveTestObject.java | 0 .../primative/LongPrimitiveTestObject.java | 0 .../primative/ShortPrimitiveTestObject.java | 0 .../primative/StringPrimitiveTestObject.java | 0 .../TimestampPrimitiveTestObject.java | 0 .../primative/UUIDPrimitiveTestObject.java | 0 .../data/mock/reference/SnapchatUser.java | 0 .../mock/reference/SnapchatUserSettings.java | 0 .../ogtest/resources/log4j.properties | 0 .../data/PersistentValueTest.java | 119 +++++++++++ .../net/staticstudios/data/SQLParseTest.java | 15 ++ .../staticstudios/data/ValueParseTest.java | 32 +++ .../net/staticstudios/data/misc/DataTest.java | 112 ++++++++++ .../data/misc/MockEnvironment.java | 10 + .../data/misc/MockThreadProvider.java | 125 +++++++++++ .../staticstudios/data/misc/TestUtils.java | 26 +++ .../net/staticstudios/data/mock/MockUser.java | 61 ++++++ .../data/mock/MockUserSettings.java | 27 +++ oldsrc/test/resources/log4j.properties | 6 + processor/build.gradle | 30 +++ .../data/processor/DataProcessor.java | 201 ++++++++++++++++++ .../gradle/incremental.annotation.processors | 2 + .../javax.annotation.processing.Processor | 1 + settings.gradle | 3 + .../net/staticstudios/data/DataAccessor.java | 1 - .../net/staticstudios/data/DataManager.java | 26 ++- .../staticstudios/data/PersistentValue.java | 5 +- .../net/staticstudios/data/delete/Delete.java | 2 + .../data/impl/data/PersistentValueImpl.java | 4 +- .../data/impl/data/ReferenceImpl.java | 1 + .../data/impl/h2/H2DataAccessor.java | 2 +- .../net/staticstudios/data/insert/Insert.java | 2 + .../data/insert/InsertContext.java | 6 +- .../staticstudios/data/parse/SQLBuilder.java | 6 +- .../data/util/AbstractBuilder.java | 14 ++ .../staticstudios/data/{ => util}/Value.java | 4 +- .../data/util/ValueUpdateHandlerWrapper.java | 2 +- .../data/MultiEnvironmentTest.java | 23 ++ .../data/PersistentValueTest.java | 54 +++-- .../net/staticstudios/data/misc/DataTest.java | 4 +- .../net/staticstudios/data/mock/MockUser.java | 43 +--- .../data/mock/MockUserSettings.java | 8 +- 137 files changed, 938 insertions(+), 90 deletions(-) create mode 100644 annotations/build.gradle rename {src/main/java/net/staticstudios/data/parse => annotations/src/main/java/net/staticstudios/data}/Column.java (92%) rename {src/main/java/net/staticstudios/data/parse => annotations/src/main/java/net/staticstudios/data}/Data.java (90%) rename {src/main/java/net/staticstudios/data/delete => annotations/src/main/java/net/staticstudios/data}/DeleteStrategy.java (70%) rename {src/main/java/net/staticstudios/data/util => annotations/src/main/java/net/staticstudios/data}/ForeignColumn.java (77%) rename {src/main/java/net/staticstudios/data/util => annotations/src/main/java/net/staticstudios/data}/IdColumn.java (82%) rename {src/main/java/net/staticstudios/data/insert => annotations/src/main/java/net/staticstudios/data}/InsertMode.java (91%) rename {src/main/java/net/staticstudios/data/insert => annotations/src/main/java/net/staticstudios/data}/InsertStrategy.java (85%) rename {src/main/java/net/staticstudios/data/util => annotations/src/main/java/net/staticstudios/data}/OneToOne.java (80%) rename {src => oldsrc}/og/java/net/staticstudios/data/CachedValue.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/DataManager.java (99%) rename {src => oldsrc}/og/java/net/staticstudios/data/PersistentCollection.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/PersistentValue.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/Reference.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/UniqueData.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/ValueSerializer.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/Data.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/DataHolder.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/Deletable.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/InitialValue.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/collection/CollectionEntry.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/value/InitialCachedValue.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/data/value/Value.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/impl/CachedValueManager.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java (99%) rename {src => oldsrc}/og/java/net/staticstudios/data/impl/PersistentValueManager.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/impl/pg/PostgresData.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/impl/pg/PostgresListener.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/impl/pg/PostgresNotification.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/impl/pg/PostgresOperation.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/key/CellKey.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/key/CollectionKey.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/key/DataKey.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/key/DatabaseKey.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/key/RedisKey.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/key/UniqueIdentifier.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/primative/Primitive.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/primative/Primitives.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/BatchInsert.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/CacheEntry.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/ConnectionConsumer.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/DataDoesNotExistException.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/DataSourceConfig.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/DeleteContext.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/DeletionStrategy.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/InsertContext.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/InsertionStrategy.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/JunctionTable.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/PostgresUtils.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/ReflectionUtils.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/SQLLogger.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/TaskQueue.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/ValueUpdate.java (100%) rename {src => oldsrc}/og/java/net/staticstudios/data/util/ValueUpdateHandler.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/CachedValueTest.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/DeletionTest.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/InsertionTest.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/PersistentValueTest.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/PostgresListenerTest.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/PrimitivesTest.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/ReferenceTest.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/misc/DataTest.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/misc/TestUtils.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java (100%) rename {src => oldsrc}/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java (100%) rename {src => oldsrc}/ogtest/resources/log4j.properties (100%) create mode 100644 oldsrc/test/java/net/staticstudios/data/PersistentValueTest.java create mode 100644 oldsrc/test/java/net/staticstudios/data/SQLParseTest.java create mode 100644 oldsrc/test/java/net/staticstudios/data/ValueParseTest.java create mode 100644 oldsrc/test/java/net/staticstudios/data/misc/DataTest.java create mode 100644 oldsrc/test/java/net/staticstudios/data/misc/MockEnvironment.java create mode 100644 oldsrc/test/java/net/staticstudios/data/misc/MockThreadProvider.java create mode 100644 oldsrc/test/java/net/staticstudios/data/misc/TestUtils.java create mode 100644 oldsrc/test/java/net/staticstudios/data/mock/MockUser.java create mode 100644 oldsrc/test/java/net/staticstudios/data/mock/MockUserSettings.java create mode 100644 oldsrc/test/resources/log4j.properties create mode 100644 processor/build.gradle create mode 100644 processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java create mode 100644 processor/src/main/resources/META-INF/gradle/incremental.annotation.processors create mode 100644 processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 src/main/java/net/staticstudios/data/util/AbstractBuilder.java rename src/main/java/net/staticstudios/data/{ => util}/Value.java (63%) create mode 100644 src/test/java/net/staticstudios/data/MultiEnvironmentTest.java diff --git a/annotations/build.gradle b/annotations/build.gradle new file mode 100644 index 00000000..aa09b1d3 --- /dev/null +++ b/annotations/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' +} + +group = 'net.staticstudios' +version = '3.0.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/parse/Column.java b/annotations/src/main/java/net/staticstudios/data/Column.java similarity index 92% rename from src/main/java/net/staticstudios/data/parse/Column.java rename to annotations/src/main/java/net/staticstudios/data/Column.java index f2b6976f..d38d7ee9 100644 --- a/src/main/java/net/staticstudios/data/parse/Column.java +++ b/annotations/src/main/java/net/staticstudios/data/Column.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.parse; +package net.staticstudios.data; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/main/java/net/staticstudios/data/parse/Data.java b/annotations/src/main/java/net/staticstudios/data/Data.java similarity index 90% rename from src/main/java/net/staticstudios/data/parse/Data.java rename to annotations/src/main/java/net/staticstudios/data/Data.java index 52f2c024..d7891504 100644 --- a/src/main/java/net/staticstudios/data/parse/Data.java +++ b/annotations/src/main/java/net/staticstudios/data/Data.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.parse; +package net.staticstudios.data; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/main/java/net/staticstudios/data/delete/DeleteStrategy.java b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java similarity index 70% rename from src/main/java/net/staticstudios/data/delete/DeleteStrategy.java rename to annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java index ad0018a0..50ece10e 100644 --- a/src/main/java/net/staticstudios/data/delete/DeleteStrategy.java +++ b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.delete; +package net.staticstudios.data; public enum DeleteStrategy { /** @@ -11,8 +11,6 @@ public enum DeleteStrategy { NO_ACTION, /** * This is only for use in PersistentCollections created via - * {@link PersistentCollection#oneToMany} or - * {@link PersistentCollection#manyToMany} */ UNLINK //todo: this } diff --git a/src/main/java/net/staticstudios/data/util/ForeignColumn.java b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java similarity index 77% rename from src/main/java/net/staticstudios/data/util/ForeignColumn.java rename to annotations/src/main/java/net/staticstudios/data/ForeignColumn.java index d079001e..f9ec3be5 100644 --- a/src/main/java/net/staticstudios/data/util/ForeignColumn.java +++ b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java @@ -1,7 +1,4 @@ -package net.staticstudios.data.util; - -import net.staticstudios.data.delete.DeleteStrategy; -import net.staticstudios.data.insert.InsertStrategy; +package net.staticstudios.data; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/main/java/net/staticstudios/data/util/IdColumn.java b/annotations/src/main/java/net/staticstudios/data/IdColumn.java similarity index 82% rename from src/main/java/net/staticstudios/data/util/IdColumn.java rename to annotations/src/main/java/net/staticstudios/data/IdColumn.java index 737b3d15..5572b661 100644 --- a/src/main/java/net/staticstudios/data/util/IdColumn.java +++ b/annotations/src/main/java/net/staticstudios/data/IdColumn.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.util; +package net.staticstudios.data; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/main/java/net/staticstudios/data/insert/InsertMode.java b/annotations/src/main/java/net/staticstudios/data/InsertMode.java similarity index 91% rename from src/main/java/net/staticstudios/data/insert/InsertMode.java rename to annotations/src/main/java/net/staticstudios/data/InsertMode.java index 806ef393..b22ba267 100644 --- a/src/main/java/net/staticstudios/data/insert/InsertMode.java +++ b/annotations/src/main/java/net/staticstudios/data/InsertMode.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.insert; +package net.staticstudios.data; public enum InsertMode { /** diff --git a/src/main/java/net/staticstudios/data/insert/InsertStrategy.java b/annotations/src/main/java/net/staticstudios/data/InsertStrategy.java similarity index 85% rename from src/main/java/net/staticstudios/data/insert/InsertStrategy.java rename to annotations/src/main/java/net/staticstudios/data/InsertStrategy.java index aaed7536..82d52874 100644 --- a/src/main/java/net/staticstudios/data/insert/InsertStrategy.java +++ b/annotations/src/main/java/net/staticstudios/data/InsertStrategy.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.insert; +package net.staticstudios.data; public enum InsertStrategy { /** diff --git a/src/main/java/net/staticstudios/data/util/OneToOne.java b/annotations/src/main/java/net/staticstudios/data/OneToOne.java similarity index 80% rename from src/main/java/net/staticstudios/data/util/OneToOne.java rename to annotations/src/main/java/net/staticstudios/data/OneToOne.java index 8c0ab67e..bcd16a6b 100644 --- a/src/main/java/net/staticstudios/data/util/OneToOne.java +++ b/annotations/src/main/java/net/staticstudios/data/OneToOne.java @@ -1,6 +1,5 @@ -package net.staticstudios.data.util; +package net.staticstudios.data; -import net.staticstudios.data.delete.DeleteStrategy; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/build.gradle b/build.gradle index 0d58bc39..a13f542b 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,14 @@ dependencies { implementation 'com.h2database:h2:2.3.232' // implementation 'org.xerial:sqlite-jdbc:3.45.1.0' implementation 'org.jetbrains:annotations:24.0.1' + api project(":annotations") + implementation project(":annotations") + annotationProcessor project(":processor") + compileOnly project(":processor") + + + testAnnotationProcessor project(":processor") + testCompileOnly project(":processor") // testImplementation(platform('org.junit:junit-bom:5.10.3')) // testImplementation('org.junit.jupiter:junit-jupiter') diff --git a/src/og/java/net/staticstudios/data/CachedValue.java b/oldsrc/og/java/net/staticstudios/data/CachedValue.java similarity index 100% rename from src/og/java/net/staticstudios/data/CachedValue.java rename to oldsrc/og/java/net/staticstudios/data/CachedValue.java diff --git a/src/og/java/net/staticstudios/data/DataManager.java b/oldsrc/og/java/net/staticstudios/data/DataManager.java similarity index 99% rename from src/og/java/net/staticstudios/data/DataManager.java rename to oldsrc/og/java/net/staticstudios/data/DataManager.java index a02b1483..3fa5e751 100644 --- a/src/og/java/net/staticstudios/data/DataManager.java +++ b/oldsrc/og/java/net/staticstudios/data/DataManager.java @@ -24,7 +24,6 @@ import net.staticstudios.data.key.DatabaseKey; import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.TaskQueue; -import net.staticstudios.data.util.*; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; import org.jetbrains.annotations.Blocking; diff --git a/src/og/java/net/staticstudios/data/PersistentCollection.java b/oldsrc/og/java/net/staticstudios/data/PersistentCollection.java similarity index 100% rename from src/og/java/net/staticstudios/data/PersistentCollection.java rename to oldsrc/og/java/net/staticstudios/data/PersistentCollection.java diff --git a/src/og/java/net/staticstudios/data/PersistentValue.java b/oldsrc/og/java/net/staticstudios/data/PersistentValue.java similarity index 100% rename from src/og/java/net/staticstudios/data/PersistentValue.java rename to oldsrc/og/java/net/staticstudios/data/PersistentValue.java diff --git a/src/og/java/net/staticstudios/data/Reference.java b/oldsrc/og/java/net/staticstudios/data/Reference.java similarity index 100% rename from src/og/java/net/staticstudios/data/Reference.java rename to oldsrc/og/java/net/staticstudios/data/Reference.java diff --git a/src/og/java/net/staticstudios/data/UniqueData.java b/oldsrc/og/java/net/staticstudios/data/UniqueData.java similarity index 100% rename from src/og/java/net/staticstudios/data/UniqueData.java rename to oldsrc/og/java/net/staticstudios/data/UniqueData.java diff --git a/src/og/java/net/staticstudios/data/ValueSerializer.java b/oldsrc/og/java/net/staticstudios/data/ValueSerializer.java similarity index 100% rename from src/og/java/net/staticstudios/data/ValueSerializer.java rename to oldsrc/og/java/net/staticstudios/data/ValueSerializer.java diff --git a/src/og/java/net/staticstudios/data/data/Data.java b/oldsrc/og/java/net/staticstudios/data/data/Data.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/Data.java rename to oldsrc/og/java/net/staticstudios/data/data/Data.java diff --git a/src/og/java/net/staticstudios/data/data/DataHolder.java b/oldsrc/og/java/net/staticstudios/data/data/DataHolder.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/DataHolder.java rename to oldsrc/og/java/net/staticstudios/data/data/DataHolder.java diff --git a/src/og/java/net/staticstudios/data/data/Deletable.java b/oldsrc/og/java/net/staticstudios/data/data/Deletable.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/Deletable.java rename to oldsrc/og/java/net/staticstudios/data/data/Deletable.java diff --git a/src/og/java/net/staticstudios/data/data/InitialValue.java b/oldsrc/og/java/net/staticstudios/data/data/InitialValue.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/InitialValue.java rename to oldsrc/og/java/net/staticstudios/data/data/InitialValue.java diff --git a/src/og/java/net/staticstudios/data/data/collection/CollectionEntry.java b/oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntry.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/collection/CollectionEntry.java rename to oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntry.java diff --git a/src/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java b/oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java rename to oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java diff --git a/src/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java b/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java rename to oldsrc/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java diff --git a/src/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java b/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java rename to oldsrc/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java diff --git a/src/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java b/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java rename to oldsrc/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java diff --git a/src/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java b/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java rename to oldsrc/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java diff --git a/src/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java b/oldsrc/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java rename to oldsrc/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java diff --git a/src/og/java/net/staticstudios/data/data/value/InitialCachedValue.java b/oldsrc/og/java/net/staticstudios/data/data/value/InitialCachedValue.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/value/InitialCachedValue.java rename to oldsrc/og/java/net/staticstudios/data/data/value/InitialCachedValue.java diff --git a/src/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java b/oldsrc/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java rename to oldsrc/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java diff --git a/src/og/java/net/staticstudios/data/data/value/Value.java b/oldsrc/og/java/net/staticstudios/data/data/value/Value.java similarity index 100% rename from src/og/java/net/staticstudios/data/data/value/Value.java rename to oldsrc/og/java/net/staticstudios/data/data/value/Value.java diff --git a/src/og/java/net/staticstudios/data/impl/CachedValueManager.java b/oldsrc/og/java/net/staticstudios/data/impl/CachedValueManager.java similarity index 100% rename from src/og/java/net/staticstudios/data/impl/CachedValueManager.java rename to oldsrc/og/java/net/staticstudios/data/impl/CachedValueManager.java diff --git a/src/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java b/oldsrc/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java similarity index 99% rename from src/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java rename to oldsrc/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java index a7c27972..5b4010fa 100644 --- a/src/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java +++ b/oldsrc/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java @@ -15,7 +15,6 @@ import net.staticstudios.data.key.CellKey; import net.staticstudios.data.key.CollectionKey; import net.staticstudios.data.key.UniqueIdentifier; -import net.staticstudios.data.util.*; import org.jetbrains.annotations.Blocking; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/og/java/net/staticstudios/data/impl/PersistentValueManager.java b/oldsrc/og/java/net/staticstudios/data/impl/PersistentValueManager.java similarity index 100% rename from src/og/java/net/staticstudios/data/impl/PersistentValueManager.java rename to oldsrc/og/java/net/staticstudios/data/impl/PersistentValueManager.java diff --git a/src/og/java/net/staticstudios/data/impl/pg/PostgresData.java b/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresData.java similarity index 100% rename from src/og/java/net/staticstudios/data/impl/pg/PostgresData.java rename to oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresData.java diff --git a/src/og/java/net/staticstudios/data/impl/pg/PostgresListener.java b/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresListener.java similarity index 100% rename from src/og/java/net/staticstudios/data/impl/pg/PostgresListener.java rename to oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresListener.java diff --git a/src/og/java/net/staticstudios/data/impl/pg/PostgresNotification.java b/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresNotification.java similarity index 100% rename from src/og/java/net/staticstudios/data/impl/pg/PostgresNotification.java rename to oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresNotification.java diff --git a/src/og/java/net/staticstudios/data/impl/pg/PostgresOperation.java b/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresOperation.java similarity index 100% rename from src/og/java/net/staticstudios/data/impl/pg/PostgresOperation.java rename to oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresOperation.java diff --git a/src/og/java/net/staticstudios/data/key/CellKey.java b/oldsrc/og/java/net/staticstudios/data/key/CellKey.java similarity index 100% rename from src/og/java/net/staticstudios/data/key/CellKey.java rename to oldsrc/og/java/net/staticstudios/data/key/CellKey.java diff --git a/src/og/java/net/staticstudios/data/key/CollectionKey.java b/oldsrc/og/java/net/staticstudios/data/key/CollectionKey.java similarity index 100% rename from src/og/java/net/staticstudios/data/key/CollectionKey.java rename to oldsrc/og/java/net/staticstudios/data/key/CollectionKey.java diff --git a/src/og/java/net/staticstudios/data/key/DataKey.java b/oldsrc/og/java/net/staticstudios/data/key/DataKey.java similarity index 100% rename from src/og/java/net/staticstudios/data/key/DataKey.java rename to oldsrc/og/java/net/staticstudios/data/key/DataKey.java diff --git a/src/og/java/net/staticstudios/data/key/DatabaseKey.java b/oldsrc/og/java/net/staticstudios/data/key/DatabaseKey.java similarity index 100% rename from src/og/java/net/staticstudios/data/key/DatabaseKey.java rename to oldsrc/og/java/net/staticstudios/data/key/DatabaseKey.java diff --git a/src/og/java/net/staticstudios/data/key/RedisKey.java b/oldsrc/og/java/net/staticstudios/data/key/RedisKey.java similarity index 100% rename from src/og/java/net/staticstudios/data/key/RedisKey.java rename to oldsrc/og/java/net/staticstudios/data/key/RedisKey.java diff --git a/src/og/java/net/staticstudios/data/key/UniqueIdentifier.java b/oldsrc/og/java/net/staticstudios/data/key/UniqueIdentifier.java similarity index 100% rename from src/og/java/net/staticstudios/data/key/UniqueIdentifier.java rename to oldsrc/og/java/net/staticstudios/data/key/UniqueIdentifier.java diff --git a/src/og/java/net/staticstudios/data/primative/Primitive.java b/oldsrc/og/java/net/staticstudios/data/primative/Primitive.java similarity index 100% rename from src/og/java/net/staticstudios/data/primative/Primitive.java rename to oldsrc/og/java/net/staticstudios/data/primative/Primitive.java diff --git a/src/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java b/oldsrc/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java similarity index 100% rename from src/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java rename to oldsrc/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java diff --git a/src/og/java/net/staticstudios/data/primative/Primitives.java b/oldsrc/og/java/net/staticstudios/data/primative/Primitives.java similarity index 100% rename from src/og/java/net/staticstudios/data/primative/Primitives.java rename to oldsrc/og/java/net/staticstudios/data/primative/Primitives.java diff --git a/src/og/java/net/staticstudios/data/util/BatchInsert.java b/oldsrc/og/java/net/staticstudios/data/util/BatchInsert.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/BatchInsert.java rename to oldsrc/og/java/net/staticstudios/data/util/BatchInsert.java diff --git a/src/og/java/net/staticstudios/data/util/CacheEntry.java b/oldsrc/og/java/net/staticstudios/data/util/CacheEntry.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/CacheEntry.java rename to oldsrc/og/java/net/staticstudios/data/util/CacheEntry.java diff --git a/src/og/java/net/staticstudios/data/util/ConnectionConsumer.java b/oldsrc/og/java/net/staticstudios/data/util/ConnectionConsumer.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/ConnectionConsumer.java rename to oldsrc/og/java/net/staticstudios/data/util/ConnectionConsumer.java diff --git a/src/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java b/oldsrc/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java rename to oldsrc/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java diff --git a/src/og/java/net/staticstudios/data/util/DataDoesNotExistException.java b/oldsrc/og/java/net/staticstudios/data/util/DataDoesNotExistException.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/DataDoesNotExistException.java rename to oldsrc/og/java/net/staticstudios/data/util/DataDoesNotExistException.java diff --git a/src/og/java/net/staticstudios/data/util/DataSourceConfig.java b/oldsrc/og/java/net/staticstudios/data/util/DataSourceConfig.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/DataSourceConfig.java rename to oldsrc/og/java/net/staticstudios/data/util/DataSourceConfig.java diff --git a/src/og/java/net/staticstudios/data/util/DeleteContext.java b/oldsrc/og/java/net/staticstudios/data/util/DeleteContext.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/DeleteContext.java rename to oldsrc/og/java/net/staticstudios/data/util/DeleteContext.java diff --git a/src/og/java/net/staticstudios/data/util/DeletionStrategy.java b/oldsrc/og/java/net/staticstudios/data/util/DeletionStrategy.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/DeletionStrategy.java rename to oldsrc/og/java/net/staticstudios/data/util/DeletionStrategy.java diff --git a/src/og/java/net/staticstudios/data/util/InsertContext.java b/oldsrc/og/java/net/staticstudios/data/util/InsertContext.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/InsertContext.java rename to oldsrc/og/java/net/staticstudios/data/util/InsertContext.java diff --git a/src/og/java/net/staticstudios/data/util/InsertionStrategy.java b/oldsrc/og/java/net/staticstudios/data/util/InsertionStrategy.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/InsertionStrategy.java rename to oldsrc/og/java/net/staticstudios/data/util/InsertionStrategy.java diff --git a/src/og/java/net/staticstudios/data/util/JunctionTable.java b/oldsrc/og/java/net/staticstudios/data/util/JunctionTable.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/JunctionTable.java rename to oldsrc/og/java/net/staticstudios/data/util/JunctionTable.java diff --git a/src/og/java/net/staticstudios/data/util/PostgresUtils.java b/oldsrc/og/java/net/staticstudios/data/util/PostgresUtils.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/PostgresUtils.java rename to oldsrc/og/java/net/staticstudios/data/util/PostgresUtils.java diff --git a/src/og/java/net/staticstudios/data/util/ReflectionUtils.java b/oldsrc/og/java/net/staticstudios/data/util/ReflectionUtils.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/ReflectionUtils.java rename to oldsrc/og/java/net/staticstudios/data/util/ReflectionUtils.java diff --git a/src/og/java/net/staticstudios/data/util/SQLLogger.java b/oldsrc/og/java/net/staticstudios/data/util/SQLLogger.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/SQLLogger.java rename to oldsrc/og/java/net/staticstudios/data/util/SQLLogger.java diff --git a/src/og/java/net/staticstudios/data/util/TaskQueue.java b/oldsrc/og/java/net/staticstudios/data/util/TaskQueue.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/TaskQueue.java rename to oldsrc/og/java/net/staticstudios/data/util/TaskQueue.java diff --git a/src/og/java/net/staticstudios/data/util/ValueUpdate.java b/oldsrc/og/java/net/staticstudios/data/util/ValueUpdate.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/ValueUpdate.java rename to oldsrc/og/java/net/staticstudios/data/util/ValueUpdate.java diff --git a/src/og/java/net/staticstudios/data/util/ValueUpdateHandler.java b/oldsrc/og/java/net/staticstudios/data/util/ValueUpdateHandler.java similarity index 100% rename from src/og/java/net/staticstudios/data/util/ValueUpdateHandler.java rename to oldsrc/og/java/net/staticstudios/data/util/ValueUpdateHandler.java diff --git a/src/ogtest/java/net/staticstudios/data/CachedValueTest.java b/oldsrc/ogtest/java/net/staticstudios/data/CachedValueTest.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/CachedValueTest.java rename to oldsrc/ogtest/java/net/staticstudios/data/CachedValueTest.java diff --git a/src/ogtest/java/net/staticstudios/data/DeletionTest.java b/oldsrc/ogtest/java/net/staticstudios/data/DeletionTest.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/DeletionTest.java rename to oldsrc/ogtest/java/net/staticstudios/data/DeletionTest.java diff --git a/src/ogtest/java/net/staticstudios/data/InsertionTest.java b/oldsrc/ogtest/java/net/staticstudios/data/InsertionTest.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/InsertionTest.java rename to oldsrc/ogtest/java/net/staticstudios/data/InsertionTest.java diff --git a/src/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java b/oldsrc/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java rename to oldsrc/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java diff --git a/src/ogtest/java/net/staticstudios/data/PersistentValueTest.java b/oldsrc/ogtest/java/net/staticstudios/data/PersistentValueTest.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/PersistentValueTest.java rename to oldsrc/ogtest/java/net/staticstudios/data/PersistentValueTest.java diff --git a/src/ogtest/java/net/staticstudios/data/PostgresListenerTest.java b/oldsrc/ogtest/java/net/staticstudios/data/PostgresListenerTest.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/PostgresListenerTest.java rename to oldsrc/ogtest/java/net/staticstudios/data/PostgresListenerTest.java diff --git a/src/ogtest/java/net/staticstudios/data/PrimitivesTest.java b/oldsrc/ogtest/java/net/staticstudios/data/PrimitivesTest.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/PrimitivesTest.java rename to oldsrc/ogtest/java/net/staticstudios/data/PrimitivesTest.java diff --git a/src/ogtest/java/net/staticstudios/data/ReferenceTest.java b/oldsrc/ogtest/java/net/staticstudios/data/ReferenceTest.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/ReferenceTest.java rename to oldsrc/ogtest/java/net/staticstudios/data/ReferenceTest.java diff --git a/src/ogtest/java/net/staticstudios/data/misc/DataTest.java b/oldsrc/ogtest/java/net/staticstudios/data/misc/DataTest.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/misc/DataTest.java rename to oldsrc/ogtest/java/net/staticstudios/data/misc/DataTest.java diff --git a/src/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java b/oldsrc/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java rename to oldsrc/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java diff --git a/src/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java b/oldsrc/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java rename to oldsrc/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java diff --git a/src/ogtest/java/net/staticstudios/data/misc/TestUtils.java b/oldsrc/ogtest/java/net/staticstudios/data/misc/TestUtils.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/misc/TestUtils.java rename to oldsrc/ogtest/java/net/staticstudios/data/misc/TestUtils.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java diff --git a/src/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java similarity index 100% rename from src/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java rename to oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java diff --git a/src/ogtest/resources/log4j.properties b/oldsrc/ogtest/resources/log4j.properties similarity index 100% rename from src/ogtest/resources/log4j.properties rename to oldsrc/ogtest/resources/log4j.properties diff --git a/oldsrc/test/java/net/staticstudios/data/PersistentValueTest.java b/oldsrc/test/java/net/staticstudios/data/PersistentValueTest.java new file mode 100644 index 00000000..8893abc6 --- /dev/null +++ b/oldsrc/test/java/net/staticstudios/data/PersistentValueTest.java @@ -0,0 +1,119 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.mock.MockUser; +import net.staticstudios.data.mock.MockUserSettings; +import net.staticstudios.data.util.ColumnValuePair; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class PersistentValueTest extends DataTest { + + @Test + public void testReadData() throws SQLException { + List userIds = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + userIds.add(UUID.randomUUID()); + } + + try (PreparedStatement ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS public.users (id UUID PRIMARY KEY, name TEXT, age INT)")) { + ps.execute(); + } + + Connection connection = getConnection(); + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO public.users (id, name) VALUES (?, ?)")) { + for (UUID id : userIds) { + ps.setObject(1, id); + ps.setString(2, "user " + id); + ps.addBatch(); + } + ps.executeBatch(); + } + + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + for (UUID id : userIds) { + MockUser user = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); + assertEquals("user " + id, user.name.get()); + assertNull(user.age.get()); + } + + waitForDataPropagation(); + } + + @Test + public void test() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUser.create(dataManager, id, "test user"); + assertEquals("test user", mockUser.name.get()); + mockUser.name.set("updated name"); + assertEquals("updated name", mockUser.name.get()); + + assertNull(mockUser.age.get()); + mockUser.age.set(25); + assertEquals(25, mockUser.age.get()); + + mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); + mockUser = null; // remove strong reference + System.gc(); + mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss + + assertNull(mockUser.favoriteColor.get()); + mockUser.favoriteColor.set("blue"); + assertEquals("blue", mockUser.favoriteColor.get()); + +// long start; +// int count = 10_000; +// for (int j = 0; j < 5; j++) { +// start = System.currentTimeMillis(); +// for (int i = 0; i < count; i++) { +// mockUser.name.set("name " + i); +// } +// +// System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " updates"); +// } +// for (int j = 0; j < 5; j++) { +// start = System.currentTimeMillis(); +// for (int i = 0; i < count; i++) { +// mockUser.name.get(); +// } +// System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " gets"); +// } + + waitForDataPropagation(); + + MockUserSettings settings = MockUserSettings.create(dataManager, UUID.randomUUID()); + assertNull(mockUser.settings.get()); + mockUser.settings.set(settings); + assertEquals(settings, mockUser.settings.get()); + } + + @Test + public void testUpdateHandlerRegistration() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + assertEquals(0, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); + MockUser mockUser = MockUser.create(dataManager, id, "test user"); + //first instance was created, handler should be registered + assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); + mockUser = null; // remove strong reference + System.gc(); + mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss + //the handler for this pv should not have been registered again + assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); + + mockUser.name.set("new name"); + } +} \ No newline at end of file diff --git a/oldsrc/test/java/net/staticstudios/data/SQLParseTest.java b/oldsrc/test/java/net/staticstudios/data/SQLParseTest.java new file mode 100644 index 00000000..2097e9c0 --- /dev/null +++ b/oldsrc/test/java/net/staticstudios/data/SQLParseTest.java @@ -0,0 +1,15 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.mock.MockUser; +import org.junit.jupiter.api.Test; + +public class SQLParseTest extends DataTest { + + @Test + public void testParse() { + DataManager dm = getMockEnvironments().getFirst().dataManager(); + dm.extractMetadata(MockUser.class); + dm.getSQLBuilder().parse(MockUser.class).forEach(System.out::println); + } +} \ No newline at end of file diff --git a/oldsrc/test/java/net/staticstudios/data/ValueParseTest.java b/oldsrc/test/java/net/staticstudios/data/ValueParseTest.java new file mode 100644 index 00000000..ddc68a0e --- /dev/null +++ b/oldsrc/test/java/net/staticstudios/data/ValueParseTest.java @@ -0,0 +1,32 @@ +package net.staticstudios.data; + +import net.staticstudios.data.util.EnvironmentVariableAccessor; +import net.staticstudios.data.util.ValueUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ValueParseTest { + + @BeforeAll + public static void setup() { + ValueUtils.ENVIRONMENT_VARIABLE_ACCESSOR = new EnvironmentVariableAccessor() { + @Override + public String getEnv(String name) { + return switch (name) { + case "env_var" -> "value_here"; + case "ENV_VAR2" -> "VALUE2_HERE"; + default -> null; + }; + } + }; + } + + @Test + public void testParse() { + assertEquals("value", ValueUtils.parseValue("value")); + assertEquals("value_here", ValueUtils.parseValue("${env_var}")); + assertEquals("VALUE2_HERE", ValueUtils.parseValue("${ENV_VAR2}")); + } +} \ No newline at end of file diff --git a/oldsrc/test/java/net/staticstudios/data/misc/DataTest.java b/oldsrc/test/java/net/staticstudios/data/misc/DataTest.java new file mode 100644 index 00000000..0891cc29 --- /dev/null +++ b/oldsrc/test/java/net/staticstudios/data/misc/DataTest.java @@ -0,0 +1,112 @@ +package net.staticstudios.data.misc; + +import com.redis.testcontainers.RedisContainer; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.util.DataSourceConfig; +import net.staticstudios.utils.ThreadUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; +import redis.clients.jedis.Jedis; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +public class DataTest { + public static final int NUM_ENVIRONMENTS = 1; + public static RedisContainer redis; + public static PostgreSQLContainer postgres = new PostgreSQLContainer<>( + "postgres:16.2" + ); + public static DataSourceConfig dataSourceConfig; + private static Connection connection; + private static Jedis jedis; + private List mockEnvironments; + + @BeforeAll + static void initPostgres() throws IOException, SQLException, InterruptedException { + postgres.start(); + redis = new RedisContainer(DockerImageName.parse("redis:6.2.6")); + redis.start(); + + redis.execInContainer("redis-cli", "config", "set", "notify-keyspace-events", "KEA"); + + dataSourceConfig = new DataSourceConfig( + postgres.getHost(), + postgres.getFirstMappedPort(), + postgres.getDatabaseName(), + postgres.getUsername(), + postgres.getPassword(), + redis.getHost(), + redis.getRedisPort() + ); + + connection = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + jedis = new Jedis(redis.getHost(), redis.getRedisPort()); + } + + @AfterAll + public static void cleanup() throws IOException { + postgres.stop(); + redis.stop(); + } + + public static Connection getConnection() { + return connection; + } + + public static Jedis getJedis() { + return jedis; + } + + @BeforeEach + public void setupMockEnvironments() { + mockEnvironments = new LinkedList<>(); + ThreadUtils.setProvider(new MockThreadProvider()); + for (int i = 0; i < NUM_ENVIRONMENTS; i++) { + mockEnvironments.add(createMockEnvironment()); + } + } + + protected MockEnvironment createMockEnvironment() { + DataManager dataManager = new DataManager(dataSourceConfig); + + MockEnvironment mockEnvironment = new MockEnvironment(dataSourceConfig, dataManager); + mockEnvironments.add(mockEnvironment); + return mockEnvironment; + } + + @AfterEach + public void teardownThreadUtils() { + ThreadUtils.shutdown(); + } + + public int getNumEnvironments() { + return mockEnvironments.size(); + } + + public List getMockEnvironments() { + return mockEnvironments; + } + + public int getWaitForDataPropagationTime() { + return 500 + (Objects.equals(System.getenv("GITHUB_ACTIONS"), "true") ? 1000 : 0); + } + + public void waitForDataPropagation() { + try { + Thread.sleep(getWaitForDataPropagationTime()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/oldsrc/test/java/net/staticstudios/data/misc/MockEnvironment.java b/oldsrc/test/java/net/staticstudios/data/misc/MockEnvironment.java new file mode 100644 index 00000000..a5b37ed1 --- /dev/null +++ b/oldsrc/test/java/net/staticstudios/data/misc/MockEnvironment.java @@ -0,0 +1,10 @@ +package net.staticstudios.data.misc; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.util.DataSourceConfig; + +public record MockEnvironment( + DataSourceConfig dataSourceConfig, + DataManager dataManager +) { +} diff --git a/oldsrc/test/java/net/staticstudios/data/misc/MockThreadProvider.java b/oldsrc/test/java/net/staticstudios/data/misc/MockThreadProvider.java new file mode 100644 index 00000000..b40bff74 --- /dev/null +++ b/oldsrc/test/java/net/staticstudios/data/misc/MockThreadProvider.java @@ -0,0 +1,125 @@ +package net.staticstudios.data.misc; + +import net.staticstudios.utils.ShutdownStage; +import net.staticstudios.utils.ShutdownTask; +import net.staticstudios.utils.ThreadUtilProvider; +import net.staticstudios.utils.ThreadUtils; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.logging.Logger; + +public class MockThreadProvider implements ThreadUtilProvider { + private final ExecutorService mainThreadExecutorService; + private final List syncOnDisableTasksRunNext = Collections.synchronizedList(new ArrayList<>()); + private final List shutdownTasks = Collections.synchronizedList(new ArrayList<>()); + private final Logger logger = Logger.getLogger(MockThreadProvider.class.getName()); + private ExecutorService executorService; + private boolean isShuttingDown = false; + private boolean doneShuttingDown = false; + + + public MockThreadProvider() { + this.mainThreadExecutorService = Executors.newSingleThreadExecutor(); + this.executorService = Executors.newCachedThreadPool((r) -> new Thread(r, "MockThreadProvider")); + } + + @Override + public void submit(Runnable runnable) { + if (doneShuttingDown) { + throw new IllegalStateException("Cannot submit tasks after shutdown"); + } + executorService.submit(() -> { + try { + runnable.run(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + @Override + public void runSync(Runnable runnable) { + if (isShuttingDown) { + syncOnDisableTasksRunNext.add(runnable); + return; + } + + mainThreadExecutorService.submit(runnable); + } + + @Override + public void onShutdownRunSync(ShutdownStage shutdownStage, Runnable runnable) { + shutdownTasks.add(new ShutdownTask(shutdownStage, () -> { + ThreadUtils.safe(runnable); + return null; + }, true)); + } + + @Override + public void onShutdownRunAsync(ShutdownStage shutdownStage, Supplier> task) { + shutdownTasks.add(new ShutdownTask(shutdownStage, task, false)); + + } + + @Override + public boolean isShuttingDown() { + return isShuttingDown; + } + + public void shutdown() { + isShuttingDown = true; + + executorService.shutdown(); + try { + executorService.awaitTermination(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + Map> tasks = new HashMap<>(); + shutdownTasks.forEach(task -> tasks.computeIfAbsent(task.stage(), k -> new ArrayList<>()).add(task)); + + ShutdownStage.getStages() + .forEach(stage -> { + if (tasks.containsKey(stage)) { + getLogger().info("Running shutdown tasks for stage " + stage); + + List> asyncFutures = new ArrayList<>(); + List syncTasks = new ArrayList<>(); + + tasks.get(stage).forEach(task -> { + if (task.sync()) { + syncTasks.add(() -> task.task().get()); + } else { + asyncFutures.add(task.task().get()); + } + }); + + //Wait for all async tasks to finish + try { + CompletableFuture.allOf(asyncFutures.toArray(new CompletableFuture[0])).get(30, TimeUnit.SECONDS); + } catch (Exception e) { + getLogger().severe("Failed to wait for async tasks to finish during shutdown stage " + stage); + e.printStackTrace(); + } + + syncTasks.forEach(Runnable::run); + + syncOnDisableTasksRunNext.forEach(Runnable::run); + + syncOnDisableTasksRunNext.clear(); + } + }); + + doneShuttingDown = true; + } + + private Logger getLogger() { + return logger; + } +} \ No newline at end of file diff --git a/oldsrc/test/java/net/staticstudios/data/misc/TestUtils.java b/oldsrc/test/java/net/staticstudios/data/misc/TestUtils.java new file mode 100644 index 00000000..eeb5a95a --- /dev/null +++ b/oldsrc/test/java/net/staticstudios/data/misc/TestUtils.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.misc; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class TestUtils { + public static int getResultCount(ResultSet rs) throws SQLException { + if (rs.getType() == ResultSet.TYPE_FORWARD_ONLY) { + int count = rs.getRow(); + while (rs.next()) { + count++; + } + return count; + } + + int currentRow = rs.getRow(); + try { + rs.last(); + int rowCount = rs.getRow(); + rs.absolute(currentRow); + return rowCount; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/oldsrc/test/java/net/staticstudios/data/mock/MockUser.java b/oldsrc/test/java/net/staticstudios/data/mock/MockUser.java new file mode 100644 index 00000000..8242d874 --- /dev/null +++ b/oldsrc/test/java/net/staticstudios/data/mock/MockUser.java @@ -0,0 +1,61 @@ +package net.staticstudios.data.mock; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.PersistentValue; +import net.staticstudios.data.Reference; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.annotations.Data; +import net.staticstudios.data.insert.InsertMode; +import net.staticstudios.data.parse.Column; +import net.staticstudios.data.util.ForeignColumn; +import net.staticstudios.data.util.IdColumn; +import net.staticstudios.data.util.OneToOne; + +import java.util.UUID; + +@Data(schema = "public", table = "users") +public class MockUser extends UniqueData { + //todo: @OneToMany, @ManyToMany, @ManyToOne + + + @IdColumn(name = "id") + public PersistentValue id = PersistentValue.of(this, UUID.class); + @Column(name = "settings_id") + public PersistentValue settingsId = PersistentValue.of(this, UUID.class); + @Column(name = "age", nullable = true) + public PersistentValue age; + @ForeignColumn(name = "fav_color", table = "user_preferences", nullable = true, link = "id=user_id") + public PersistentValue favoriteColor; + @OneToOne(link = "settings_id=user_id") + public Reference settings; + @Column(name = "name", index = true) + public PersistentValue name = PersistentValue.of(this, String.class) + .onUpdate(MockUser.class, (user, update) -> { +// System.out.println("User " + user.id.get() + " changed name from " + update.oldValue() + " to " + update.newValue()); + }) + .withDefault("Unknown"); + + public static MockUser create(DataManager dataManager, UUID id, String name) { + return dataManager.createInsertContext() + .set(MockUser.class, "id", id) //todo: i dislike that we lose type safety + .set(MockUser.class, "name", name) + //todo: add support for unique constraints and test them. + //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. + .insert(InsertMode.SYNC) + .get(MockUser.class); + + + //TODO: generate the following patterns at compile time from the @Data annotation. + + /* MockUser.builder(datamanager) + * .id(id) + * .name(name) + * .insert(InsertMode.SYNC); //returns the inserted object + */ + /* MockUser.builder(datamanager) + * .id(id) + * .name(name) + * .insert(insertContext); //add the object to an existing insert context, and return void. + */ + } +} diff --git a/oldsrc/test/java/net/staticstudios/data/mock/MockUserSettings.java b/oldsrc/test/java/net/staticstudios/data/mock/MockUserSettings.java new file mode 100644 index 00000000..ccb0a8d3 --- /dev/null +++ b/oldsrc/test/java/net/staticstudios/data/mock/MockUserSettings.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.mock; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.PersistentValue; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.annotations.Data; +import net.staticstudios.data.insert.InsertMode; +import net.staticstudios.data.parse.Column; +import net.staticstudios.data.util.IdColumn; + +import java.util.UUID; + +@Data(schema = "public", table = "user_settings") +public class MockUserSettings extends UniqueData { + @IdColumn(name = "user_id") + public PersistentValue id; + @Column(name = "font_size") + public PersistentValue fontSide; + + public static MockUserSettings create(DataManager dataManager, UUID id) { + return dataManager.createInsertContext() + .set(MockUserSettings.class, "user_id", id) + //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. + .insert(InsertMode.SYNC) + .get(MockUserSettings.class); + } +} diff --git a/oldsrc/test/resources/log4j.properties b/oldsrc/test/resources/log4j.properties new file mode 100644 index 00000000..c87dc64f --- /dev/null +++ b/oldsrc/test/resources/log4j.properties @@ -0,0 +1,6 @@ +log4j.rootLogger=INFO, STDOUT +log4j.logger.net.staticstudios=TRACE +log4j.logger.deng=INFO +log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender +log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout +log4j.appender.STDOUT.layout.ConversionPattern=%5p %d [%t] (%F:%L) - %m%n \ No newline at end of file diff --git a/processor/build.gradle b/processor/build.gradle new file mode 100644 index 00000000..c72da7ad --- /dev/null +++ b/processor/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' +} + +group = 'net.staticstudios' +version = '3.0.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +java { + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +dependencies { + implementation project(":annotations") + implementation 'com.palantir.javapoet:javapoet:0.7.0' + + compileOnly 'com.google.auto.service:auto-service-annotations:1.1.1' + annotationProcessor 'com.google.auto.service:auto-service:1.1.1' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java new file mode 100644 index 00000000..9b34a584 --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -0,0 +1,201 @@ +package net.staticstudios.data.processor; + +import com.palantir.javapoet.*; +import net.staticstudios.data.Column; +import net.staticstudios.data.Data; +import net.staticstudios.data.ForeignColumn; +import net.staticstudios.data.IdColumn; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.*; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.tools.Diagnostic; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@SupportedAnnotationTypes("net.staticstudios.data.annotations.Data") +@SupportedSourceVersion(SourceVersion.RELEASE_21) +public class DataProcessor extends AbstractProcessor { + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + for (Element annotated : roundEnv.getElementsAnnotatedWith(Data.class)) { + if (!(annotated instanceof TypeElement type)) continue; + + try { + generateFactory(type); + } catch (IOException e) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + "Failed to generate factory: " + e.getMessage(), + annotated + ); + } + } + return true; + } + + private void generateFactory(TypeElement entityType) throws IOException { //todo: if the class is abstract dont generate it. furthermore, we need to handle inheritance properly. + String entityName = entityType.getSimpleName().toString(); + String factoryName = entityName + "Factory"; + PackageElement pkg = processingEnv.getElementUtils().getPackageOf(entityType); + String packageName = pkg.isUnnamed() ? "" : pkg.getQualifiedName().toString(); + + ClassName entityClass = ClassName.get(packageName, entityName); + ClassName dataManager = ClassName.get("net.staticstudios.data", "DataManager"); + ClassName insertMode = ClassName.get("net.staticstudios.data.annotations", "InsertMode"); + ClassName insertContext = ClassName.get("net.staticstudios.data.insert", "InsertContext"); + + + List valueMetaData = collectProperties(entityType, processingEnv.getElementUtils().getTypeElement("net.staticstudios.data.util.Value"), entityType.getAnnotation(Data.class)); + + TypeSpec.Builder builderType = TypeSpec.classBuilder("Builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL); + + builderType.addField(dataManager, "dataManager", Modifier.PRIVATE, Modifier.FINAL); + + builderType.addMethod(MethodSpec.constructorBuilder() + .addParameter(dataManager, "dataManager") + .addStatement("this.dataManager = dataManager") + .build()); + + //todo: support collections and references. + + for (Metadata metadata : valueMetaData) { + builderType.addField(metadata.typeName(), metadata.fieldName(), Modifier.PRIVATE); + + // since we support env variables in the name, parse these at runtime. + builderType.addField(FieldSpec.builder(String.class, metadata.fieldName() + "$Schema", Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), metadata.schema()) + .build()); + builderType.addField(FieldSpec.builder(String.class, metadata.fieldName() + "$Table", Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), metadata.table()) + .build()); + builderType.addField(FieldSpec.builder(String.class, metadata.fieldName() + "$Column", Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), metadata.column()) + .build()); + + builderType.addMethod(MethodSpec.methodBuilder(metadata.fieldName()) + .addModifiers(Modifier.PUBLIC) + .returns(ClassName.get(packageName, factoryName, "Builder")) + .addParameter(metadata.typeName(), metadata.fieldName()) + .addStatement("this.$N = $N", metadata.fieldName(), metadata.fieldName()) + .addStatement("return this") + .build()); + } + + MethodSpec.Builder insertModeMethod = MethodSpec.methodBuilder("insert") + .addModifiers(Modifier.PUBLIC) + .returns(entityClass) + .addParameter(insertMode, "mode") + .addStatement("$T ctx = dataManager.createInsertContext()", insertContext); + + for (Metadata metadata : valueMetaData) { + insertModeMethod.addStatement("ctx.set($N, $N, $N, this.$N)", + metadata.fieldName() + "$Schema", + metadata.fieldName() + "$Table", + metadata.fieldName() + "$Column", + metadata.fieldName()); + } + insertModeMethod.addStatement("return ctx.insert(mode).get($T.class)", entityClass); + + builderType.addMethod(insertModeMethod.build()); + + MethodSpec.Builder insertCtxMethod = MethodSpec.methodBuilder("insert") + .addModifiers(Modifier.PUBLIC) + .returns(TypeName.VOID) + .addParameter(insertContext, "ctx"); + + for (Metadata metadata : valueMetaData) { + insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N)", + metadata.fieldName() + "$Schema", + metadata.fieldName() + "$Table", + metadata.fieldName() + "$Column", + metadata.fieldName()); + } + + builderType.addMethod(insertCtxMethod.build()); + + TypeSpec factory = TypeSpec.classBuilder(factoryName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addMethod(MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .build()) + .addMethod(MethodSpec.methodBuilder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(ClassName.get(packageName, factoryName, "Builder")) + .addParameter(dataManager, "dataManager") + .addStatement("return new Builder(dataManager)") + .build()) + .addMethod(MethodSpec.methodBuilder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(ClassName.get(packageName, factoryName, "Builder")) + .addStatement("return new Builder(DataManager.getInstance())") + .build()) + .addType(builderType.build()) + .build(); + + JavaFile.builder(packageName, factory) + .indent(" ") + .build() + .writeTo(processingEnv.getFiler()); + } + + private List collectProperties(TypeElement type, TypeElement superType, Data dataAnnotation) { + List meta = new ArrayList<>(); + for (VariableElement field : ElementFilter.fieldsIn(type.getEnclosedElements())) { + if (field.getModifiers().contains(Modifier.STATIC)) continue; + + TypeMirror mirror = field.asType(); + if (processingEnv.getTypeUtils().isAssignable(processingEnv.getTypeUtils().erasure(mirror), superType.asType())) { + if (mirror instanceof DeclaredType declared && declared.getTypeArguments().size() == 1) { + TypeMirror inner = declared.getTypeArguments().getFirst(); + meta.add(getMetadata(field, TypeName.get(inner), dataAnnotation)); + } + } + } + return meta; + } + + private Metadata getMetadata(VariableElement field, TypeName typeName, Data dataAnnotation) { + String schemaName = null; + String tableName = null; + String columnName = null; + + IdColumn idColumn = field.getAnnotation(IdColumn.class); + Column column = field.getAnnotation(Column.class); + ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); + + if (idColumn != null) { + tableName = dataAnnotation.table(); + schemaName = dataAnnotation.schema(); + columnName = idColumn.name(); + } else if (column != null) { + tableName = dataAnnotation.table(); + schemaName = dataAnnotation.schema(); + columnName = column.name(); + } else if (foreignColumn != null) { + tableName = foreignColumn.table().isEmpty() ? dataAnnotation.table() : foreignColumn.table(); + schemaName = foreignColumn.schema().isEmpty() ? dataAnnotation.schema() : foreignColumn.schema(); + columnName = foreignColumn.name(); + } + + return new Metadata( + schemaName, + tableName, + columnName, + field.getSimpleName().toString(), + typeName + ); + } + + record Metadata(String schema, String table, String column, String fieldName, TypeName typeName) { + } +} \ No newline at end of file diff --git a/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors b/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 00000000..5d220ddc --- /dev/null +++ b/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1,2 @@ +net.staticstudios.data.processor.DataProcessor,isolating + diff --git a/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 00000000..c8df4050 --- /dev/null +++ b/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +net.staticstudios.data.processor.DataProcessor diff --git a/settings.gradle b/settings.gradle index 1f62a038..c5274d1a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,5 @@ rootProject.name = 'static-data' + +include 'annotations' +include 'processor' \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/DataAccessor.java b/src/main/java/net/staticstudios/data/DataAccessor.java index e4c56cca..b438a136 100644 --- a/src/main/java/net/staticstudios/data/DataAccessor.java +++ b/src/main/java/net/staticstudios/data/DataAccessor.java @@ -1,7 +1,6 @@ package net.staticstudios.data; import net.staticstudios.data.insert.InsertContext; -import net.staticstudios.data.insert.InsertMode; import org.intellij.lang.annotations.Language; import java.sql.ResultSet; diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 9328d0fa..32117922 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -7,8 +7,6 @@ import net.staticstudios.data.impl.h2.H2DataAccessor; import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.insert.InsertContext; -import net.staticstudios.data.insert.InsertMode; -import net.staticstudios.data.parse.Data; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.parse.UniqueDataMetadata; import net.staticstudios.data.util.*; @@ -26,6 +24,8 @@ import java.util.concurrent.CopyOnWriteArrayList; public class DataManager { + private static Boolean useGlobal = null; + private static DataManager instance; private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final String applicationName; private final DataAccessor dataAccessor; @@ -39,6 +39,18 @@ public class DataManager { private final Set registeredUpdateHandlersForColumns = Collections.synchronizedSet(new HashSet<>()); public DataManager(DataSourceConfig dataSourceConfig) { + this(dataSourceConfig, true); + } + + public DataManager(DataSourceConfig dataSourceConfig, boolean setGlobal) { + if (setGlobal) { + if (DataManager.useGlobal == false) { + throw new IllegalStateException("DataManager global instance has been disabled"); + } + Preconditions.checkArgument(instance == null, "DataManager instance already exists"); + instance = this; + } + DataManager.useGlobal = setGlobal; applicationName = "static_data_manager_v3-" + UUID.randomUUID(); postgresListener = new PostgresListener(this, dataSourceConfig); sqlBuilder = new SQLBuilder(this); @@ -53,6 +65,11 @@ public DataManager(DataSourceConfig dataSourceConfig) { //todo: support for CachedValues } + public static DataManager getInstance() { + Preconditions.checkState(DataManager.instance != null, "Global DataManager instance has not been initialized"); + return DataManager.instance; + } + public String getApplicationName() { return applicationName; } @@ -285,7 +302,10 @@ public void submitAsyncTask(ConnectionJedisConsumer task) { public void insert(InsertContext insertContext, InsertMode insertMode) { //todo: process default values for any schemas involved. - // note: defaults should be applied by the db, not us. + // note: defaults should be applied by us, but db defaults may be applied for primative types if not null is specified. for example an Integer will by default be 0 in the db, if not nullable. + + //todo: when inserting validate all id column values are present + try { insertContext.markInserted(); dataAccessor.insert(insertContext, insertMode); diff --git a/src/main/java/net/staticstudios/data/PersistentValue.java b/src/main/java/net/staticstudios/data/PersistentValue.java index 614db40e..afefc6d3 100644 --- a/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/src/main/java/net/staticstudios/data/PersistentValue.java @@ -2,10 +2,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Supplier; -import net.staticstudios.data.util.ColumnMetadata; -import net.staticstudios.data.util.PersistentValueMetadata; -import net.staticstudios.data.util.ValueUpdateHandler; -import net.staticstudios.data.util.ValueUpdateHandlerWrapper; +import net.staticstudios.data.util.*; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/net/staticstudios/data/delete/Delete.java b/src/main/java/net/staticstudios/data/delete/Delete.java index 29506bdd..6b4fac53 100644 --- a/src/main/java/net/staticstudios/data/delete/Delete.java +++ b/src/main/java/net/staticstudios/data/delete/Delete.java @@ -1,5 +1,7 @@ package net.staticstudios.data.delete; +import net.staticstudios.data.DeleteStrategy; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index c871d1f3..eec9a699 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -5,7 +5,9 @@ import net.staticstudios.data.DataAccessor; import net.staticstudios.data.PersistentValue; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.parse.Column; +import net.staticstudios.data.Column; +import net.staticstudios.data.ForeignColumn; +import net.staticstudios.data.IdColumn; import net.staticstudios.data.util.*; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java index 3a334da6..e7b8270e 100644 --- a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -4,6 +4,7 @@ import net.staticstudios.data.DataAccessor; import net.staticstudios.data.Reference; import net.staticstudios.data.UniqueData; +import net.staticstudios.data.OneToOne; import net.staticstudios.data.parse.UniqueDataMetadata; import net.staticstudios.data.util.*; import org.intellij.lang.annotations.Language; diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index 0217de7c..650a3c98 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -3,9 +3,9 @@ import com.impossibl.postgres.api.jdbc.PGConnection; import net.staticstudios.data.DataAccessor; import net.staticstudios.data.DataManager; +import net.staticstudios.data.InsertMode; import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.insert.InsertContext; -import net.staticstudios.data.insert.InsertMode; import net.staticstudios.data.util.ColumnMetadata; import net.staticstudios.data.util.SQlStatement; import net.staticstudios.data.util.TaskQueue; diff --git a/src/main/java/net/staticstudios/data/insert/Insert.java b/src/main/java/net/staticstudios/data/insert/Insert.java index ff168f4d..88b79201 100644 --- a/src/main/java/net/staticstudios/data/insert/Insert.java +++ b/src/main/java/net/staticstudios/data/insert/Insert.java @@ -1,5 +1,7 @@ package net.staticstudios.data.insert; +import net.staticstudios.data.InsertStrategy; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/main/java/net/staticstudios/data/insert/InsertContext.java b/src/main/java/net/staticstudios/data/insert/InsertContext.java index 4018c57c..858d09ee 100644 --- a/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.DataManager; import net.staticstudios.data.UniqueData; +import net.staticstudios.data.InsertMode; import net.staticstudios.data.parse.SQLColumn; import net.staticstudios.data.parse.SQLSchema; import net.staticstudios.data.parse.SQLTable; @@ -33,6 +34,9 @@ public InsertContext set(Class holderClass, String col } public InsertContext set(String schema, String table, String column, @Nullable Object value) { + if (value == null) { + return this; //todo: realistically we should validate the nullability stuff when we actually insert for better consistency. + } Preconditions.checkState(!inserted.get(), "Cannot modify InsertContext after it has been inserted"); SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(schema); Preconditions.checkNotNull(sqlSchema, "Schema not found: " + schema); @@ -46,7 +50,7 @@ public InsertContext set(String schema, String table, String column, @Nullable O ColumnMetadata columnMetadata = new ColumnMetadata(column, sqlColumn.getType(), sqlColumn.isNullable(), sqlColumn.isIndexed(), table, schema); entries.put(columnMetadata, value); return this; - }//todo: when inserting validate all id column values are present + } public Map getEntries() { return entries; diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index d1da6d7d..f6fba491 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -4,6 +4,10 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.Relation; import net.staticstudios.data.UniqueData; +import net.staticstudios.data.Column; +import net.staticstudios.data.Data; +import net.staticstudios.data.ForeignColumn; +import net.staticstudios.data.IdColumn; import net.staticstudios.data.util.*; import org.jetbrains.annotations.Nullable; @@ -60,7 +64,7 @@ public List parse(Class clazz) { return parsedSchemas.get(name); } - private List getDefs(Collection schemas) { + private List getDefs(Collection schemas) { //todo: add fkeys, indexes, uniques, nullables, defaults, etc List statements = new ArrayList<>(); for (SQLSchema schema : schemas) { statements.add("CREATE SCHEMA IF NOT EXISTS \"" + schema.getName() + "\";"); diff --git a/src/main/java/net/staticstudios/data/util/AbstractBuilder.java b/src/main/java/net/staticstudios/data/util/AbstractBuilder.java new file mode 100644 index 00000000..1781e6e3 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/AbstractBuilder.java @@ -0,0 +1,14 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +public class AbstractBuilder { +// private final DataManager dataManager; +// private final Class holderClass; +// private final Map toInsert; +// +// protected void set(String schema, String table, String column, Object value) { +// String key = schema + "." + table + "." + column; +// toInsert.put(key, value); +// } +} diff --git a/src/main/java/net/staticstudios/data/Value.java b/src/main/java/net/staticstudios/data/util/Value.java similarity index 63% rename from src/main/java/net/staticstudios/data/Value.java rename to src/main/java/net/staticstudios/data/util/Value.java index cd400ac3..de52834c 100644 --- a/src/main/java/net/staticstudios/data/Value.java +++ b/src/main/java/net/staticstudios/data/util/Value.java @@ -1,7 +1,7 @@ -package net.staticstudios.data; +package net.staticstudios.data.util; public interface Value { T get(); void set(T value); -} +} \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java b/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java index 0913c248..5526ff9b 100644 --- a/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java +++ b/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java @@ -11,7 +11,7 @@ public class ValueUpdateHandlerWrapper { public ValueUpdateHandlerWrapper(ValueUpdateHandler handler, Class dataType, Class holderClass) { if (handler.getClass().getDeclaredFields().length > 0) { - throw new ValueUpdateHandlerNonStaticException("Value update handler must not capture any variables! It must act as a static function."); + throw new ValueUpdateHandlerNonStaticException("Value update handler must not capture any variables! It must act as a static function. Did you reference 'this' or a member variable? Use the provided instance instead!"); // 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. } diff --git a/src/test/java/net/staticstudios/data/MultiEnvironmentTest.java b/src/test/java/net/staticstudios/data/MultiEnvironmentTest.java new file mode 100644 index 00000000..366e8468 --- /dev/null +++ b/src/test/java/net/staticstudios/data/MultiEnvironmentTest.java @@ -0,0 +1,23 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.misc.MockEnvironment; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + +public class MultiEnvironmentTest extends DataTest { + + private MockEnvironment environment1; + private MockEnvironment environment2; + + @BeforeAll + public static void setup() { + NUM_ENVIRONMENTS = 2; + } + + @BeforeEach + public void setEnvironments() { + this.environment1 = getMockEnvironments().getFirst(); + this.environment2 = getMockEnvironments().get(1); + } +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 8893abc6..0620a608 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -1,13 +1,13 @@ package net.staticstudios.data; import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.mock.MockUser; +import net.staticstudios.data.mock.MockUserFactory; import net.staticstudios.data.mock.MockUserSettings; import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Test; -import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -25,22 +25,15 @@ public void testReadData() throws SQLException { userIds.add(UUID.randomUUID()); } - try (PreparedStatement ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS public.users (id UUID PRIMARY KEY, name TEXT, age INT)")) { - ps.execute(); - } - - Connection connection = getConnection(); - try (PreparedStatement ps = connection.prepareStatement("INSERT INTO public.users (id, name) VALUES (?, ?)")) { - for (UUID id : userIds) { - ps.setObject(1, id); - ps.setString(2, "user " + id); - ps.addBatch(); - } - ps.executeBatch(); - } - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + for (UUID id : userIds) { + MockUserFactory.builder(dataManager) + .id(id) + .name("user " + id) + .insert(InsertMode.SYNC); + } + for (UUID id : userIds) { MockUser user = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); assertEquals("user " + id, user.name.get()); @@ -48,6 +41,15 @@ public void testReadData() throws SQLException { } waitForDataPropagation(); + + MockEnvironment environment2 = createMockEnvironment(); + DataManager dataManager2 = environment2.dataManager(); + dataManager2.load(MockUser.class); + for (UUID id : userIds) { + MockUser user = dataManager2.get(MockUser.class, ColumnValuePair.of("id", id)); + assertEquals("user " + id, user.name.get()); + assertNull(user.age.get()); + } } @Test @@ -55,7 +57,10 @@ public void test() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - MockUser mockUser = MockUser.create(dataManager, id, "test user"); + MockUser mockUser = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); assertEquals("test user", mockUser.name.get()); mockUser.name.set("updated name"); assertEquals("updated name", mockUser.name.get()); @@ -105,7 +110,11 @@ public void testUpdateHandlerRegistration() { dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); assertEquals(0, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); - MockUser mockUser = MockUser.create(dataManager, id, "test user"); + MockUser mockUser = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); + assertEquals("test user", mockUser.name.get()); //first instance was created, handler should be registered assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); mockUser = null; // remove strong reference @@ -113,7 +122,16 @@ public void testUpdateHandlerRegistration() { mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss //the handler for this pv should not have been registered again assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); + assertEquals(0, mockUser.nameUpdates.get()); mockUser.name.set("new name"); + assertEquals(1, mockUser.nameUpdates.get()); + mockUser.name.set("new name"); + assertEquals(1, mockUser.nameUpdates.get()); + mockUser.name.set("new name2"); + assertEquals(2, mockUser.nameUpdates.get()); + mockUser.name.set("new name"); + assertEquals(3, mockUser.nameUpdates.get()); + } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/misc/DataTest.java b/src/test/java/net/staticstudios/data/misc/DataTest.java index 0891cc29..e8cd8f89 100644 --- a/src/test/java/net/staticstudios/data/misc/DataTest.java +++ b/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -21,7 +21,7 @@ import java.util.Objects; public class DataTest { - public static final int NUM_ENVIRONMENTS = 1; + public static int NUM_ENVIRONMENTS = 1; public static RedisContainer redis; public static PostgreSQLContainer postgres = new PostgreSQLContainer<>( "postgres:16.2" @@ -77,7 +77,7 @@ public void setupMockEnvironments() { } protected MockEnvironment createMockEnvironment() { - DataManager dataManager = new DataManager(dataSourceConfig); + DataManager dataManager = new DataManager(dataSourceConfig, false); MockEnvironment mockEnvironment = new MockEnvironment(dataSourceConfig, dataManager); mockEnvironments.add(mockEnvironment); diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/MockUser.java index 82ce4ef9..d9f832e1 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/MockUser.java @@ -1,15 +1,6 @@ package net.staticstudios.data.mock; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.Reference; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.insert.InsertMode; -import net.staticstudios.data.parse.Column; -import net.staticstudios.data.parse.Data; -import net.staticstudios.data.util.ForeignColumn; -import net.staticstudios.data.util.IdColumn; -import net.staticstudios.data.util.OneToOne; +import net.staticstudios.data.*; import java.util.UUID; @@ -17,10 +8,9 @@ public class MockUser extends UniqueData { //todo: @OneToMany, @ManyToMany, @ManyToOne - @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); - @Column(name = "settings_id") + @Column(name = "settings_id", nullable = true) public PersistentValue settingsId = PersistentValue.of(this, UUID.class); @Column(name = "age", nullable = true) public PersistentValue age; @@ -28,33 +18,16 @@ public class MockUser extends UniqueData { public PersistentValue favoriteColor; @OneToOne(link = "settings_id=user_id") public Reference settings; + @ForeignColumn(name = "name_updates", table = "user_metadata", link = "id=user_id") + public PersistentValue nameUpdates; @Column(name = "name", index = true) public PersistentValue name = PersistentValue.of(this, String.class) .onUpdate(MockUser.class, (user, update) -> { -// System.out.println("User " + user.id.get() + " changed name from " + update.oldValue() + " to " + update.newValue()); + //todo: nameupdates shouldnt be null but until we get default values implemented we have to do this + user.nameUpdates.set(user.nameUpdates.get() == null ? 1 : user.nameUpdates.get() + 1); }) .withDefault("Unknown"); - public static MockUser create(DataManager dataManager, UUID id, String name) { - return dataManager.createInsertContext() - .set(MockUser.class, "id", id) //todo: i dislike that we lose type safety - .set(MockUser.class, "name", name) - //todo: add support for unique constraints and test them. - //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. - .insert(InsertMode.SYNC) - .get(MockUser.class); - - //TODO: generate the following patterns at compile time from the @Data annotation. - - /* MockUser.builder(datamanager) - * .id(id) - * .name(name) - * .insert(InsertMode.SYNC); //returns the inserted object - */ - /* MockUser.builder(datamanager) - * .id(id) - * .name(name) - * .insert(insertContext); //add the object to an existing insert context, and return void. - */ - } + //todo: add support for unique constraints and test them. + //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. } diff --git a/src/test/java/net/staticstudios/data/mock/MockUserSettings.java b/src/test/java/net/staticstudios/data/mock/MockUserSettings.java index f7882f86..ff9feb11 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUserSettings.java +++ b/src/test/java/net/staticstudios/data/mock/MockUserSettings.java @@ -3,10 +3,10 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.PersistentValue; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.insert.InsertMode; -import net.staticstudios.data.parse.Column; -import net.staticstudios.data.parse.Data; -import net.staticstudios.data.util.IdColumn; +import net.staticstudios.data.Column; +import net.staticstudios.data.Data; +import net.staticstudios.data.IdColumn; +import net.staticstudios.data.InsertMode; import java.util.UUID; From fd05687f2a6c848ca11349b581ad50c162b6f2e4 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Fri, 12 Sep 2025 11:15:42 -0400 Subject: [PATCH 04/75] fix package name --- .../java/net/staticstudios/data/processor/DataProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index 9b34a584..26ce75f0 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.Set; -@SupportedAnnotationTypes("net.staticstudios.data.annotations.Data") +@SupportedAnnotationTypes("net.staticstudios.data.Data") @SupportedSourceVersion(SourceVersion.RELEASE_21) public class DataProcessor extends AbstractProcessor { @Override @@ -50,7 +50,7 @@ private void generateFactory(TypeElement entityType) throws IOException { //todo ClassName entityClass = ClassName.get(packageName, entityName); ClassName dataManager = ClassName.get("net.staticstudios.data", "DataManager"); - ClassName insertMode = ClassName.get("net.staticstudios.data.annotations", "InsertMode"); + ClassName insertMode = ClassName.get("net.staticstudios.data", "InsertMode"); ClassName insertContext = ClassName.get("net.staticstudios.data.insert", "InsertContext"); From 98048feabb473a6612e65072c78b3f9049b92ce8 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 14 Sep 2025 14:13:04 -0400 Subject: [PATCH 05/75] progress --- .../data/processor/DataProcessor.java | 173 ++++++------------ .../data/processor/Metadata.java | 4 + .../data/processor/MetadataUtils.java | 95 ++++++++++ .../processor/PersistentValueMetadata.java | 7 + .../net/staticstudios/data/DataManager.java | 1 + .../data/impl/h2/H2DataAccessor.java | 31 +++- .../data/insert/InsertContext.java | 12 +- .../data/PersistentValueTest.java | 11 +- .../net/staticstudios/data/misc/DataTest.java | 33 +++- .../net/staticstudios/data/mock/MockUser.java | 13 +- 10 files changed, 251 insertions(+), 129 deletions(-) create mode 100644 processor/src/main/java/net/staticstudios/data/processor/Metadata.java create mode 100644 processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java create mode 100644 processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index 26ce75f0..582d187d 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -1,23 +1,19 @@ package net.staticstudios.data.processor; import com.palantir.javapoet.*; -import net.staticstudios.data.Column; import net.staticstudios.data.Data; -import net.staticstudios.data.ForeignColumn; -import net.staticstudios.data.IdColumn; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; -import javax.lang.model.element.*; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.ElementFilter; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -42,11 +38,15 @@ public boolean process(Set annotations, RoundEnvironment return true; } - private void generateFactory(TypeElement entityType) throws IOException { //todo: if the class is abstract dont generate it. furthermore, we need to handle inheritance properly. + private void generateFactory(TypeElement entityType) throws IOException { + if (entityType.getModifiers().contains(Modifier.ABSTRACT)) { + return; + } + String entityName = entityType.getSimpleName().toString(); String factoryName = entityName + "Factory"; - PackageElement pkg = processingEnv.getElementUtils().getPackageOf(entityType); - String packageName = pkg.isUnnamed() ? "" : pkg.getQualifiedName().toString(); + PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(entityType); + String packageName = packageElement.isUnnamed() ? "" : packageElement.getQualifiedName().toString(); ClassName entityClass = ClassName.get(packageName, entityName); ClassName dataManager = ClassName.get("net.staticstudios.data", "DataManager"); @@ -54,73 +54,73 @@ private void generateFactory(TypeElement entityType) throws IOException { //todo ClassName insertContext = ClassName.get("net.staticstudios.data.insert", "InsertContext"); - List valueMetaData = collectProperties(entityType, processingEnv.getElementUtils().getTypeElement("net.staticstudios.data.util.Value"), entityType.getAnnotation(Data.class)); + List metadataList = MetadataUtils.extractMetadata(entityType); TypeSpec.Builder builderType = TypeSpec.classBuilder("Builder") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL); - - builderType.addField(dataManager, "dataManager", Modifier.PRIVATE, Modifier.FINAL); - - builderType.addMethod(MethodSpec.constructorBuilder() - .addParameter(dataManager, "dataManager") - .addStatement("this.dataManager = dataManager") - .build()); + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addField(dataManager, "dataManager", Modifier.PRIVATE, Modifier.FINAL) + .addMethod(MethodSpec.constructorBuilder() + .addParameter(dataManager, "dataManager") + .addStatement("this.dataManager = dataManager") + .build()); //todo: support collections and references. - for (Metadata metadata : valueMetaData) { - builderType.addField(metadata.typeName(), metadata.fieldName(), Modifier.PRIVATE); - - // since we support env variables in the name, parse these at runtime. - builderType.addField(FieldSpec.builder(String.class, metadata.fieldName() + "$Schema", Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), metadata.schema()) - .build()); - builderType.addField(FieldSpec.builder(String.class, metadata.fieldName() + "$Table", Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), metadata.table()) - .build()); - builderType.addField(FieldSpec.builder(String.class, metadata.fieldName() + "$Column", Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), metadata.column()) - .build()); - - builderType.addMethod(MethodSpec.methodBuilder(metadata.fieldName()) - .addModifiers(Modifier.PUBLIC) - .returns(ClassName.get(packageName, factoryName, "Builder")) - .addParameter(metadata.typeName(), metadata.fieldName()) - .addStatement("this.$N = $N", metadata.fieldName(), metadata.fieldName()) - .addStatement("return this") - .build()); - } - MethodSpec.Builder insertModeMethod = MethodSpec.methodBuilder("insert") .addModifiers(Modifier.PUBLIC) .returns(entityClass) .addParameter(insertMode, "mode") .addStatement("$T ctx = dataManager.createInsertContext()", insertContext); - - for (Metadata metadata : valueMetaData) { - insertModeMethod.addStatement("ctx.set($N, $N, $N, this.$N)", - metadata.fieldName() + "$Schema", - metadata.fieldName() + "$Table", - metadata.fieldName() + "$Column", - metadata.fieldName()); - } - insertModeMethod.addStatement("return ctx.insert(mode).get($T.class)", entityClass); - - builderType.addMethod(insertModeMethod.build()); - MethodSpec.Builder insertCtxMethod = MethodSpec.methodBuilder("insert") .addModifiers(Modifier.PUBLIC) .returns(TypeName.VOID) .addParameter(insertContext, "ctx"); - for (Metadata metadata : valueMetaData) { - insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N)", - metadata.fieldName() + "$Schema", - metadata.fieldName() + "$Table", - metadata.fieldName() + "$Column", - metadata.fieldName()); + for (Metadata metadata : metadataList) { + if (metadata instanceof PersistentValueMetadata( + String schema, String table, String column, String fieldName, TypeName genericType + )) { + String schemaFieldName = fieldName + "$schema"; + String tableFieldName = fieldName + "$table"; + String columnFieldName = fieldName + "$column"; + + + builderType.addField(genericType, fieldName, Modifier.PRIVATE); + + // since we support env variables in the name, parse these at runtime. + builderType.addField(FieldSpec.builder(String.class, schemaFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), schema) + .build()); + builderType.addField(FieldSpec.builder(String.class, tableFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), table) + .build()); + builderType.addField(FieldSpec.builder(String.class, columnFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), column) + .build()); + + builderType.addMethod(MethodSpec.methodBuilder(fieldName) + .addModifiers(Modifier.PUBLIC) + .returns(ClassName.get(packageName, factoryName, "Builder")) + .addParameter(genericType, fieldName) + .addStatement("this.$N = $N", fieldName, fieldName) + .addStatement("return this") + .build()); + + insertModeMethod.addStatement("ctx.set($N, $N, $N, this.$N)", + schemaFieldName, + tableFieldName, + columnFieldName, + fieldName); + insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N)", + schemaFieldName, + tableFieldName, + columnFieldName, + fieldName); + } } + insertModeMethod.addStatement("return ctx.insert(mode).get($T.class)", entityClass); + builderType.addMethod(insertModeMethod.build()); builderType.addMethod(insertCtxMethod.build()); TypeSpec factory = TypeSpec.classBuilder(factoryName) @@ -147,55 +147,4 @@ private void generateFactory(TypeElement entityType) throws IOException { //todo .build() .writeTo(processingEnv.getFiler()); } - - private List collectProperties(TypeElement type, TypeElement superType, Data dataAnnotation) { - List meta = new ArrayList<>(); - for (VariableElement field : ElementFilter.fieldsIn(type.getEnclosedElements())) { - if (field.getModifiers().contains(Modifier.STATIC)) continue; - - TypeMirror mirror = field.asType(); - if (processingEnv.getTypeUtils().isAssignable(processingEnv.getTypeUtils().erasure(mirror), superType.asType())) { - if (mirror instanceof DeclaredType declared && declared.getTypeArguments().size() == 1) { - TypeMirror inner = declared.getTypeArguments().getFirst(); - meta.add(getMetadata(field, TypeName.get(inner), dataAnnotation)); - } - } - } - return meta; - } - - private Metadata getMetadata(VariableElement field, TypeName typeName, Data dataAnnotation) { - String schemaName = null; - String tableName = null; - String columnName = null; - - IdColumn idColumn = field.getAnnotation(IdColumn.class); - Column column = field.getAnnotation(Column.class); - ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); - - if (idColumn != null) { - tableName = dataAnnotation.table(); - schemaName = dataAnnotation.schema(); - columnName = idColumn.name(); - } else if (column != null) { - tableName = dataAnnotation.table(); - schemaName = dataAnnotation.schema(); - columnName = column.name(); - } else if (foreignColumn != null) { - tableName = foreignColumn.table().isEmpty() ? dataAnnotation.table() : foreignColumn.table(); - schemaName = foreignColumn.schema().isEmpty() ? dataAnnotation.schema() : foreignColumn.schema(); - columnName = foreignColumn.name(); - } - - return new Metadata( - schemaName, - tableName, - columnName, - field.getSimpleName().toString(), - typeName - ); - } - - record Metadata(String schema, String table, String column, String fieldName, TypeName typeName) { - } } \ No newline at end of file diff --git a/processor/src/main/java/net/staticstudios/data/processor/Metadata.java b/processor/src/main/java/net/staticstudios/data/processor/Metadata.java new file mode 100644 index 00000000..00d240dd --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/processor/Metadata.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.processor; + +public interface Metadata { +} diff --git a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java new file mode 100644 index 00000000..7046cecb --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java @@ -0,0 +1,95 @@ +package net.staticstudios.data.processor; + +import com.palantir.javapoet.TypeName; +import net.staticstudios.data.Column; +import net.staticstudios.data.Data; +import net.staticstudios.data.ForeignColumn; +import net.staticstudios.data.IdColumn; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MetadataUtils { + private static final String FQN_OBJECT = Object.class.getName(); + private static final String FQN_PERSISTENT_VALUE = "net.staticstudios.data.PersistentValue"; + + public static List extractMetadata(TypeElement typeElement) { + Data dataAnnotation = typeElement.getAnnotation(Data.class); + if (dataAnnotation == null) { + return Collections.emptyList(); + } + + + List metadata = new ArrayList<>(); + extractMetadata(dataAnnotation, metadata, typeElement); + + return metadata; + } + + private static void extractMetadata(Data dataAnnotation, List list, TypeElement typeElement) { + for (VariableElement field : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) { + if (field.getModifiers().contains(Modifier.STATIC)) { + continue; + } + + TypeMirror mirror = field.asType(); + if (!(mirror instanceof DeclaredType declaredType)) { + continue; + } + TypeElement fieldTypeElement = (TypeElement) declaredType.asElement(); + if (fieldTypeElement.getQualifiedName().toString().equals(FQN_PERSISTENT_VALUE)) { + TypeMirror innerType = declaredType.getTypeArguments().getFirst(); + PersistentValueMetadata metadata = getPersistentValueMetadata(dataAnnotation, field, TypeName.get(innerType)); + list.add(metadata); + } + } + + TypeMirror superClass = typeElement.getSuperclass(); + if (superClass instanceof DeclaredType declaredSuper && declaredSuper.asElement() instanceof TypeElement superElement) { + if (superClass.getKind() != TypeKind.NONE && superClass.getKind() != TypeKind.VOID && !superElement.getQualifiedName().toString().equals(FQN_OBJECT)) { + extractMetadata(dataAnnotation, list, superElement); + } + } + } + + private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnotation, VariableElement field, TypeName typeName) { + String schemaName = null; + String tableName = null; + String columnName = null; + + IdColumn idColumn = field.getAnnotation(IdColumn.class); + Column column = field.getAnnotation(Column.class); + ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); + + if (idColumn != null) { + tableName = dataAnnotation.table(); + schemaName = dataAnnotation.schema(); + columnName = idColumn.name(); + } else if (column != null) { + tableName = dataAnnotation.table(); + schemaName = dataAnnotation.schema(); + columnName = column.name(); + } else if (foreignColumn != null) { + tableName = foreignColumn.table().isEmpty() ? dataAnnotation.table() : foreignColumn.table(); + schemaName = foreignColumn.schema().isEmpty() ? dataAnnotation.schema() : foreignColumn.schema(); + columnName = foreignColumn.name(); + } + + return new PersistentValueMetadata( + schemaName, + tableName, + columnName, + field.getSimpleName().toString(), + typeName + ); + } + +} diff --git a/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java b/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java new file mode 100644 index 00000000..8ca98404 --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.processor; + +import com.palantir.javapoet.TypeName; + +public record PersistentValueMetadata(String schema, String table, String column, String fieldName, + TypeName genericType) implements Metadata { +} diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 32117922..27750051 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -96,6 +96,7 @@ public void addUpdateHandler(String schema, String table, String column, ValueUp //todo: when a row is updated, provide the entire row to this method (so we can grab id cols). this is the responsibility of the data accessor impl @ApiStatus.Internal public void callUpdateHandlers(List columnNames, String schema, String table, String column, Object[] oldSerializedValues, Object[] newSerializedValues) { + logger.trace("Calling update handlers for {}/{}/{} with old values {} and new values {}", schema, table, column, Arrays.toString(oldSerializedValues), Arrays.toString(newSerializedValues)); //todo: submit to somewhere for where to run these, configured during setup. default to thread utils Map, List>> handlersForColumn = updateHandlers.get(schema + "." + table + "." + column); if (handlersForColumn == null) { diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index 650a3c98..cf7cbc0a 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -9,6 +9,8 @@ import net.staticstudios.data.util.ColumnMetadata; import net.staticstudios.data.util.SQlStatement; import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.utils.ShutdownStage; +import net.staticstudios.utils.ThreadUtils; import org.intellij.lang.annotations.Language; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +53,18 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener } } }); + + ThreadUtils.onShutdownRunSync(ShutdownStage.FINAL, () -> { + // wipe the db on shutdown. This is especially useful for unit tests. + try (Connection connection = DriverManager.getConnection(jdbcUrl)) { + try (Statement statement = connection.createStatement()) { + String resetDb = "DROP ALL OBJECTS DELETE FILES".toString(); // call toString so that IDEs dont freak out about invalid SQL + statement.execute(resetDb); + } + } catch (SQLException e) { + logger.error("Failed to shutdown H2 database", e); + } + }); } public synchronized void sync(String schema, String table) throws SQLException { @@ -117,6 +131,18 @@ private Connection getConnection() throws SQLException { connection = DriverManager.getConnection(jdbcUrl); connection.setAutoCommit(false); threadConnection.set(connection); + + ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, () -> { + Connection _connection = threadConnection.get(); + if (_connection == null) return; + try { + _connection.close(); + } catch (SQLException e) { + logger.error("Failed to close H2 connection", e); + } finally { + threadConnection.remove(); + } + }); } return connection; } @@ -285,8 +311,10 @@ private synchronized void updateKnownTables() throws SQLException { @Language("SQL") String sql = "CREATE TRIGGER IF NOT EXISTS \"trg_%s_%s\" AFTER INSERT, UPDATE, DELETE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; try (Statement createTrigger = connection.createStatement()) { + String formatted = sql.formatted(table, randomId.toString().replace('-', '_'), schema, table, H2Trigger.class.getName()); + logger.trace("[H2] {}", formatted); H2Trigger.registerDataManager(randomId, dataManager); - createTrigger.execute(sql.formatted(table, randomId.toString().replace('-', '_'), schema, table, H2Trigger.class.getName())); + createTrigger.execute(formatted); } dataManager.submitBlockingTask(realDbConnection -> postgresListener.ensureTableHasTrigger(realDbConnection, schema, table)); @@ -317,4 +345,3 @@ private List getColumnsInTable(String schema, String table) throws SQLEx //todo: maintain a buffer of what to send the the real db, and then collapse similar prepared statements into one so we can batch them. have a configurable interval to flush, but by default this will be 0ms - diff --git a/src/main/java/net/staticstudios/data/insert/InsertContext.java b/src/main/java/net/staticstudios/data/insert/InsertContext.java index 858d09ee..7c106064 100644 --- a/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -34,9 +34,7 @@ public InsertContext set(Class holderClass, String col } public InsertContext set(String schema, String table, String column, @Nullable Object value) { - if (value == null) { - return this; //todo: realistically we should validate the nullability stuff when we actually insert for better consistency. - } + Preconditions.checkState(!inserted.get(), "Cannot modify InsertContext after it has been inserted"); SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(schema); Preconditions.checkNotNull(sqlSchema, "Schema not found: " + schema); @@ -44,10 +42,16 @@ public InsertContext set(String schema, String table, String column, @Nullable O Preconditions.checkNotNull(sqlTable, "Table not found: " + table); SQLColumn sqlColumn = sqlTable.getColumn(column); Preconditions.checkNotNull(sqlColumn, "Column not found: " + column + " in table: " + table + " schema: " + schema); + + ColumnMetadata columnMetadata = new ColumnMetadata(column, sqlColumn.getType(), sqlColumn.isNullable(), sqlColumn.isIndexed(), table, schema); + if (value == null) { + entries.remove(columnMetadata); + return this; //todo: realistically we should validate the nullability stuff when we actually insert for better consistency. + } + Preconditions.checkArgument(value != null || sqlColumn.isNullable(), "Column " + column + " in table " + table + " schema " + schema + " cannot be null"); Preconditions.checkArgument(sqlColumn.getType().isInstance(value), "Value type mismatch for column " + column + " in table " + table + " schema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); - ColumnMetadata columnMetadata = new ColumnMetadata(column, sqlColumn.getType(), sqlColumn.isNullable(), sqlColumn.isIndexed(), table, schema); entries.put(columnMetadata, value); return this; } diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 0620a608..5c05a453 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -122,16 +122,15 @@ public void testUpdateHandlerRegistration() { mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss //the handler for this pv should not have been registered again assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); - assertEquals(0, mockUser.nameUpdates.get()); + assertEquals(0, mockUser.getNameUpdates()); mockUser.name.set("new name"); - assertEquals(1, mockUser.nameUpdates.get()); + assertEquals(1, mockUser.getNameUpdates()); mockUser.name.set("new name"); - assertEquals(1, mockUser.nameUpdates.get()); + assertEquals(1, mockUser.getNameUpdates()); mockUser.name.set("new name2"); - assertEquals(2, mockUser.nameUpdates.get()); + assertEquals(2, mockUser.getNameUpdates()); mockUser.name.set("new name"); - assertEquals(3, mockUser.nameUpdates.get()); - + assertEquals(3, mockUser.getNameUpdates()); } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/misc/DataTest.java b/src/test/java/net/staticstudios/data/misc/DataTest.java index e8cd8f89..711b8745 100644 --- a/src/test/java/net/staticstudios/data/misc/DataTest.java +++ b/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -13,9 +13,7 @@ import redis.clients.jedis.Jedis; import java.io.IOException; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; +import java.sql.*; import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -89,6 +87,35 @@ public void teardownThreadUtils() { ThreadUtils.shutdown(); } + @AfterEach + public void wipeDatabase() throws SQLException { + try (Statement statement = getConnection().createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'public', 'pg_toast')") + ) { + List schemas = new LinkedList<>(); + while (resultSet.next()) { + String schema = resultSet.getString(1); + schemas.add(schema); + } + + for (String schema : schemas) { + statement.executeUpdate(String.format("DROP SCHEMA IF EXISTS \"%s\" CASCADE", schema)); + } + } + try (Statement statement = getConnection().createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT tablename FROM pg_tables WHERE schemaname = 'public'") + ) { + List tables = new LinkedList<>(); + while (resultSet.next()) { + String table = resultSet.getString(1); + tables.add(table); + } + for (String table : tables) { + statement.executeUpdate(String.format("DROP TABLE IF EXISTS \"public\".\"%s\" CASCADE", table)); + } + } + } + public int getNumEnvironments() { return mockEnvironments.size(); } diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/MockUser.java index d9f832e1..f68f5a2e 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/MockUser.java @@ -4,10 +4,14 @@ import java.util.UUID; +//todo: heres how inheritance should look: +// if the super class provides a data annotation, ignore it and use the child's annotation. it would be cool tho to allow the super class to use a @data annotation. the former is whats implemented now. if changed, update the processor. + @Data(schema = "public", table = "users") public class MockUser extends UniqueData { //todo: @OneToMany, @ManyToMany, @ManyToOne + //todo: if nullable is false, find the sql type and set a reasonable default. @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); @Column(name = "settings_id", nullable = true) @@ -23,11 +27,16 @@ public class MockUser extends UniqueData { @Column(name = "name", index = true) public PersistentValue name = PersistentValue.of(this, String.class) .onUpdate(MockUser.class, (user, update) -> { - //todo: nameupdates shouldnt be null but until we get default values implemented we have to do this - user.nameUpdates.set(user.nameUpdates.get() == null ? 1 : user.nameUpdates.get() + 1); + user.nameUpdates.set(user.getNameUpdates() + 1); }) .withDefault("Unknown"); //todo: add support for unique constraints and test them. //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. + + public int getNameUpdates() { + //todo: nameupdates shouldnt be null but until we get default values implemented we have to do this + return nameUpdates.get() == null ? 0 : nameUpdates.get(); + } + } From ef48be92fb79e66c7d918591186bb287ba1d2d71 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 15 Sep 2025 09:32:32 -0400 Subject: [PATCH 06/75] fkeys and more --- .../java/net/staticstudios/data/Column.java | 5 +- .../java/net/staticstudios/data/Data.java | 3 +- .../net/staticstudios/data/ForeignColumn.java | 2 + .../staticstudios/data/utils/StringUtils.java | 9 + .../data/processor/DataProcessor.java | 85 ++++++--- .../ForeignPersistentValueMetadata.java | 18 ++ .../data/processor/MetadataUtils.java | 40 +++- .../processor/PersistentValueMetadata.java | 37 +++- .../net/staticstudios/data/DataAccessor.java | 7 +- .../net/staticstudios/data/DataManager.java | 150 +++++++++++++-- .../staticstudios/data/PersistentValue.java | 36 ---- .../net/staticstudios/data/UniqueData.java | 4 +- .../data/impl/data/PersistentValueImpl.java | 57 +++--- .../data/impl/data/ReferenceImpl.java | 5 +- .../data/impl/h2/H2DataAccessor.java | 153 ++++++++-------- .../staticstudios/data/impl/h2/H2Trigger.java | 2 +- .../data/insert/InsertContext.java | 39 ++-- .../data/parse/DDLStatement.java | 12 ++ .../staticstudios/data/parse/ForeignKey.java | 38 ++++ .../staticstudios/data/parse/SQLBuilder.java | 173 +++++++++++++++--- .../staticstudios/data/parse/SQLColumn.java | 13 +- .../staticstudios/data/parse/SQLTable.java | 18 ++ .../data/util/AbstractBuilder.java | 4 +- .../data/util/ColumnMetadata.java | 6 +- .../data/util/ColumnValuePair.java | 2 +- .../net/staticstudios/data/util/SQLUtils.java | 12 ++ .../staticstudios/data/util/SchemaTable.java | 4 + .../data/util/SimpleColumnMetadata.java | 8 + .../{parse => util}/UniqueDataMetadata.java | 3 +- .../staticstudios/data/util/ValueUtils.java | 11 +- .../data/PersistentValueTest.java | 26 +++ .../net/staticstudios/data/SQLParseTest.java | 88 ++++++++- .../net/staticstudios/data/misc/DataTest.java | 6 +- .../net/staticstudios/data/mock/MockPost.java | 21 +++ .../net/staticstudios/data/mock/MockUser.java | 15 +- .../data/mock/MockUserSettings.java | 11 +- 36 files changed, 826 insertions(+), 297 deletions(-) create mode 100644 annotations/src/main/java/net/staticstudios/data/utils/StringUtils.java create mode 100644 processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java create mode 100644 src/main/java/net/staticstudios/data/parse/DDLStatement.java create mode 100644 src/main/java/net/staticstudios/data/parse/ForeignKey.java create mode 100644 src/main/java/net/staticstudios/data/util/SchemaTable.java create mode 100644 src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java rename src/main/java/net/staticstudios/data/{parse => util}/UniqueDataMetadata.java (72%) create mode 100644 src/test/java/net/staticstudios/data/mock/MockPost.java diff --git a/annotations/src/main/java/net/staticstudios/data/Column.java b/annotations/src/main/java/net/staticstudios/data/Column.java index d38d7ee9..8ea0fd71 100644 --- a/annotations/src/main/java/net/staticstudios/data/Column.java +++ b/annotations/src/main/java/net/staticstudios/data/Column.java @@ -3,7 +3,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -//todo: annotations would break compatability, but they make static analysis easier for meta data parsing and for building sql @Retention(RetentionPolicy.RUNTIME) public @interface Column { String name(); @@ -14,5 +13,7 @@ boolean index() default false; //todo: this - boolean nullable() default false; //todo: this + boolean nullable() default false; + + String defaultValue() default ""; } diff --git a/annotations/src/main/java/net/staticstudios/data/Data.java b/annotations/src/main/java/net/staticstudios/data/Data.java index d7891504..7dbe9b24 100644 --- a/annotations/src/main/java/net/staticstudios/data/Data.java +++ b/annotations/src/main/java/net/staticstudios/data/Data.java @@ -3,8 +3,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -//todo: annotations would break compatability, but they make static analysis easier for meta data parsing and for building sql -@Retention(RetentionPolicy.RUNTIME) //todo: we should support env variables in here as well. +@Retention(RetentionPolicy.RUNTIME) public @interface Data { String schema(); diff --git a/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java index f9ec3be5..7d788819 100644 --- a/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java +++ b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java @@ -15,6 +15,8 @@ boolean index() default false; + String defaultValue() default ""; + String link(); InsertStrategy insertStrategy() default InsertStrategy.OVERWRITE_EXISTING; diff --git a/annotations/src/main/java/net/staticstudios/data/utils/StringUtils.java b/annotations/src/main/java/net/staticstudios/data/utils/StringUtils.java new file mode 100644 index 00000000..0df40733 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/utils/StringUtils.java @@ -0,0 +1,9 @@ +package net.staticstudios.data.utils; + +import java.util.List; + +public class StringUtils { + public static List parseCommaSeperatedList(String input) { + return List.of(input.split(",")); + } +} diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index 582d187d..fd44ced6 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -15,6 +15,7 @@ import javax.tools.Diagnostic; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.Set; @SupportedAnnotationTypes("net.staticstudios.data.Data") @@ -53,7 +54,8 @@ private void generateFactory(TypeElement entityType) throws IOException { ClassName insertMode = ClassName.get("net.staticstudios.data", "InsertMode"); ClassName insertContext = ClassName.get("net.staticstudios.data.insert", "InsertContext"); - + Data dataAnnotation = entityType.getAnnotation(Data.class); + assert dataAnnotation != null; List metadataList = MetadataUtils.extractMetadata(entityType); TypeSpec.Builder builderType = TypeSpec.classBuilder("Builder") @@ -66,60 +68,91 @@ private void generateFactory(TypeElement entityType) throws IOException { //todo: support collections and references. - MethodSpec.Builder insertModeMethod = MethodSpec.methodBuilder("insert") - .addModifiers(Modifier.PUBLIC) - .returns(entityClass) - .addParameter(insertMode, "mode") - .addStatement("$T ctx = dataManager.createInsertContext()", insertContext); + MethodSpec.Builder insertCtxMethod = MethodSpec.methodBuilder("insert") .addModifiers(Modifier.PUBLIC) .returns(TypeName.VOID) .addParameter(insertContext, "ctx"); for (Metadata metadata : metadataList) { - if (metadata instanceof PersistentValueMetadata( - String schema, String table, String column, String fieldName, TypeName genericType - )) { - String schemaFieldName = fieldName + "$schema"; - String tableFieldName = fieldName + "$table"; - String columnFieldName = fieldName + "$column"; + if (metadata instanceof PersistentValueMetadata persistentValueMetadata) { + String schemaFieldName = persistentValueMetadata.fieldName() + "$schema"; + String tableFieldName = persistentValueMetadata.fieldName() + "$table"; + String columnFieldName = persistentValueMetadata.fieldName() + "$column"; - builderType.addField(genericType, fieldName, Modifier.PRIVATE); + builderType.addField(persistentValueMetadata.genericType(), persistentValueMetadata.fieldName(), Modifier.PRIVATE); // since we support env variables in the name, parse these at runtime. builderType.addField(FieldSpec.builder(String.class, schemaFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), schema) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.schema()) .build()); builderType.addField(FieldSpec.builder(String.class, tableFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), table) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.table()) .build()); builderType.addField(FieldSpec.builder(String.class, columnFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), column) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.column()) .build()); - builderType.addMethod(MethodSpec.methodBuilder(fieldName) + builderType.addMethod(MethodSpec.methodBuilder(persistentValueMetadata.fieldName()) .addModifiers(Modifier.PUBLIC) .returns(ClassName.get(packageName, factoryName, "Builder")) - .addParameter(genericType, fieldName) - .addStatement("this.$N = $N", fieldName, fieldName) + .addParameter(persistentValueMetadata.genericType(), persistentValueMetadata.fieldName()) + .addStatement("this.$N = $N", persistentValueMetadata.fieldName(), persistentValueMetadata.fieldName()) .addStatement("return this") .build()); - insertModeMethod.addStatement("ctx.set($N, $N, $N, this.$N)", - schemaFieldName, - tableFieldName, - columnFieldName, - fieldName); + if (persistentValueMetadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { + insertCtxMethod.beginControlFlow("if (this.$N != null)", persistentValueMetadata.fieldName()); + } + insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N)", schemaFieldName, tableFieldName, columnFieldName, - fieldName); + persistentValueMetadata.fieldName()); + + if (persistentValueMetadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { + int i = 0; + for (Map.Entry link : foreignPersistentValueMetadata.links().entrySet()) { + String localColumn = link.getKey(); + String foreignColumn = link.getValue(); + + PersistentValueMetadata localColumnMetadata = metadataList.stream() + .filter(m -> m instanceof PersistentValueMetadata) + .map(m -> (PersistentValueMetadata) m) + .filter(m -> m.column().equals(localColumn) && m.table().equals(dataAnnotation.table()) && m.schema().equals(dataAnnotation.schema())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find local column metadata for link: " + localColumn)); + + String columnLinkFieldName = columnFieldName + "$" + localColumnMetadata.fieldName() + "$" + i; + + builderType.addField(FieldSpec.builder(String.class, columnLinkFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), foreignColumn) + .build()); + + + insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N)", + schemaFieldName, + tableFieldName, + columnLinkFieldName, + localColumnMetadata.fieldName()); + i++; + } + insertCtxMethod.endControlFlow(); + } + } } - insertModeMethod.addStatement("return ctx.insert(mode).get($T.class)", entityClass); + MethodSpec.Builder insertModeMethod = MethodSpec.methodBuilder("insert") + .addModifiers(Modifier.PUBLIC) + .returns(entityClass) + .addParameter(insertMode, "mode") + .addStatement("$T ctx = dataManager.createInsertContext()", insertContext) + .addStatement("this.insert(ctx)") + .addStatement("return ctx.insert(mode).get($T.class)", entityClass); + builderType.addMethod(insertModeMethod.build()); builderType.addMethod(insertCtxMethod.build()); diff --git a/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java b/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java new file mode 100644 index 00000000..feb57065 --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java @@ -0,0 +1,18 @@ +package net.staticstudios.data.processor; + +import com.palantir.javapoet.TypeName; + +import java.util.Map; + +public class ForeignPersistentValueMetadata extends PersistentValueMetadata { + private final Map links; + + public ForeignPersistentValueMetadata(String schema, String table, String column, String fieldName, TypeName genericType, Map links) { + super(schema, table, column, fieldName, genericType); + this.links = links; + } + + public Map links() { + return links; + } +} diff --git a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java index 7046cecb..a34108f5 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java +++ b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java @@ -5,6 +5,7 @@ import net.staticstudios.data.Data; import net.staticstudios.data.ForeignColumn; import net.staticstudios.data.IdColumn; +import net.staticstudios.data.utils.StringUtils; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; @@ -13,9 +14,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; public class MetadataUtils { private static final String FQN_OBJECT = Object.class.getName(); @@ -83,13 +82,34 @@ private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnot columnName = foreignColumn.name(); } - return new PersistentValueMetadata( - schemaName, - tableName, - columnName, - field.getSimpleName().toString(), - typeName - ); + if (idColumn != null || column != null) { + return new PersistentValueMetadata( + schemaName, + tableName, + columnName, + field.getSimpleName().toString(), + typeName + ); + } + if (foreignColumn != null) { + Map links = new HashMap<>(); + for (String link : StringUtils.parseCommaSeperatedList(foreignColumn.link())) { + String[] parts = link.split("="); + if (parts.length != 2) { + throw new IllegalArgumentException("Invalid link format in @ForeignColumn: " + link); + } + links.put(parts[0].trim(), parts[1].trim()); + } + return new ForeignPersistentValueMetadata( + schemaName, + tableName, + columnName, + field.getSimpleName().toString(), + typeName, + links + ); + } + throw new IllegalStateException("Field " + field.getSimpleName() + " is not annotated with @IdColumn, @Column, or @ForeignColumn"); } } diff --git a/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java b/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java index 8ca98404..277e280e 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java +++ b/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java @@ -2,6 +2,39 @@ import com.palantir.javapoet.TypeName; -public record PersistentValueMetadata(String schema, String table, String column, String fieldName, - TypeName genericType) implements Metadata { +public class PersistentValueMetadata implements Metadata { + private final String schema; + private final String table; + private final String column; + private final String fieldName; + private final TypeName genericType; + + public PersistentValueMetadata(String schema, String table, String column, String fieldName, + TypeName genericType) { + this.schema = schema; + this.table = table; + this.column = column; + this.fieldName = fieldName; + this.genericType = genericType; + } + + public String schema() { + return schema; + } + + public String table() { + return table; + } + + public String column() { + return column; + } + + public String fieldName() { + return fieldName; + } + + public TypeName genericType() { + return genericType; + } } diff --git a/src/main/java/net/staticstudios/data/DataAccessor.java b/src/main/java/net/staticstudios/data/DataAccessor.java index b438a136..f14e4f25 100644 --- a/src/main/java/net/staticstudios/data/DataAccessor.java +++ b/src/main/java/net/staticstudios/data/DataAccessor.java @@ -1,6 +1,7 @@ package net.staticstudios.data; -import net.staticstudios.data.insert.InsertContext; +import net.staticstudios.data.parse.DDLStatement; +import net.staticstudios.data.util.SQlStatement; import org.intellij.lang.annotations.Language; import java.sql.ResultSet; @@ -16,9 +17,9 @@ public interface DataAccessor { void executeUpdate(@Language("SQL") String sql, List values) throws SQLException; - void insert(InsertContext insertContext, InsertMode insertMode) throws SQLException; + void insert(List sqlStatements, InsertMode insertMode) throws SQLException; - void runDDL(@Language("SQL") String sql) throws SQLException; + void runDDL(DDLStatement ddl) throws SQLException; void postDDL() throws SQLException; } diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 27750051..8909011d 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -7,8 +7,7 @@ import net.staticstudios.data.impl.h2.H2DataAccessor; import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.insert.InsertContext; -import net.staticstudios.data.parse.SQLBuilder; -import net.staticstudios.data.parse.UniqueDataMetadata; +import net.staticstudios.data.parse.*; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; import org.jetbrains.annotations.ApiStatus; @@ -86,7 +85,7 @@ public InsertContext createInsertContext() { return new InsertContext(this); } - public void addUpdateHandler(String schema, String table, String column, ValueUpdateHandlerWrapper handler) { + public void addUpdateHandler(String schema, String table, String column, ValueUpdateHandlerWrapper handler) {//todo: allow us to specify what data type to convert the data to. this is useful when this method is called externally String key = schema + "." + table + "." + column; updateHandlers.computeIfAbsent(key, k -> new ConcurrentHashMap<>()) .computeIfAbsent(handler.getHolderClass(), k -> new CopyOnWriteArrayList<>()) @@ -104,7 +103,7 @@ public void callUpdateHandlers(List columnNames, String schema, String t } int columnIndex = columnNames.indexOf(column); - Preconditions.checkArgument(columnIndex != -1, "Column %s not found in provided column names %s", column, columnNames); + Preconditions.checkArgument(columnIndex != -1, "Column %s not found in provided name names %s", column, columnNames); for (Map.Entry, List>> entry : handlersForColumn.entrySet()) { Class holderClass = entry.getKey(); @@ -157,14 +156,14 @@ public final void load(Class... classes) { for (Class clazz : classes) { extractMetadata(clazz); } - List defs = new ArrayList<>(); + List defs = new ArrayList<>(); for (Class clazz : classes) { defs.addAll(sqlBuilder.parse(clazz)); } - for (String def : defs) { + for (DDLStatement ddl : defs) { try { - dataAccessor.runDDL(def); + dataAccessor.runDDL(ddl); } catch (SQLException e) { throw new RuntimeException(e); } @@ -192,7 +191,15 @@ public void extractMetadata(Class clazz) { if (idColumnAnnotation == null) { continue; } - idColumns.add(new ColumnMetadata(ValueUtils.parseValue(idColumnAnnotation.name()), ReflectionUtils.getGenericType(field), false, false, ValueUtils.parseValue(dataAnnotation.table()), ValueUtils.parseValue(dataAnnotation.schema()))); + idColumns.add(new ColumnMetadata( + ValueUtils.parseValue(dataAnnotation.schema()), + ValueUtils.parseValue(dataAnnotation.table()), + ValueUtils.parseValue(idColumnAnnotation.name()), + ReflectionUtils.getGenericType(field), + false, + false, + "" + )); } Preconditions.checkArgument(!idColumns.isEmpty(), "UniqueData class %s must have at least one @IdColumn annotated PersistentValue field", clazz.getName()); UniqueDataMetadata metadata = new UniqueDataMetadata(clazz, ValueUtils.parseValue(dataAnnotation.schema()), ValueUtils.parseValue(dataAnnotation.table()), idColumns); @@ -233,7 +240,7 @@ public T get(Class clazz, ColumnValuePair... idColumnV } for (ColumnValuePair providedIdColumn : idColumns) { - Preconditions.checkNotNull(providedIdColumn.value(), "ID column value for column %s in UniqueData class %s cannot be null", providedIdColumn.column(), clazz.getName()); + Preconditions.checkNotNull(providedIdColumn.value(), "ID name value for name %s in UniqueData class %s cannot be null", providedIdColumn.column(), clazz.getName()); } Preconditions.checkArgument(hasAllIdColumns, "Not all @IdColumn columns were provided for UniqueData class %s. Required: %s, Provided: %s", clazz.getName(), metadata.idColumns(), idColumns); @@ -302,16 +309,133 @@ public void submitAsyncTask(ConnectionJedisConsumer task) { } public void insert(InsertContext insertContext, InsertMode insertMode) { - //todo: process default values for any schemas involved. - // note: defaults should be applied by us, but db defaults may be applied for primative types if not null is specified. for example an Integer will by default be 0 in the db, if not nullable. + //todo: when inserting validate all id and required values are present - this will be enforced by h2, but we should do it here for better logging/errors. + Set tables = new HashSet<>(); + insertContext.getEntries().forEach((simpleColumnMetadata, o) -> { + SQLTable table = Objects.requireNonNull(sqlBuilder.getSchema(simpleColumnMetadata.schema())).getTable(simpleColumnMetadata.table()); + tables.add(table); + }); + + // handle foreign keys. for each foreign key, if we have a value for the local column, we need to set the value for the foreign column. this consists of linking ids mostly. + for (SQLTable table : tables) { + for (ForeignKey fKey : table.getForeignKeys()) { + SQLSchema otherSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getSchema())); + SQLTable otherTable = Objects.requireNonNull(otherSchema.getTable(fKey.getTable())); + for (Map.Entry link : fKey.getLinkingColumns().entrySet()) { + String myColumnName = link.getKey(); + String otherColumnName = link.getValue(); + SQLColumn otherColumn = Objects.requireNonNull(otherTable.getColumn(otherColumnName)); + insertContext.set(fKey.getSchema(), fKey.getTable(), otherColumn.getName(), insertContext.getEntries().get(new SimpleColumnMetadata(table.getSchema().getName(), table.getName(), myColumnName, otherColumn.getType()))); + } + } + } + + tables.clear(); // rebuild the table set in case we added any new tables from foreign keys + insertContext.getEntries().forEach((simpleColumnMetadata, o) -> { + SQLTable table = Objects.requireNonNull(sqlBuilder.getSchema(simpleColumnMetadata.schema())).getTable(simpleColumnMetadata.table()); + tables.add(table); + }); + + //sort tables based on foreign key dependencies. tables who are depended on should come first + + // Build dependency graph: table -> set of tables it depends on + Map> dependencyGraph = new HashMap<>(); + for (SQLTable table : tables) { + Set dependsOn = new HashSet<>(); + for (ForeignKey fKey : table.getForeignKeys()) { + SQLSchema otherSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getSchema())); + SQLTable otherTable = Objects.requireNonNull(otherSchema.getTable(fKey.getTable())); + dependsOn.add(otherTable); + } + dependencyGraph.put(table, dependsOn); + } + + // DFS to detect cycles + Set visited = new HashSet<>(); + Set stack = new HashSet<>(); + for (SQLTable table : dependencyGraph.keySet()) { + if (hasCycle(table, dependencyGraph, visited, stack)) { + throw new IllegalStateException(String.format("Cycle detected in foreign key dependencies involving table %s.%s", table.getSchema().getName(), table.getName())); + } + } + + // Topological sort for insert order + List orderedTables = new ArrayList<>(); + visited.clear(); + for (SQLTable table : dependencyGraph.keySet()) { + topoSort(table, dependencyGraph, visited, orderedTables); + } + + List sqlStatements = new ArrayList<>(); + + Map> columnsToInsert = new HashMap<>(); + for (Map.Entry entry : insertContext.getEntries().entrySet()) { + SimpleColumnMetadata column = entry.getKey(); + columnsToInsert.computeIfAbsent(column.table(), k -> new ArrayList<>()) + .add(column); + } + + for (SQLTable table : orderedTables) { + String schemaName = table.getSchema().getName(); + String tableName = table.getName(); + List columnsInTable = columnsToInsert.get(tableName); - //todo: when inserting validate all id column values are present + + StringBuilder sqlBuilder = new StringBuilder("INSERT INTO \""); + sqlBuilder.append(schemaName).append("\".\"").append(tableName).append("\" ("); + for (SimpleColumnMetadata column : columnsInTable) { + sqlBuilder.append("\"").append(column.name()).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") VALUES ("); + sqlBuilder.append("?, ".repeat(columnsInTable.size())); + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(")"); + + String sql = sqlBuilder.toString(); + List values = new ArrayList<>(); + for (SimpleColumnMetadata column : columnsInTable) { + Object deserializedValue = insertContext.getEntries().get(column); + Object serializedValue = deserializedValue; //todo: serialization + values.add(serializedValue); + } + sqlStatements.add(new SQlStatement(sql, values)); + } try { insertContext.markInserted(); - dataAccessor.insert(insertContext, insertMode); + dataAccessor.insert(sqlStatements, insertMode); } catch (SQLException e) { throw new RuntimeException(e); } } + + private boolean hasCycle(SQLTable table, Map> dependencyGraph, Set visited, Set stack) { + if (stack.contains(table)) { + return true; + } + if (visited.contains(table)) { + return false; + } + visited.add(table); + stack.add(table); + for (SQLTable dep : dependencyGraph.get(table)) { + if (hasCycle(dep, dependencyGraph, visited, stack)) { + return true; + } + } + stack.remove(table); + return false; + } + + private void topoSort(SQLTable table, Map> dependencyGraph, Set visited, List ordered) { + if (visited.contains(table)) { + return; + } + visited.add(table); + for (SQLTable dep : dependencyGraph.get(table)) { + topoSort(dep, dependencyGraph, visited, ordered); + } + ordered.add(table); + } } diff --git a/src/main/java/net/staticstudios/data/PersistentValue.java b/src/main/java/net/staticstudios/data/PersistentValue.java index afefc6d3..9e622a70 100644 --- a/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/src/main/java/net/staticstudios/data/PersistentValue.java @@ -1,7 +1,6 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; -import com.google.common.base.Supplier; import net.staticstudios.data.util.*; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -11,8 +10,6 @@ import java.util.List; import java.util.Map; -//todo: keep this as an interface, since we'll allow the data accessor decide what to use. for example are we writing to the DB or the cache. - /** * A persistent value represents a single cell in a database table. * @@ -36,10 +33,6 @@ static PersistentValue of(UniqueData holder, Class dataType) { PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler); - PersistentValue withDefault(@Nullable T defaultValue); - - PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier); - // PersistentValue updateInterval(long intervalMillis); @@ -47,12 +40,6 @@ class ProxyPersistentValue implements PersistentValue { protected final UniqueData holder; protected final Class dataType; private final List> updateHandlers = new ArrayList<>(); - //todo: here's how update handlers should be stored - // we store them globally on the data manager. we have a map of > - // additionally, for every field of type PV, we store the update handlers once - after the first unique data object is created. we will set them right before setting the delegate. - // this also means that after weve set the delegate, we cannot add any more update handlers. - // i think it would be useful to expose a method on the DM publicly to add an update handler to a specific column - private @Nullable Supplier<@Nullable T> defaultValueSupplier; private @Nullable PersistentValue delegate; private Map idColumnLinks = Collections.emptyMap(); private long updateIntervalMillis = -1; @@ -66,14 +53,6 @@ public void setDelegate(ColumnMetadata columnMetadata, PersistentValue delega Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); this.delegate = delegate; -// for (ValueUpdateHandler handler : updateHandlers) { -// this.delegate.onUpdate(handler); -// } -// this.updateHandlers.clear(); - if (this.defaultValueSupplier != null) { - this.delegate.withDefault(this.defaultValueSupplier); - } - PersistentValueMetadata metadata = new PersistentValueMetadata( holder.getClass(), columnMetadata.schema(), @@ -122,16 +101,6 @@ public PersistentValue onUpdate(Class holderClass, return this; } - @Override - public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier) { - if (delegate != null) { - delegate.withDefault(defaultValueSupplier); - } else { - this.defaultValueSupplier = defaultValueSupplier; - } - return this; - } - // @Override // public PersistentValue updateInterval(long intervalMillis) { // if (delegate != null) { @@ -142,11 +111,6 @@ public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultVal // return this; // } - @Override - public PersistentValue withDefault(@Nullable T defaultValue) { - return withDefault(() -> defaultValue); - } - @Override public T get() { if (delegate != null) { diff --git a/src/main/java/net/staticstudios/data/UniqueData.java b/src/main/java/net/staticstudios/data/UniqueData.java index 6bc1f1e9..977e283c 100644 --- a/src/main/java/net/staticstudios/data/UniqueData.java +++ b/src/main/java/net/staticstudios/data/UniqueData.java @@ -1,14 +1,14 @@ package net.staticstudios.data; -import net.staticstudios.data.parse.UniqueDataMetadata; import net.staticstudios.data.util.ColumnValuePairs; +import net.staticstudios.data.util.UniqueDataMetadata; import org.jetbrains.annotations.ApiStatus; public abstract class UniqueData { private ColumnValuePairs idColumns; private DataManager dataManager; - //todo: when an update is done to an id column, we need to handle it here. + //todo: when an update is done to an id name, we need to handle it here. //todo: when this row is deleted from the database, we should mark this with a deleted flag and throw an error if any operations are attempted on it. more specifically, any pvs referencing this object should throw an error if this has been deleted. @ApiStatus.Internal protected final void setDataManager(DataManager dataManager) { diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index eec9a699..7d7bf849 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -1,13 +1,7 @@ package net.staticstudios.data.impl.data; import com.google.common.base.Preconditions; -import com.google.common.base.Supplier; -import net.staticstudios.data.DataAccessor; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.Column; -import net.staticstudios.data.ForeignColumn; -import net.staticstudios.data.IdColumn; +import net.staticstudios.data.*; import net.staticstudios.data.util.*; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; @@ -25,7 +19,6 @@ public class PersistentValueImpl implements PersistentValue { private final String column; // private final Deque> updateHandlers = new ConcurrentLinkedDeque<>(); private Map idColumnLinks; - private @Nullable Supplier<@Nullable T> defaultValueSupplier; private PersistentValueImpl(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, Map idColumnLinks) { this.dataAccessor = dataAccessor; @@ -65,15 +58,35 @@ public static void delegate(String schema, String table, if (idColumn != null) { Preconditions.checkArgument(columnAnnotation == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @Column", pair.field().getName()); Preconditions.checkArgument(foreignColumn == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @ForeignColumn", pair.field().getName()); - columnMetadata = new ColumnMetadata(ValueUtils.parseValue(idColumn.name()), ReflectionUtils.getGenericType(pair.field()), false, false, table, schema); + columnMetadata = new ColumnMetadata( + schema, + table, + ValueUtils.parseValue(idColumn.name()), + ReflectionUtils.getGenericType(pair.field()), + false, + false, + "" + ); } else if (columnAnnotation != null) { - columnMetadata = new ColumnMetadata(ValueUtils.parseValue(columnAnnotation.name()), ReflectionUtils.getGenericType(pair.field()), columnAnnotation.nullable(), columnAnnotation.index(), + columnMetadata = new ColumnMetadata( + columnAnnotation.schema().isEmpty() ? schema : ValueUtils.parseValue(columnAnnotation.schema()), columnAnnotation.table().isEmpty() ? table : ValueUtils.parseValue(columnAnnotation.table()), - columnAnnotation.schema().isEmpty() ? schema : ValueUtils.parseValue(columnAnnotation.schema())); + ValueUtils.parseValue(columnAnnotation.name()), + ReflectionUtils.getGenericType(pair.field()), + columnAnnotation.nullable(), + columnAnnotation.index(), + columnAnnotation.defaultValue() + ); } else if (foreignColumn != null) { - columnMetadata = new ColumnMetadata(ValueUtils.parseValue(foreignColumn.name()), ReflectionUtils.getGenericType(pair.field()), foreignColumn.nullable(), foreignColumn.index(), + columnMetadata = new ColumnMetadata( + foreignColumn.schema().isEmpty() ? schema : ValueUtils.parseValue(foreignColumn.schema()), foreignColumn.table().isEmpty() ? table : ValueUtils.parseValue(foreignColumn.table()), - foreignColumn.schema().isEmpty() ? schema : ValueUtils.parseValue(foreignColumn.schema())); + ValueUtils.parseValue(foreignColumn.name()), + ReflectionUtils.getGenericType(pair.field()), + foreignColumn.nullable(), + foreignColumn.index(), + foreignColumn.defaultValue() + ); idColumnLinks = new HashMap<>(); List links = StringUtils.parseCommaSeperatedList(foreignColumn.link()); for (String link : links) { @@ -84,7 +97,7 @@ public static void delegate(String schema, String table, } Preconditions.checkNotNull(columnMetadata, "PersistentValue field %s is missing @Column annotation", pair.field().getName()); - //todo: the primary key gets a bit more complicated when we are dealing with a foreign key. this needs to be handled, and a new ForeignKey created which properly maps my id column to the foreign key column. + //todo: the primary key gets a bit more complicated when we are dealing with a foreign key. this needs to be handled, and a new ForeignKey created which properly maps my id name to the foreign key name. //todo: update: what??? if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { @@ -122,17 +135,6 @@ public PersistentValue onUpdate(Class holderClass, throw new UnsupportedOperationException("Dynamically adding update handlers is not supported"); } - @Override - public PersistentValue withDefault(@Nullable T defaultValue) { - return withDefault(() -> defaultValue); - } - - @Override - public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier) { - this.defaultValueSupplier = defaultValueSupplier; - return this; - } - @Override public Map getIdColumnLinks() { return idColumnLinks; @@ -156,9 +158,6 @@ public T get() { T deserialized = (T) serializedValue; //todo: this return deserialized; } - if (defaultValueSupplier != null) { - return defaultValueSupplier.get(); - } return null; } catch (SQLException e) { throw new RuntimeException(e); @@ -167,7 +166,7 @@ public T get() { @Override public void set(T value) { - //todo: whenever we set an id column of something, we need to tell the datamanager to update any tracked instance of uniquedata with that id. + //todo: whenever we set an id name of something, we need to tell the datamanager to update any tracked instance of uniquedata with that id. T oldValue = get(); StringBuilder sqlBuilder; if (idColumnLinks.isEmpty()) { diff --git a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java index e7b8270e..48babc21 100644 --- a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -2,10 +2,9 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.DataAccessor; +import net.staticstudios.data.OneToOne; import net.staticstudios.data.Reference; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.OneToOne; -import net.staticstudios.data.parse.UniqueDataMetadata; import net.staticstudios.data.util.*; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; @@ -132,7 +131,7 @@ public void set(T value) { break; } } - Preconditions.checkNotNull(theirValue, "Could not find value for column %s in referenced object of type %s".formatted(theirColumn, type.getName())); + Preconditions.checkNotNull(theirValue, "Could not find value for name %s in referenced object of type %s".formatted(theirColumn, type.getName())); sqlBuilder.append("\"").append(myColumn).append("\" = ?, "); values.add(theirValue); diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index cf7cbc0a..2ee80a23 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -5,9 +5,9 @@ import net.staticstudios.data.DataManager; import net.staticstudios.data.InsertMode; import net.staticstudios.data.impl.pg.PostgresListener; -import net.staticstudios.data.insert.InsertContext; -import net.staticstudios.data.util.ColumnMetadata; +import net.staticstudios.data.parse.DDLStatement; import net.staticstudios.data.util.SQlStatement; +import net.staticstudios.data.util.SchemaTable; import net.staticstudios.data.util.TaskQueue; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; @@ -29,6 +29,10 @@ */ public class H2DataAccessor implements DataAccessor { private static final Logger logger = LoggerFactory.getLogger(H2DataAccessor.class); + @Language("SQL") + private static final String SET_REFERENTIAL_INTEGRITY_FALSE = "SET REFERENTIAL_INTEGRITY FALSE"; + @Language("SQL") + private static final String SET_REFERENTIAL_INTEGRITY_TRUE = "SET REFERENTIAL_INTEGRITY TRUE"; private final TaskQueue taskQueue; private final String jdbcUrl; private final ThreadLocal threadConnection = new ThreadLocal<>(); @@ -67,53 +71,63 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener }); } - public synchronized void sync(String schema, String table) throws SQLException { + public synchronized void sync(List schemaTables) throws SQLException { //todo: when we resync we should clear the task queue and steal the connection //todo: if possible, id like to pause everything else until we are done syncing dataManager.submitBlockingTask(realDbConnection -> { - Path tmpFile = Paths.get(System.getProperty("java.io.tmpdir"), schema + "_" + table + ".csv"); - String absolutePath = tmpFile.toAbsolutePath().toString(); - - List columns = getColumnsInTable(schema, table); - - StringBuilder sqlBuilder = new StringBuilder("COPY (SELECT "); - for (String column : columns) { - sqlBuilder.append("\"").append(column).append("\", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(" FROM \"").append(schema).append("\".\"").append(table).append("\") TO STDOUT WITH CSV HEADER"); - @Language("SQL") String copySql = sqlBuilder.toString(); - - @Language("SQL") String truncateSql = "TRUNCATE TABLE \"" + schema + "\".\"" + table + "\""; - StringBuilder insertSqlBuilder = new StringBuilder("INSERT INTO \"").append(schema).append("\".\"").append(table).append("\" ("); - for (String column : columns) { - insertSqlBuilder.append("\"").append(column).append("\", "); - } - insertSqlBuilder.setLength(insertSqlBuilder.length() - 2); - insertSqlBuilder.append(") SELECT * FROM CSVREAD('").append(absolutePath).append("')"); - String insertSql = insertSqlBuilder.toString(); - PGConnection pgConnection = realDbConnection.unwrap(PGConnection.class); - FileOutputStream fileOutputStream; - try { - fileOutputStream = new FileOutputStream(absolutePath); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - logger.debug("[DB] {}", copySql); - pgConnection.copyTo(copySql, fileOutputStream); Connection h2Connection = getConnection(); boolean autoCommit = h2Connection.getAutoCommit(); try ( Statement h2Statement = h2Connection.createStatement() ) { h2Connection.setAutoCommit(false); - logger.trace("[H2] {}", truncateSql); - h2Statement.execute(truncateSql); - logger.trace("[H2] {}", insertSql); - h2Statement.execute(insertSql); + logger.trace("[H2] {}", SET_REFERENTIAL_INTEGRITY_FALSE); + h2Statement.execute(SET_REFERENTIAL_INTEGRITY_FALSE); + + for (SchemaTable schemaTable : schemaTables) { + String schema = schemaTable.schema(); + String table = schemaTable.table(); + Path tmpFile = Paths.get(System.getProperty("java.io.tmpdir"), UUID.randomUUID() + "_" + schema + "_" + table + ".csv"); + String absolutePath = tmpFile.toAbsolutePath().toString(); + + List columns = getColumnsInTable(schema, table); + + StringBuilder sqlBuilder = new StringBuilder("COPY (SELECT "); + for (String column : columns) { + sqlBuilder.append("\"").append(column).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(schema).append("\".\"").append(table).append("\") TO STDOUT WITH CSV HEADER"); + @Language("SQL") String copySql = sqlBuilder.toString(); + @Language("SQL") String truncateSql = "TRUNCATE TABLE \"" + schema + "\".\"" + table + "\""; + StringBuilder insertSqlBuilder = new StringBuilder("INSERT INTO \"").append(schema).append("\".\"").append(table).append("\" ("); + for (String column : columns) { + insertSqlBuilder.append("\"").append(column).append("\", "); + } + insertSqlBuilder.setLength(insertSqlBuilder.length() - 2); + insertSqlBuilder.append(") SELECT * FROM CSVREAD('").append(absolutePath).append("')"); + String insertSql = insertSqlBuilder.toString(); + PGConnection pgConnection = realDbConnection.unwrap(PGConnection.class); + FileOutputStream fileOutputStream; + try { + fileOutputStream = new FileOutputStream(absolutePath); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + logger.debug("[DB] {}", copySql); + pgConnection.copyTo(copySql, fileOutputStream); + logger.trace("[H2] {}", truncateSql); + h2Statement.execute(truncateSql); + logger.trace("[H2] {}", insertSql); + h2Statement.execute(insertSql); + } + logger.trace("[H2] {}", SET_REFERENTIAL_INTEGRITY_TRUE); + h2Statement.execute(SET_REFERENTIAL_INTEGRITY_TRUE); } finally { if (autoCommit) { h2Connection.setAutoCommit(true); + } else { + h2Connection.commit(); } } }); @@ -165,50 +179,20 @@ public PreparedStatement prepareStatement(@Language("SQL") String sql) throws SQ } @Override - public void insert(InsertContext insertContext, InsertMode insertMode) throws SQLException { + public void insert(List sqlStatements, InsertMode insertMode) throws SQLException { Connection connection = getConnection(); boolean autoCommit = connection.getAutoCommit(); try { connection.setAutoCommit(false); - List sqlStatements = new ArrayList<>(); - Map>> columnsByTable = new HashMap<>(); - for (Map.Entry entry : insertContext.getEntries().entrySet()) { - ColumnMetadata column = entry.getKey(); - columnsByTable.computeIfAbsent(column.schema(), k -> new HashMap<>()) - .computeIfAbsent(column.table(), k -> new ArrayList<>()) - .add(column); - } - - for (Map.Entry>> schemaEntry : columnsByTable.entrySet()) { - String schema = schemaEntry.getKey(); - for (Map.Entry> tableEntry : schemaEntry.getValue().entrySet()) { - String table = tableEntry.getKey(); - List columns = tableEntry.getValue(); - StringBuilder sqlBuilder = new StringBuilder("INSERT INTO \""); - sqlBuilder.append(schema).append("\".\"").append(table).append("\" ("); - for (ColumnMetadata column : columns) { - sqlBuilder.append("\"").append(column.name()).append("\", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(") VALUES ("); - sqlBuilder.append("?, ".repeat(columns.size())); - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(")"); - - String sql = sqlBuilder.toString(); - List values = new ArrayList<>(); - try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { - for (int i = 0; i < columns.size(); i++) { - ColumnMetadata column = columns.get(i); - Object value = insertContext.getEntries().get(column); - preparedStatement.setObject(i + 1, value); - values.add(value); - } - logger.debug("[H2] {}", sql); - sqlStatements.add(new SQlStatement(sql, values)); - preparedStatement.executeUpdate(); + for (SQlStatement sqlStatement : sqlStatements) { + try (PreparedStatement preparedStatement = connection.prepareStatement(sqlStatement.getSql())) { + int i = 1; + for (Object value : sqlStatement.getValues()) { + preparedStatement.setObject(i++, value); } + logger.debug("[H2] {}", sqlStatement.getSql()); + preparedStatement.executeUpdate(); } } @@ -229,6 +213,8 @@ public void insert(InsertContext insertContext, InsertMode insertMode) throws SQ } finally { if (realAutoCommit) { realConnection.setAutoCommit(true); + } else { + realConnection.commit(); } } }); @@ -238,11 +224,14 @@ public void insert(InsertContext insertContext, InsertMode insertMode) throws SQ future.join(); } catch (CompletionException e) { connection.rollback(); + logger.error("Error updating the real db", e.getCause()); } } } finally { if (autoCommit) { connection.setAutoCommit(true); + } else { + connection.commit(); } } } @@ -277,13 +266,13 @@ public void executeUpdate(@Language("SQL") String sql, List values) thro } @Override - public void runDDL(String sql) { + public void runDDL(DDLStatement ddl) { taskQueue.submitTask(connection -> { - logger.debug("[DB] {}", sql); - connection.createStatement().execute(sql); + logger.debug("[DB] {}", ddl.postgresqlStatement()); + connection.createStatement().execute(ddl.postgresqlStatement()); try (Statement statement = getConnection().createStatement()) { - logger.trace("[H2] {}", sql); - statement.execute(sql); + logger.trace("[H2] {}", ddl.h2Statement()); + statement.execute(ddl.h2Statement()); } }).join(); @@ -300,6 +289,7 @@ private synchronized void updateKnownTables() throws SQLException { try (Statement statement = connection.createStatement(); ResultSet rs = statement.executeQuery( "SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA NOT IN ('INFORMATION_SCHEMA', 'SYSTEM_LOBS') AND TABLE_TYPE='BASE TABLE'")) { + List toSync = new ArrayList<>(); while (rs.next()) { String schema = rs.getString("TABLE_SCHEMA"); String table = rs.getString("TABLE_NAME"); @@ -318,9 +308,10 @@ private synchronized void updateKnownTables() throws SQLException { } dataManager.submitBlockingTask(realDbConnection -> postgresListener.ensureTableHasTrigger(realDbConnection, schema, table)); - sync(schema, table); + toSync.add(new SchemaTable(schema, table)); } } + sync(toSync); } knownTables.clear(); knownTables.addAll(currentTables); diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java b/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java index ba107eed..ad66fd48 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java @@ -49,7 +49,7 @@ public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws } } } - logger.trace("Schema change detected (or first run). Old column names: {}, new column names: {}", columnNames, columns); + logger.trace("Schema change detected (or first run). Old name names: {}, new name names: {}", columnNames, columns); columnNames.clear(); columnNames.addAll(columns); } diff --git a/src/main/java/net/staticstudios/data/insert/InsertContext.java b/src/main/java/net/staticstudios/data/insert/InsertContext.java index 7c106064..2cd6f10f 100644 --- a/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -2,14 +2,14 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.DataManager; -import net.staticstudios.data.UniqueData; import net.staticstudios.data.InsertMode; +import net.staticstudios.data.UniqueData; import net.staticstudios.data.parse.SQLColumn; import net.staticstudios.data.parse.SQLSchema; import net.staticstudios.data.parse.SQLTable; -import net.staticstudios.data.parse.UniqueDataMetadata; -import net.staticstudios.data.util.ColumnMetadata; import net.staticstudios.data.util.ColumnValuePair; +import net.staticstudios.data.util.SimpleColumnMetadata; +import net.staticstudios.data.util.UniqueDataMetadata; import org.jetbrains.annotations.Nullable; import java.util.HashMap; @@ -20,7 +20,7 @@ public class InsertContext { //todo: insert strategy, on a per pv level. private final AtomicBoolean inserted = new AtomicBoolean(false); private final DataManager dataManager; - private final Map entries = new HashMap<>(); + private final Map entries = new HashMap<>(); public InsertContext(DataManager dataManager) { this.dataManager = dataManager; @@ -34,7 +34,6 @@ public InsertContext set(Class holderClass, String col } public InsertContext set(String schema, String table, String column, @Nullable Object value) { - Preconditions.checkState(!inserted.get(), "Cannot modify InsertContext after it has been inserted"); SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(schema); Preconditions.checkNotNull(sqlSchema, "Schema not found: " + schema); @@ -43,20 +42,24 @@ public InsertContext set(String schema, String table, String column, @Nullable O SQLColumn sqlColumn = sqlTable.getColumn(column); Preconditions.checkNotNull(sqlColumn, "Column not found: " + column + " in table: " + table + " schema: " + schema); - ColumnMetadata columnMetadata = new ColumnMetadata(column, sqlColumn.getType(), sqlColumn.isNullable(), sqlColumn.isIndexed(), table, schema); + SimpleColumnMetadata columnMetadata = new SimpleColumnMetadata( + schema, + table, + column, + sqlColumn.getType() + ); if (value == null) { entries.remove(columnMetadata); - return this; //todo: realistically we should validate the nullability stuff when we actually insert for better consistency. + return this; } - Preconditions.checkArgument(value != null || sqlColumn.isNullable(), "Column " + column + " in table " + table + " schema " + schema + " cannot be null"); - Preconditions.checkArgument(sqlColumn.getType().isInstance(value), "Value type mismatch for column " + column + " in table " + table + " schema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); + Preconditions.checkArgument(sqlColumn.getType().isInstance(value), "Value type mismatch for name " + column + " in table " + table + " schema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); entries.put(columnMetadata, value); return this; } - public Map getEntries() { + public Map getEntries() { return entries; } @@ -83,18 +86,16 @@ public T get(Class holderClass) { Preconditions.checkNotNull(sqlSchema, "Schema not found: " + metadata.schema()); SQLTable sqlTable = sqlSchema.getTable(metadata.table()); Preconditions.checkNotNull(sqlTable, "Table not found: " + metadata.table()); - boolean insertedAllIdColumns = true; - for (ColumnMetadata idColumn : metadata.idColumns()) { - if (!entries.containsKey(idColumn)) { - insertedAllIdColumns = false; - break; - } - } + boolean insertedAllIdColumns = metadata.idColumns().stream() + .allMatch(idColumn -> entries.keySet().stream() + .anyMatch(entry -> Objects.equals(entry.schema(), idColumn.schema()) && + Objects.equals(entry.table(), idColumn.table()) && + Objects.equals(entry.name(), idColumn.name()))); - Preconditions.checkState(insertedAllIdColumns, "The requested class was not inserted. Class: " + holderClass.getName() + " is missing one or more ID column values. Required ID columns: " + metadata.idColumns()); + Preconditions.checkState(insertedAllIdColumns, "The requested class was not inserted. Class: " + holderClass.getName() + " is missing one or more ID name values. Required ID columns: " + metadata.idColumns()); ColumnValuePair[] idColumnValues = new ColumnValuePair[metadata.idColumns().size()]; for (int i = 0; i < metadata.idColumns().size(); i++) { - idColumnValues[i] = new ColumnValuePair(metadata.idColumns().get(i).name(), entries.get(metadata.idColumns().get(i))); + idColumnValues[i] = new ColumnValuePair(metadata.idColumns().get(i).name(), entries.get(new SimpleColumnMetadata(metadata.idColumns().get(i)))); } return dataManager.get(holderClass, idColumnValues); } diff --git a/src/main/java/net/staticstudios/data/parse/DDLStatement.java b/src/main/java/net/staticstudios/data/parse/DDLStatement.java new file mode 100644 index 00000000..0c531282 --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/DDLStatement.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.parse; + +public record DDLStatement(String h2Statement, String postgresqlStatement) { + + public static DDLStatement both(String statement) { + return new DDLStatement(statement, statement); + } + + public static DDLStatement of(String h2Statement, String postgresqlStatement) { + return new DDLStatement(h2Statement, postgresqlStatement); + } +} diff --git a/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/src/main/java/net/staticstudios/data/parse/ForeignKey.java new file mode 100644 index 00000000..4efaa052 --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/ForeignKey.java @@ -0,0 +1,38 @@ +package net.staticstudios.data.parse; + +import java.util.HashMap; +import java.util.Map; + +public class ForeignKey { + // my column -> foreign schema.table.column + private final String column; + private final Map linkingColumns = new HashMap<>(); + private final String schema; + private final String table; + + public ForeignKey(String schema, String table, String column) { + this.schema = schema; + this.table = table; + this.column = column; + } + + public void addColumnMapping(String myColumn, String foreignColumn) { + linkingColumns.put(myColumn, foreignColumn); + } + + public Map getLinkingColumns() { + return linkingColumns; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getColumn() { + return column; + } +} diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index f6fba491..32e5185e 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -1,13 +1,7 @@ package net.staticstudios.data.parse; import com.google.common.base.Preconditions; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.Relation; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.Column; -import net.staticstudios.data.Data; -import net.staticstudios.data.ForeignColumn; -import net.staticstudios.data.IdColumn; +import net.staticstudios.data.*; import net.staticstudios.data.util.*; import org.jetbrains.annotations.Nullable; @@ -24,7 +18,7 @@ public SQLBuilder(DataManager dataManager) { this.parsedSchemas = new HashMap<>(); } - public List parse(Class clazz) { + public List parse(Class clazz) { Preconditions.checkNotNull(clazz, "Class cannot be null"); Set> visited = walk(clazz); @@ -64,10 +58,10 @@ public List parse(Class clazz) { return parsedSchemas.get(name); } - private List getDefs(Collection schemas) { //todo: add fkeys, indexes, uniques, nullables, defaults, etc - List statements = new ArrayList<>(); + private List getDefs(Collection schemas) { //todo: add indexes, uniques, + List statements = new ArrayList<>(); for (SQLSchema schema : schemas) { - statements.add("CREATE SCHEMA IF NOT EXISTS \"" + schema.getName() + "\";"); + statements.add(DDLStatement.both("CREATE SCHEMA IF NOT EXISTS \"" + schema.getName() + "\";")); StringBuilder sb; for (SQLTable table : schema.getTables()) { // if (metadata.table().equals(table.getName()) && metadata.schema().equals(schema.getName())) { @@ -83,22 +77,78 @@ private List getDefs(Collection schemas) { //todo: add fkeys, sb.setLength(sb.length() - 2); sb.append(")\n"); sb.append(");"); - statements.add(sb.toString()); + statements.add(DDLStatement.both(sb.toString())); // } if (!table.getColumns().isEmpty()) { for (SQLColumn column : table.getColumns()) { sb = new StringBuilder(); sb.append("ALTER TABLE \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ").append("ADD COLUMN IF NOT EXISTS ").append("\"").append(column.getName()).append("\" ").append(SQLUtils.getSqlType(column.getType())); -// if (!column.isNullable()) { -// sb.append(" NOT NULL"); //todo: not valid in h2 -// } + if (!column.isNullable()) { + sb.append(" NOT NULL"); + } + if (column.getDefaultValue() != null) { + sb.append(" DEFAULT ").append(column.getDefaultValue()); + } + if (column.isIndexed()) { // sb.append(" INDEXED"); //todo: this is not valid sql, need to create index separately } + sb.append(";"); - statements.add(sb.toString()); + statements.add(DDLStatement.both(sb.toString())); + } + } + } + } + + // define fkeys after table creation, to ensure all tables exist before adding fkeys + for (SQLSchema schema : schemas) { + for (SQLTable table : schema.getTables()) { + for (ForeignKey foreignKey : table.getForeignKeys()) { + if (foreignKey == null) { + continue; + } + String fKeyName = "fk_" + table.getName() + "_" + String.join("_", foreignKey.getLinkingColumns().keySet()) + "_to_" + foreignKey.getSchema() + "_" + foreignKey.getTable() + "_" + String.join("_", foreignKey.getLinkingColumns().values()); + StringBuilder sb = new StringBuilder(); + sb.append("ALTER TABLE \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" "); + sb.append("ADD CONSTRAINT IF NOT EXISTS ").append(fKeyName).append(" "); + sb.append("FOREIGN KEY ("); + for (String localCol : foreignKey.getLinkingColumns().keySet()) { + sb.append("\"").append(localCol).append("\", "); + } + sb.setLength(sb.length() - 2); + sb.append(") "); + sb.append("REFERENCES \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" ("); + for (String foreignCol : foreignKey.getLinkingColumns().values()) { + sb.append("\"").append(foreignCol).append("\", "); + } + sb.setLength(sb.length() - 2); + sb.append(") ON DELETE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type + String h2 = sb.toString(); + + + sb = new StringBuilder(); + sb.append("DO $$ BEGIN "); + sb.append("IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = '").append(fKeyName).append("' AND table_name = '").append(table.getName()).append("' AND constraint_schema = '").append(schema.getName()).append("' AND constraint_type = 'FOREIGN KEY') THEN "); + + sb.append("ALTER TABLE \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" "); + sb.append("ADD CONSTRAINT ").append(fKeyName).append(" "); + sb.append("FOREIGN KEY ("); + for (String localCol : foreignKey.getLinkingColumns().keySet()) { + sb.append("\"").append(localCol).append("\", "); + } + sb.setLength(sb.length() - 2); + sb.append(") "); + sb.append("REFERENCES \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" ("); + for (String foreignCol : foreignKey.getLinkingColumns().values()) { + sb.append("\"").append(foreignCol).append("\", "); } + sb.setLength(sb.length() - 2); + sb.append(") ON DELETE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type + sb.append(" END IF; END $$;"); + String pg = sb.toString(); + statements.add(DDLStatement.of(h2, pg)); } } } @@ -143,27 +193,51 @@ private void parseIndividual(Class clazz, Map clazz, Map type = ReflectionUtils.getGenericType(field); //todo: handle custom types to sql types - SQLColumn sqlColumn = new SQLColumn(table, type, columnName, columnMetadata.nullable(), columnMetadata.indexed()); + SQLColumn sqlColumn = new SQLColumn(table, type, columnName, nullable, indexed, defaultValue.isEmpty() ? null : SQLUtils.parseDefaultValue(type, defaultValue)); SQLColumn existingColumn = table.getColumn(columnName); if (existingColumn != null) { diff --git a/src/main/java/net/staticstudios/data/parse/SQLColumn.java b/src/main/java/net/staticstudios/data/parse/SQLColumn.java index 8f7b7fd2..cc7ca1c7 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLColumn.java +++ b/src/main/java/net/staticstudios/data/parse/SQLColumn.java @@ -1,5 +1,7 @@ package net.staticstudios.data.parse; +import org.jetbrains.annotations.Nullable; + import java.util.Objects; public class SQLColumn { @@ -8,13 +10,15 @@ public class SQLColumn { private final String name; private final boolean nullable; private final boolean indexed; + private final @Nullable String defaultValue; - public SQLColumn(SQLTable table, Class type, String name, boolean nullable, boolean indexed) { + public SQLColumn(SQLTable table, Class type, String name, boolean nullable, boolean indexed, @Nullable String defaultValue) { this.table = table; this.type = type; this.name = name; this.nullable = nullable; this.indexed = indexed; + this.defaultValue = defaultValue; } public SQLTable getTable() { @@ -37,10 +41,13 @@ public boolean isIndexed() { return indexed; } + public @Nullable String getDefaultValue() { + return defaultValue; + } @Override public int hashCode() { - return Objects.hash(table, type, name, nullable, indexed); + return Objects.hash(table, type, name, nullable, indexed, defaultValue); } @Override @@ -50,6 +57,7 @@ public boolean equals(Object obj) { SQLColumn other = (SQLColumn) obj; return nullable == other.nullable && indexed == other.indexed && + Objects.equals(defaultValue, other.defaultValue) && Objects.equals(type, other.type) && Objects.equals(name, other.name); } @@ -61,6 +69,7 @@ public String toString() { ", name='" + name + '\'' + ", nullable=" + nullable + ", indexed=" + indexed + + ", defaultValue='" + defaultValue + '\'' + '}'; } } diff --git a/src/main/java/net/staticstudios/data/parse/SQLTable.java b/src/main/java/net/staticstudios/data/parse/SQLTable.java index 9f006670..e4843935 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLTable.java +++ b/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -11,12 +11,14 @@ public class SQLTable { private final String name; private final List idColumns; private final Map columns; + private final List foreignKeys; public SQLTable(SQLSchema schema, String name, List idColumns) { this.schema = schema; this.name = name; this.idColumns = idColumns; this.columns = new HashMap<>(); + this.foreignKeys = new ArrayList<>(); } public SQLSchema getSchema() { @@ -35,6 +37,10 @@ public Set getColumns() { return columns.get(columnName); } + public List getForeignKeys() { + return foreignKeys; + } + public List getIdColumns() { return idColumns; } @@ -51,4 +57,16 @@ public void addColumn(SQLColumn column) { columns.put(column.getName(), column); } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof SQLTable other)) return false; + return Objects.equals(schema.getName(), other.schema.getName()) && Objects.equals(name, other.name); + } + + @Override + public int hashCode() { + return Objects.hash(schema.getName(), name); + } } diff --git a/src/main/java/net/staticstudios/data/util/AbstractBuilder.java b/src/main/java/net/staticstudios/data/util/AbstractBuilder.java index 1781e6e3..66bd51bf 100644 --- a/src/main/java/net/staticstudios/data/util/AbstractBuilder.java +++ b/src/main/java/net/staticstudios/data/util/AbstractBuilder.java @@ -7,8 +7,8 @@ public class AbstractBuilder { // private final Class holderClass; // private final Map toInsert; // -// protected void set(String schema, String table, String column, Object value) { -// String key = schema + "." + table + "." + column; +// protected void set(String schema, String table, String name, Object value) { +// String key = schema + "." + table + "." + name; // toInsert.put(key, value); // } } diff --git a/src/main/java/net/staticstudios/data/util/ColumnMetadata.java b/src/main/java/net/staticstudios/data/util/ColumnMetadata.java index 8b95dd50..15f11dd0 100644 --- a/src/main/java/net/staticstudios/data/util/ColumnMetadata.java +++ b/src/main/java/net/staticstudios/data/util/ColumnMetadata.java @@ -1,5 +1,7 @@ package net.staticstudios.data.util; -public record ColumnMetadata(String name, Class type, boolean nullable, boolean indexed, String table, - String schema) { +import org.jetbrains.annotations.NotNull; + +public record ColumnMetadata(String schema, String table, String name, Class type, boolean nullable, boolean indexed, + @NotNull String encodedDefaultValue) { } diff --git a/src/main/java/net/staticstudios/data/util/ColumnValuePair.java b/src/main/java/net/staticstudios/data/util/ColumnValuePair.java index e7abda9e..6f2d16cf 100644 --- a/src/main/java/net/staticstudios/data/util/ColumnValuePair.java +++ b/src/main/java/net/staticstudios/data/util/ColumnValuePair.java @@ -40,7 +40,7 @@ public int hashCode() { @Override public String toString() { return "ColumnValuePair[" + - "column=" + column + ", " + + "name=" + column + ", " + "value=" + value + ']'; } diff --git a/src/main/java/net/staticstudios/data/util/SQLUtils.java b/src/main/java/net/staticstudios/data/util/SQLUtils.java index 039f82f3..4246fec4 100644 --- a/src/main/java/net/staticstudios/data/util/SQLUtils.java +++ b/src/main/java/net/staticstudios/data/util/SQLUtils.java @@ -25,4 +25,16 @@ public static String getSqlType(Class clazz) { } throw new IllegalArgumentException("Unsupported class type: " + clazz.getName()); } + + public static String parseDefaultValue(Class clazz, String defaultValue) { + try { + return switch (getSqlType(clazz)) { + case "TEXT" -> "'" + defaultValue.replace("'", "''") + "'"; + case "BOOLEAN" -> Boolean.parseBoolean(defaultValue) ? "TRUE" : "FALSE"; + default -> defaultValue; + }; + } catch (IllegalArgumentException e) { + return defaultValue; + } + } } diff --git a/src/main/java/net/staticstudios/data/util/SchemaTable.java b/src/main/java/net/staticstudios/data/util/SchemaTable.java new file mode 100644 index 00000000..7a667acf --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/SchemaTable.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.util; + +public record SchemaTable(String schema, String table) { +} diff --git a/src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java b/src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java new file mode 100644 index 00000000..89a3d736 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java @@ -0,0 +1,8 @@ +package net.staticstudios.data.util; + +public record SimpleColumnMetadata(String schema, String table, String name, Class type) { + + public SimpleColumnMetadata(ColumnMetadata metadata) { + this(metadata.schema(), metadata.table(), metadata.name(), metadata.type()); + } +} diff --git a/src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java b/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java similarity index 72% rename from src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java rename to src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java index 35d4b5e2..51c38c77 100644 --- a/src/main/java/net/staticstudios/data/parse/UniqueDataMetadata.java +++ b/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java @@ -1,7 +1,6 @@ -package net.staticstudios.data.parse; +package net.staticstudios.data.util; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.util.ColumnMetadata; import java.util.List; diff --git a/src/main/java/net/staticstudios/data/util/ValueUtils.java b/src/main/java/net/staticstudios/data/util/ValueUtils.java index 832c1905..93af28f3 100644 --- a/src/main/java/net/staticstudios/data/util/ValueUtils.java +++ b/src/main/java/net/staticstudios/data/util/ValueUtils.java @@ -16,14 +16,15 @@ public class ValueUtils { public static String parseValue(String encoded) { Preconditions.checkNotNull(encoded, "Encoded value cannot be null"); Matcher matcher = ENVIRONMENT_VARIABLE_PATTERN.matcher(encoded); - if (matcher.matches()) { + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { String varName = matcher.group(1); String value = ENVIRONMENT_VARIABLE_ACCESSOR.getEnv(varName); - Preconditions.checkArgument(value != null, "Environment variable " + varName + " is not set"); - return value; - } else { - return encoded; + Preconditions.checkArgument(value != null, String.format("Environment variable %s is not set", varName)); + matcher.appendReplacement(sb, Matcher.quoteReplacement(value)); } + matcher.appendTail(sb); + return sb.toString(); } public static List parseCommaSeperatedList(String encoded) { diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 5c05a453..7ae9a238 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -1,5 +1,6 @@ package net.staticstudios.data; +import net.staticstudios.data.impl.h2.H2DataAccessor; import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.mock.MockUser; @@ -8,7 +9,12 @@ import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Test; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -34,6 +40,23 @@ public void testReadData() throws SQLException { .insert(InsertMode.SYNC); } + + Connection h2Connection; + try { + Method getConnectionMethod = H2DataAccessor.class.getDeclaredMethod("getConnection"); + getConnectionMethod.setAccessible(true); + h2Connection = (Connection) getConnectionMethod.invoke(dataManager.getDataAccessor()); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + + try (Statement statement = h2Connection.createStatement()) { + ResultSet rs = statement.executeQuery("SCRIPT"); + while (rs.next()) { + System.out.println(rs.getString(1)); + } + } + for (UUID id : userIds) { MockUser user = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); assertEquals("user " + id, user.name.get()); @@ -60,6 +83,7 @@ public void test() throws SQLException { MockUser mockUser = MockUserFactory.builder(dataManager) .id(id) .name("test user") + .nameUpdates(0) .insert(InsertMode.SYNC); assertEquals("test user", mockUser.name.get()); mockUser.name.set("updated name"); @@ -113,6 +137,8 @@ public void testUpdateHandlerRegistration() { MockUser mockUser = MockUserFactory.builder(dataManager) .id(id) .name("test user") + .favoriteColor("orange") + .nameUpdates(0) .insert(InsertMode.SYNC); assertEquals("test user", mockUser.name.get()); //first instance was created, handler should be registered diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index 2097e9c0..3f934305 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -1,15 +1,95 @@ package net.staticstudios.data; import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.mock.MockUser; +import net.staticstudios.data.mock.MockPost; +import net.staticstudios.data.parse.DDLStatement; +import net.staticstudios.data.util.EnvironmentVariableAccessor; +import net.staticstudios.data.util.ValueUtils; +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Container; + +import java.sql.Connection; +import java.sql.Statement; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; public class SQLParseTest extends DataTest { + @BeforeAll + public static void setup() { + ValueUtils.ENVIRONMENT_VARIABLE_ACCESSOR = new EnvironmentVariableAccessor() { + @Override + public String getEnv(String name) { + return switch (name) { + case "POST_SCHEMA" -> "social_media"; + case "POST_TABLE" -> "posts"; + case "POST_ID_COLUMN" -> "post_id"; + default -> null; + }; + } + }; + } + @Test - public void testParse() { + public void testParse() throws Exception { DataManager dm = getMockEnvironments().getFirst().dataManager(); - dm.extractMetadata(MockUser.class); - dm.getSQLBuilder().parse(MockUser.class).forEach(System.out::println); + dm.extractMetadata(MockPost.class); + Connection postgresConnection = getConnection(); + List ddlStatements = dm.getSQLBuilder().parse(MockPost.class); + for (DDLStatement ddl : ddlStatements) { + System.out.println(ddl.postgresqlStatement()); + try (Statement statement = postgresConnection.createStatement()) { + statement.execute(ddl.postgresqlStatement()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + try (Statement statement = postgresConnection.createStatement()) { + statement.execute("DROP FUNCTION IF EXISTS public.propagate_data_update_v3"); + } + + Container.ExecResult result = postgres.execInContainer("pg_dump", + "--schema-only", + "--no-owner", + "--no-privileges", + "--no-comments", + "--section=pre-data", + "--section=post-data", + "-U", postgres.getUsername(), + postgres.getDatabaseName() + ); + String schemaDump = result.getStdout(); + StringBuilder cleanedDump = new StringBuilder(); + for (String line : schemaDump.split("\n")) { + if (line.startsWith("--") || line.startsWith("SET") || line.startsWith("SELECT") || line.trim().isEmpty()) { + continue; + } + cleanedDump.append(line).append("\n"); + } + + @Language("SQL") String expected = """ + CREATE SCHEMA social_media; + CREATE TABLE social_media.posts ( + post_id integer NOT NULL, + likes integer DEFAULT 0 NOT NULL, + text_content text NOT NULL + ); + CREATE TABLE social_media.posts_interactions ( + post_id integer NOT NULL, + interactions integer DEFAULT 0 NOT NULL + ); + ALTER TABLE ONLY social_media.posts_interactions + ADD CONSTRAINT posts_interactions_pkey PRIMARY KEY (post_id); + ALTER TABLE ONLY social_media.posts + ADD CONSTRAINT posts_pkey PRIMARY KEY (post_id); + ALTER TABLE ONLY social_media.posts + ADD CONSTRAINT fk_posts_post_id_to_social_media_posts_interactions_post_id FOREIGN KEY (post_id) REFERENCES social_media.posts_interactions(post_id) ON DELETE CASCADE; + """; + + assertEquals(expected.trim(), cleanedDump.toString().trim()); } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/misc/DataTest.java b/src/test/java/net/staticstudios/data/misc/DataTest.java index 711b8745..295e0fa4 100644 --- a/src/test/java/net/staticstudios/data/misc/DataTest.java +++ b/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -23,7 +23,11 @@ public class DataTest { public static RedisContainer redis; public static PostgreSQLContainer postgres = new PostgreSQLContainer<>( "postgres:16.2" - ); + ) + .withExposedPorts(5432) + .withPassword("password") + .withUsername("postgres") + .withDatabaseName("postgres"); public static DataSourceConfig dataSourceConfig; private static Connection connection; private static Jedis jedis; diff --git a/src/test/java/net/staticstudios/data/mock/MockPost.java b/src/test/java/net/staticstudios/data/mock/MockPost.java new file mode 100644 index 00000000..31a6f086 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/MockPost.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.mock; + +import net.staticstudios.data.*; + +/** + * Used to validate schema generation. + */ +@Data(schema = "${POST_SCHEMA}", table = "${POST_TABLE}") +public class MockPost extends UniqueData { + @IdColumn(name = "${POST_ID_COLUMN}") + public PersistentValue id; + + @Column(name = "text_content") + public PersistentValue textContent; + @Column(name = "likes", defaultValue = "0") + public PersistentValue likes; + @ForeignColumn(name = "interactions", table = "${POST_TABLE}_interactions", link = "${POST_ID_COLUMN}=post_id", defaultValue = "0") + public PersistentValue interactions; + + //todo: test relationships +} diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/MockUser.java index f68f5a2e..d7b5e645 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/MockUser.java @@ -10,8 +10,6 @@ @Data(schema = "public", table = "users") public class MockUser extends UniqueData { //todo: @OneToMany, @ManyToMany, @ManyToOne - - //todo: if nullable is false, find the sql type and set a reasonable default. @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); @Column(name = "settings_id", nullable = true) @@ -20,23 +18,20 @@ public class MockUser extends UniqueData { public PersistentValue age; @ForeignColumn(name = "fav_color", table = "user_preferences", nullable = true, link = "id=user_id") public PersistentValue favoriteColor; - @OneToOne(link = "settings_id=user_id") + @OneToOne(link = "settings_id=user_id") //todo: this should generate an fkey public Reference settings; - @ForeignColumn(name = "name_updates", table = "user_metadata", link = "id=user_id") + @ForeignColumn(name = "name_updates", table = "user_metadata", link = "id=user_id", defaultValue = "0") public PersistentValue nameUpdates; - @Column(name = "name", index = true) + @Column(name = "name", index = true, defaultValue = "Unknown") public PersistentValue name = PersistentValue.of(this, String.class) .onUpdate(MockUser.class, (user, update) -> { user.nameUpdates.set(user.getNameUpdates() + 1); - }) - .withDefault("Unknown"); + }); //todo: add support for unique constraints and test them. - //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. public int getNameUpdates() { - //todo: nameupdates shouldnt be null but until we get default values implemented we have to do this - return nameUpdates.get() == null ? 0 : nameUpdates.get(); + return nameUpdates.get(); } } diff --git a/src/test/java/net/staticstudios/data/mock/MockUserSettings.java b/src/test/java/net/staticstudios/data/mock/MockUserSettings.java index ff9feb11..b820da46 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUserSettings.java +++ b/src/test/java/net/staticstudios/data/mock/MockUserSettings.java @@ -1,12 +1,6 @@ package net.staticstudios.data.mock; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.Column; -import net.staticstudios.data.Data; -import net.staticstudios.data.IdColumn; -import net.staticstudios.data.InsertMode; +import net.staticstudios.data.*; import java.util.UUID; @@ -14,13 +8,12 @@ public class MockUserSettings extends UniqueData { @IdColumn(name = "user_id") public PersistentValue id; - @Column(name = "font_size") + @Column(name = "font_size", defaultValue = "10") public PersistentValue fontSide; public static MockUserSettings create(DataManager dataManager, UUID id) { return dataManager.createInsertContext() .set(MockUserSettings.class, "user_id", id) - //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. .insert(InsertMode.SYNC) .get(MockUserSettings.class); } From f3916b3e2a61e7b3e844161e2a6370e76a8c8c25 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 16 Sep 2025 11:15:22 -0400 Subject: [PATCH 07/75] handle pg notifications --- .../data/processor/DataProcessor.java | 1 + .../net/staticstudios/data/DataManager.java | 35 +++- .../net/staticstudios/data/UniqueData.java | 39 ++++- .../data/impl/data/PersistentValueImpl.java | 5 +- .../data/impl/h2/H2DataAccessor.java | 133 +++++++++++++++- .../staticstudios/data/impl/h2/H2Trigger.java | 5 +- .../data/impl/pg/PostgresListener.java | 1 + .../data/primative/Primitive.java | 47 ++++++ .../data/primative/PrimitiveBuilder.java | 60 +++++++ .../data/primative/Primitives.java | 150 ++++++++++++++++++ .../data/util/ColumnValuePairs.java | 4 + .../data/util/PostgresUtils.java | 22 +++ .../data/PersistentValueTest.java | 110 +++++++++++-- .../net/staticstudios/data/misc/DataTest.java | 14 ++ .../net/staticstudios/data/mock/MockUser.java | 2 + 15 files changed, 598 insertions(+), 30 deletions(-) create mode 100644 src/main/java/net/staticstudios/data/primative/Primitive.java create mode 100644 src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java create mode 100644 src/main/java/net/staticstudios/data/primative/Primitives.java create mode 100644 src/main/java/net/staticstudios/data/util/PostgresUtils.java diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index fd44ced6..c78fd7dd 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -41,6 +41,7 @@ public boolean process(Set annotations, RoundEnvironment private void generateFactory(TypeElement entityType) throws IOException { if (entityType.getModifiers().contains(Modifier.ABSTRACT)) { + //todo: if abstract, we can ignore the factory, but we still need to generate a queryBuilder or something return; } diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 8909011d..8ac057d5 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -219,6 +219,34 @@ public UniqueDataMetadata getMetadata(Class clazz) { return metadata; } + @ApiStatus.Internal + public void delete(List columnNames, String schema, String table, Object[] values) { + uniqueDataMetadataMap.values().forEach(uniqueDataMetadata -> { + if (!uniqueDataMetadata.schema().equals(schema) || !uniqueDataMetadata.table().equals(table)) { + return; + } + + ColumnValuePair[] idColumns = new ColumnValuePair[uniqueDataMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : uniqueDataMetadata.idColumns()) { + boolean found = false; + for (int i = 0; i < columnNames.size(); i++) { + if (idColumn.name().equals(columnNames.get(i))) { + idColumns[uniqueDataMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), values[i]); + found = true; + break; + } + } + Preconditions.checkArgument(found, "Not all ID columns were provided for UniqueData class %s. Required: %s, Provided: %s", uniqueDataMetadata.clazz().getName(), uniqueDataMetadata.idColumns(), Arrays.toString(values)); + } + + UniqueData instance = uniqueDataInstanceCache.getOrDefault(uniqueDataMetadata.clazz(), Collections.emptyMap()).get(new ColumnValuePairs(idColumns)); + if (instance == null) { + return; + } + instance.markDeleted(); + }); + } + @SuppressWarnings("unchecked") public T get(Class clazz, ColumnValuePair... idColumnValues) { ColumnValuePairs idColumns = new ColumnValuePairs(idColumnValues); @@ -245,12 +273,15 @@ public T get(Class clazz, ColumnValuePair... idColumnV Preconditions.checkArgument(hasAllIdColumns, "Not all @IdColumn columns were provided for UniqueData class %s. Required: %s, Provided: %s", clazz.getName(), metadata.idColumns(), idColumns); + T instance; if (uniqueDataInstanceCache.containsKey(clazz) && uniqueDataInstanceCache.get(clazz).containsKey(idColumns)) { logger.trace("Cache hit for UniqueData class {} with ID columns {}", clazz.getName(), idColumns); - return (T) uniqueDataInstanceCache.get(clazz).get(idColumns); + instance = (T) uniqueDataInstanceCache.get(clazz).get(idColumns); + if (instance.isDeleted()) { + return null; + } } - T instance; try { Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); diff --git a/src/main/java/net/staticstudios/data/UniqueData.java b/src/main/java/net/staticstudios/data/UniqueData.java index 977e283c..b98682a5 100644 --- a/src/main/java/net/staticstudios/data/UniqueData.java +++ b/src/main/java/net/staticstudios/data/UniqueData.java @@ -1,5 +1,6 @@ package net.staticstudios.data; +import net.staticstudios.data.util.ColumnValuePair; import net.staticstudios.data.util.ColumnValuePairs; import net.staticstudios.data.util.UniqueDataMetadata; import org.jetbrains.annotations.ApiStatus; @@ -7,8 +8,9 @@ public abstract class UniqueData { private ColumnValuePairs idColumns; private DataManager dataManager; + private volatile boolean isDeleted = false; - //todo: when an update is done to an id name, we need to handle it here. + //todo: when an update is done to an id column, we need to handle it here. //todo: when this row is deleted from the database, we should mark this with a deleted flag and throw an error if any operations are attempted on it. more specifically, any pvs referencing this object should throw an error if this has been deleted. @ApiStatus.Internal protected final void setDataManager(DataManager dataManager) { @@ -20,6 +22,14 @@ protected final synchronized void setIdColumns(ColumnValuePairs idColumns) { this.idColumns = idColumns; } + protected final synchronized void markDeleted() { + this.isDeleted = true; + } + + public final synchronized boolean isDeleted() { + return isDeleted; + } + public DataManager getDataManager() { return dataManager; } @@ -32,5 +42,30 @@ public final UniqueDataMetadata getMetadata() { return dataManager.getMetadata(this.getClass()); } - //todo: toString, equals, hashcode - all based on the id columns and class type + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.getClass().getSimpleName()).append("{"); + for (ColumnValuePair idColumn : idColumns) { + sb.append(idColumn.column()).append("=").append(idColumn.value()).append(", "); + } + if (!idColumns.isEmpty()) { + sb.setLength(sb.length() - 2); + } + sb.append("}"); + return sb.toString(); + } + + @Override + public final int hashCode() { + return idColumns.hashCode(); + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof UniqueData other)) return false; + if (!this.getClass().equals(other.getClass())) return false; + return this.dataManager.equals(other.dataManager) && this.idColumns.equals(other.idColumns); + } } diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index 7d7bf849..b292d5f8 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -17,8 +17,7 @@ public class PersistentValueImpl implements PersistentValue { private final String schema; private final String table; private final String column; - // private final Deque> updateHandlers = new ConcurrentLinkedDeque<>(); - private Map idColumnLinks; + private final Map idColumnLinks; private PersistentValueImpl(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, Map idColumnLinks) { this.dataAccessor = dataAccessor; @@ -142,6 +141,7 @@ public Map getIdColumnLinks() { @Override public T get() { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot get value from a deleted UniqueData instance"); StringBuilder sqlBuilder = new StringBuilder().append("SELECT \"").append(column).append("\" FROM \"").append(schema).append("\".\"").append(table).append("\" WHERE "); for (ColumnValuePair columnValuePair : holder.getIdColumns()) { String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); @@ -166,6 +166,7 @@ public T get() { @Override public void set(T value) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set value on a deleted UniqueData instance"); //todo: whenever we set an id name of something, we need to tell the datamanager to update any tracked instance of uniquedata with that id. T oldValue = get(); StringBuilder sqlBuilder; diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index 2ee80a23..2cde1100 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -1,14 +1,21 @@ package net.staticstudios.data.impl.h2; +import com.google.common.base.Preconditions; import com.impossibl.postgres.api.jdbc.PGConnection; import net.staticstudios.data.DataAccessor; import net.staticstudios.data.DataManager; import net.staticstudios.data.InsertMode; import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.parse.DDLStatement; +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.SQlStatement; import net.staticstudios.data.util.SchemaTable; import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.utils.Pair; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; import org.intellij.lang.annotations.Language; @@ -48,13 +55,127 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener this.dataManager = dataManager; postgresListener.addHandler(notification -> { - switch (notification.getOperation()) { //todo: update our cache - case UPDATE -> { - } - case INSERT -> { - } - case DELETE -> { + try { + SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(notification.getSchema()); + Preconditions.checkNotNull(sqlSchema, "Schema %s not found".formatted(notification.getSchema())); + SQLTable sqlTable = sqlSchema.getTable(notification.getTable()); + Preconditions.checkNotNull(sqlTable, "Table %s.%s not found".formatted(notification.getSchema(), notification.getTable())); + switch (notification.getOperation()) { + case UPDATE -> { //todo: when we update an id column, we have to update references to uniquedata objects, and the map. do this logic in the H2Trigger. + List values = new ArrayList<>(); + StringBuilder sb = new StringBuilder("UPDATE \"").append(notification.getSchema()).append("\".\"").append(notification.getTable()).append("\" SET "); + Pair[] changedValues = new Pair[notification.getData().newDataValueMap().size()]; + + int index = 0; + for (Map.Entry entry : notification.getData().newDataValueMap().entrySet()) { + String column = entry.getKey(); + String newValue = entry.getValue(); + String oldValue = notification.getData().oldDataValueMap().get(column); + if (!Objects.equals(newValue, oldValue)) { + changedValues[index++] = Pair.of(column, newValue); + } + } + + for (Pair changed : changedValues) { + if (changed == null) break; + String column = changed.first(); + String encoded = changed.second(); + SQLColumn sqlColumn = sqlTable.getColumn(column); + Preconditions.checkNotNull(sqlColumn, "Column %s.%s.%s not found".formatted(notification.getSchema(), notification.getTable(), column)); + Object decoded = encoded == null ? null : Primitives.decodePrimitive(sqlColumn.getType(), encoded); + values.add(decoded); + sb.append("\"").append(column).append("\" = ?, "); + } + sb.setLength(sb.length() - 2); + sb.append(" WHERE "); + for (ColumnMetadata idColumnMetadata : sqlTable.getIdColumns()) { + String idColumn = idColumnMetadata.name(); + sb.append("\"").append(idColumn).append("\" = ? AND "); + SQLColumn sqlColumn = sqlTable.getColumn(idColumn); + Preconditions.checkNotNull(sqlColumn, "Column %s.%s.%s not found".formatted(notification.getSchema(), notification.getTable(), idColumn)); + String encoded = notification.getData().oldDataValueMap().get(idColumn); + Preconditions.checkNotNull(encoded, "ID Column %s.%s.%s not found in notification".formatted(notification.getSchema(), notification.getTable(), idColumn)); + Object decoded = Primitives.decodePrimitive(sqlColumn.getType(), encoded); + values.add(decoded); + } + sb.setLength(sb.length() - 5); + String sql = sb.toString(); + + Connection connection = getConnection(); + try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + for (int i = 0; i < values.size(); i++) { + preparedStatement.setObject(i + 1, values.get(i)); + } + logger.debug("[H2] [HANDLE POSTGRES UPDATE] {}", sql); + preparedStatement.executeUpdate(); + if (!connection.getAutoCommit()) { + connection.commit(); + } + } + } + case INSERT -> { + List values = new ArrayList<>(); + StringBuilder sb = new StringBuilder("INSERT INTO \"").append(notification.getSchema()).append("\".\"").append(notification.getTable()).append("\" ("); + + for (Map.Entry entry : notification.getData().newDataValueMap().entrySet()) { + String column = entry.getKey(); + String encoded = entry.getValue(); + SQLColumn sqlColumn = sqlTable.getColumn(column); + Preconditions.checkNotNull(sqlColumn, "Column %s.%s.%s not found".formatted(notification.getSchema(), notification.getTable(), column)); + Object decoded = encoded == null ? null : Primitives.decodePrimitive(sqlColumn.getType(), encoded); + values.add(decoded); + sb.append("\"").append(column).append("\", "); + } + sb.setLength(sb.length() - 2); + sb.append(") VALUES ("); + sb.append("?, ".repeat(values.size())); + sb.setLength(sb.length() - 2); + sb.append(")"); + String sql = sb.toString(); + + Connection connection = getConnection(); + try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + for (int i = 0; i < values.size(); i++) { + preparedStatement.setObject(i + 1, values.get(i)); + } + logger.debug("[H2] [HANDLE POSTGRES INSERT] {}", sql); + preparedStatement.executeUpdate(); + if (!connection.getAutoCommit()) { + connection.commit(); + } + } + } + case DELETE -> { + List values = new ArrayList<>(); + StringBuilder sb = new StringBuilder("DELETE FROM \"").append(notification.getSchema()).append("\".\"").append(notification.getTable()).append("\" WHERE "); + for (ColumnMetadata idColumnMetadata : sqlTable.getIdColumns()) { + String idColumn = idColumnMetadata.name(); + sb.append("\"").append(idColumn).append("\" = ? AND "); + SQLColumn sqlColumn = sqlTable.getColumn(idColumn); + Preconditions.checkNotNull(sqlColumn, "Column %s.%s.%s not found".formatted(notification.getSchema(), notification.getTable(), idColumn)); + String encoded = notification.getData().oldDataValueMap().get(idColumn); + Preconditions.checkNotNull(encoded, "ID Column %s.%s.%s not found in notification".formatted(notification.getSchema(), notification.getTable(), idColumn)); + Object decoded = Primitives.decodePrimitive(sqlColumn.getType(), encoded); + values.add(decoded); + } + sb.setLength(sb.length() - 5); + String sql = sb.toString(); + + Connection connection = getConnection(); + try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) { + for (int i = 0; i < values.size(); i++) { + preparedStatement.setObject(i + 1, values.get(i)); + } + logger.debug("[H2] [HANDLE POSTGRES DELETE] {}", sql); + preparedStatement.executeUpdate(); + if (!connection.getAutoCommit()) { + connection.commit(); + } + } + } } + } catch (Exception e) { + logger.error("Error handling notification from postgres", e); } }); diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java b/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java index ad66fd48..e9ff5eb8 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java @@ -34,7 +34,7 @@ public void init(Connection conn, String schemaName, String triggerName, String @Override public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException { - //todo: when were loading our initial data, we should ignore all triggers. + //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); @@ -83,8 +83,11 @@ private void handleUpdate(Object[] oldRow, Object[] newRow) { for (String changedColumn : changedColumns) { dataManager.callUpdateHandlers(columnNames, schema, table, changedColumn, oldRow, newRow); } + + //todo: handle id columns being changed } private void handleDelete(Object[] oldRow) { + dataManager.delete(columnNames, schema, table, oldRow); } } diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java b/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java index 7557c581..c0cfffd2 100644 --- a/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java +++ b/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java @@ -83,6 +83,7 @@ public PostgresListener(DataManager dataManager, DataSourceConfig ds) { logger.warn("Connection closed, re-establishing connection"); try { setPgConnection(dataManager, ds); + //todo: after we reconnect (not on the initial connection) we should re-sync everything. } catch (SQLException e) { logger.error("Error re-establishing connection", e); } diff --git a/src/main/java/net/staticstudios/data/primative/Primitive.java b/src/main/java/net/staticstudios/data/primative/Primitive.java new file mode 100644 index 00000000..2634fd64 --- /dev/null +++ b/src/main/java/net/staticstudios/data/primative/Primitive.java @@ -0,0 +1,47 @@ +package net.staticstudios.data.primative; + +import java.util.function.Function; + +public class Primitive { + private final Class runtimeType; + private final Function decoder; + private final Function encoder; + private final boolean nullable; + private final T defaultValue; + + public Primitive(Class runtimeType, Function decoder, Function encoder, boolean nullable, T defaultValue) { + this.runtimeType = runtimeType; + this.decoder = decoder; + this.encoder = encoder; + this.nullable = nullable; + this.defaultValue = defaultValue; + } + + public static PrimitiveBuilder builder(Class runtimeType) { + return new PrimitiveBuilder<>(runtimeType); + } + + public T decode(String value) { + return decoder.apply(value); + } + + public String encode(T value) { + return encoder.apply(value); + } + + public String unsafeEncode(Object value) { + return encoder.apply(runtimeType.cast(value)); + } + + public boolean isNullable() { + return nullable; + } + + public T getDefaultValue() { + return defaultValue; + } + + public Class getRuntimeType() { + return runtimeType; + } +} diff --git a/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java b/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java new file mode 100644 index 00000000..e40bed41 --- /dev/null +++ b/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java @@ -0,0 +1,60 @@ +package net.staticstudios.data.primative; + +import com.google.common.base.Preconditions; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class PrimitiveBuilder { + private final Class runtimeType; + private Function decoder; + private Function encoder; + private Boolean nullable; + private T defaultValue; + + public PrimitiveBuilder(Class runtimeType) { + this.runtimeType = runtimeType; + } + + public PrimitiveBuilder decoder(Function decoder) { + this.decoder = decoder; + return this; + } + + /** + * Note that the encoder should encode the value to a string the exact same as Postgres would. + * + * @param encoder The encoder function + * @return The builder + */ + public PrimitiveBuilder encoder(Function encoder) { + this.encoder = encoder; + return this; + } + + public PrimitiveBuilder nullable(boolean nullable) { + this.nullable = nullable; + return this; + } + + public PrimitiveBuilder defaultValue(T defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public Primitive build(Consumer> consumer) { + Preconditions.checkNotNull(decoder, "Decoder is null"); + Preconditions.checkNotNull(encoder, "Encoder is null"); + Preconditions.checkNotNull(consumer, "Consumer is null"); + Preconditions.checkNotNull(nullable, "Nullable flag is null"); + + if (!nullable) { + Preconditions.checkNotNull(defaultValue, "Default value is null"); + } + + Primitive primitive = new Primitive<>(runtimeType, decoder, encoder, nullable, defaultValue); + consumer.accept(primitive); + + return primitive; + } +} diff --git a/src/main/java/net/staticstudios/data/primative/Primitives.java b/src/main/java/net/staticstudios/data/primative/Primitives.java new file mode 100644 index 00000000..8d3bd07c --- /dev/null +++ b/src/main/java/net/staticstudios/data/primative/Primitives.java @@ -0,0 +1,150 @@ +package net.staticstudios.data.primative; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.util.PostgresUtils; + +import java.sql.Timestamp; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unused") +public class Primitives { + // General rule of thumb: if the Primitive is a Java primitive, it should not be nullable, everything else should be nullable. + private static final DateTimeFormatter TIMESTAMP_FORMATTER = new DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .appendPattern("xxx") + .toFormatter() + .withZone(ZoneId.of("UTC")); + + private static Map, Primitive> primitives; + public static final Primitive STRING = Primitive.builder(String.class) + .nullable(true) + .encoder(s -> s) + .decoder(s -> s) + .build(Primitives::register); + public static final Primitive CHARACTER = Primitive.builder(Character.class) + .nullable(false) + .defaultValue((char) 0) + .encoder(c -> Character.toString(c)) + .decoder(s -> s.charAt(0)) + .build(Primitives::register); + public static final Primitive BYTE = Primitive.builder(Byte.class) + .nullable(false) + .defaultValue((byte) 0) + .encoder(b -> Byte.toString(b)) + .decoder(Byte::parseByte) + .build(Primitives::register); + public static final Primitive SHORT = Primitive.builder(Short.class) + .nullable(false) + .defaultValue((short) 0) + .encoder(s -> Short.toString(s)) + .decoder(Short::parseShort) + .build(Primitives::register); + public static final Primitive INTEGER = Primitive.builder(Integer.class) + .nullable(false) + .defaultValue(0) + .encoder(i -> Integer.toString(i)) + .decoder(Integer::parseInt) + .build(Primitives::register); + public static final Primitive LONG = Primitive.builder(Long.class) + .nullable(false) + .defaultValue(0L) + .encoder(l -> Long.toString(l)) + .decoder(Long::parseLong) + .build(Primitives::register); + public static final Primitive FLOAT = Primitive.builder(Float.class) + .nullable(false) + .defaultValue(0.0f) + .encoder(f -> Float.toString(f)) + .decoder(Float::parseFloat) + .build(Primitives::register); + public static final Primitive DOUBLE = Primitive.builder(Double.class) + .nullable(false) + .defaultValue(0.0) + .encoder(d -> Double.toString(d)) + .decoder(Double::parseDouble) + .build(Primitives::register); + public static final Primitive BOOLEAN = Primitive.builder(Boolean.class) + .nullable(false) + .defaultValue(false) + .encoder(b -> Boolean.toString(b)) + .decoder(Boolean::parseBoolean) + .build(Primitives::register); + public static final Primitive UUID = Primitive.builder(java.util.UUID.class) + .nullable(true) + .encoder(uuid -> uuid == null ? null : uuid.toString()) + .decoder(s -> s == null ? null : java.util.UUID.fromString(s)) + .build(Primitives::register); + public static final Primitive TIMESTAMP = Primitive.builder(Timestamp.class) + .nullable(true) + .encoder(timestamp -> { + if (timestamp == null) { + return null; + } + + return TIMESTAMP_FORMATTER.format(timestamp.toInstant()); + }) + .decoder(s -> { + if (s == null) { + return null; + } + + OffsetDateTime parsedTimestamp = OffsetDateTime.parse(s, TIMESTAMP_FORMATTER); + return Timestamp.from(parsedTimestamp.toInstant()); + }) + .build(Primitives::register); + public static final Primitive BYTE_ARRAY = Primitive.builder(byte[].class) + .nullable(true) + .encoder(b -> { + if (b == null) { + return null; + } + + return PostgresUtils.toHex(b); + }) + .decoder(s -> { + if (s == null) { + return null; + } + + return PostgresUtils.toBytes(s); + }) + .build(Primitives::register); + + public static Primitive getPrimitive(Class type) { + return primitives.get(type); + } + + public static Object decodePrimitive(Class type, String value) { + Primitive primitive = getPrimitive(type); + Preconditions.checkNotNull(primitive, "No primitive found for type: " + type.getName()); + return primitive.decode(value); + } + + public static boolean isPrimitive(Class type) { + return primitives.containsKey(type); + } + + @SuppressWarnings("unchecked") + public static T decode(Class type, String value) { + return (T) getPrimitive(type).decode(value); + } + + public static String encode(Object value) { + if (value == null) { + return null; + } + return getPrimitive(value.getClass()).unsafeEncode(value); + } + + private static void register(Primitive primitive) { + if (primitives == null) { + primitives = new HashMap<>(); + } + primitives.put(primitive.getRuntimeType(), primitive); + } +} diff --git a/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java b/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java index a1602107..502f04d3 100644 --- a/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java +++ b/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java @@ -25,6 +25,10 @@ public Stream stream() { return Arrays.stream(pairs); } + public boolean isEmpty() { + return pairs.length == 0; + } + @Override public java.util.@NotNull Iterator iterator() { return new java.util.Iterator<>() { diff --git a/src/main/java/net/staticstudios/data/util/PostgresUtils.java b/src/main/java/net/staticstudios/data/util/PostgresUtils.java new file mode 100644 index 00000000..89813c45 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/PostgresUtils.java @@ -0,0 +1,22 @@ +package net.staticstudios.data.util; + +public class PostgresUtils { + public static byte[] toBytes(String hex) { + hex = hex.substring(2); // Remove the \x prefix + byte[] bytes = new byte[hex.length() / 2]; + for (int i = 0; i < hex.length(); i += 2) { + bytes[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16)); + } + + return bytes; + } + + public static String toHex(byte[] bytes) { + StringBuilder sb = new StringBuilder("\\x"); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + + return sb.toString(); + } +} diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 7ae9a238..112a66b0 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -1,6 +1,5 @@ package net.staticstudios.data; -import net.staticstudios.data.impl.h2.H2DataAccessor; import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.mock.MockUser; @@ -9,9 +8,7 @@ import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Test; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -19,8 +16,7 @@ import java.util.List; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; public class PersistentValueTest extends DataTest { @@ -40,17 +36,7 @@ public void testReadData() throws SQLException { .insert(InsertMode.SYNC); } - - Connection h2Connection; - try { - Method getConnectionMethod = H2DataAccessor.class.getDeclaredMethod("getConnection"); - getConnectionMethod.setAccessible(true); - h2Connection = (Connection) getConnectionMethod.invoke(dataManager.getDataAccessor()); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - throw new RuntimeException(e); - } - - try (Statement statement = h2Connection.createStatement()) { + try (Statement statement = getH2Connection(dataManager).createStatement()) { ResultSet rs = statement.executeQuery("SCRIPT"); while (rs.next()) { System.out.println(rs.getString(1)); @@ -159,4 +145,94 @@ public void testUpdateHandlerRegistration() { mockUser.name.set("new name"); assertEquals(3, mockUser.getNameUpdates()); } + + @Test + public void testReceiveUpdateFromPostgres() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .favoriteColor("orange") + .nameUpdates(0) + .insert(InsertMode.SYNC); + + assertEquals("test user", mockUser.name.get()); + assertEquals(0, mockUser.getNameUpdates()); + + try (PreparedStatement preparedStatement = getConnection().prepareStatement("UPDATE users SET name = ? WHERE id = ?")) { + preparedStatement.setString(1, "updated from pg"); + preparedStatement.setObject(2, id); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + waitForDataPropagation(); + + assertEquals("updated from pg", mockUser.name.get()); + assertEquals(1, mockUser.getNameUpdates()); + } + + @Test + public void testReceiveInsertFromPostgres() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + + try (PreparedStatement preferencesStatement = getConnection().prepareStatement("INSERT INTO user_preferences (user_id, fav_color) VALUES (?, ?)"); + PreparedStatement metadataStatement = getConnection().prepareStatement("INSERT INTO user_metadata (user_id, name_updates) VALUES (?, ?)"); + PreparedStatement userStatement = getConnection().prepareStatement("INSERT INTO users (id, name) VALUES (?, ?)")) { + preferencesStatement.setObject(1, id); + preferencesStatement.setObject(2, 0); + preferencesStatement.executeUpdate(); + metadataStatement.setObject(1, id); + metadataStatement.setInt(2, 0); + metadataStatement.executeUpdate(); + userStatement.setObject(1, id); + userStatement.setString(2, "inserted from pg"); + userStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + waitForDataPropagation(); + + MockUser mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); //todo: i want a type safe version of this. query builder? + + assertEquals("inserted from pg", mockUser.name.get()); + assertEquals(0, mockUser.getNameUpdates()); + } + + @Test + public void testReceiveDeleteFromPostgres() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .favoriteColor("orange") + .nameUpdates(0) + .insert(InsertMode.SYNC); + + assertEquals("test user", mockUser.name.get()); + assertEquals(0, mockUser.getNameUpdates()); + assertFalse(mockUser.isDeleted()); + + try (PreparedStatement preparedStatement = getConnection().prepareStatement("DELETE FROM users WHERE id = ?")) { + preparedStatement.setObject(1, id); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + waitForDataPropagation(); + + assertTrue(mockUser.isDeleted()); + + mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); + assertNull(mockUser); + } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/misc/DataTest.java b/src/test/java/net/staticstudios/data/misc/DataTest.java index 295e0fa4..b563e8dd 100644 --- a/src/test/java/net/staticstudios/data/misc/DataTest.java +++ b/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -2,6 +2,7 @@ import com.redis.testcontainers.RedisContainer; import net.staticstudios.data.DataManager; +import net.staticstudios.data.impl.h2.H2DataAccessor; import net.staticstudios.data.util.DataSourceConfig; import net.staticstudios.utils.ThreadUtils; import org.junit.jupiter.api.AfterAll; @@ -13,6 +14,8 @@ import redis.clients.jedis.Jedis; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.sql.*; import java.util.LinkedList; import java.util.List; @@ -140,4 +143,15 @@ public void waitForDataPropagation() { } } + public Connection getH2Connection(DataManager dataManager) { + Connection h2Connection; + try { + Method getConnectionMethod = H2DataAccessor.class.getDeclaredMethod("getConnection"); + getConnectionMethod.setAccessible(true); + h2Connection = (Connection) getConnectionMethod.invoke(dataManager.getDataAccessor()); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + return h2Connection; + } } diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/MockUser.java index d7b5e645..eb69be86 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/MockUser.java @@ -9,7 +9,9 @@ @Data(schema = "public", table = "users") public class MockUser extends UniqueData { + //todo: cached values //todo: @OneToMany, @ManyToMany, @ManyToOne + //todo: note - maybe PC's add and remove handlers can be implemented using update handlers @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); @Column(name = "settings_id", nullable = true) From 94738a0c7b59e0439e0acdb9584124c7585b6eda Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 16 Sep 2025 14:46:01 -0400 Subject: [PATCH 08/75] handle updating id columns --- .../net/staticstudios/data/DataManager.java | 139 ++++++++++++++++-- .../net/staticstudios/data/UniqueData.java | 2 - .../data/impl/data/PersistentValueImpl.java | 56 +------ .../data/impl/h2/H2DataAccessor.java | 5 +- .../staticstudios/data/impl/h2/H2Trigger.java | 6 +- .../staticstudios/data/parse/SQLBuilder.java | 34 +++-- .../staticstudios/data/parse/SQLTable.java | 2 +- .../data/PersistentValueTest.java | 82 ++++++++++- .../net/staticstudios/data/SQLParseTest.java | 4 +- .../net/staticstudios/data/misc/DataTest.java | 12 ++ .../data/misc/MockThreadProvider.java | 7 +- 11 files changed, 258 insertions(+), 91 deletions(-) diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 8ac057d5..93bd9883 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -10,6 +10,8 @@ import net.staticstudios.data.parse.*; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.utils.ThreadUtils; +import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Blocking; import org.slf4j.Logger; @@ -17,6 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -56,9 +59,6 @@ public DataManager(DataSourceConfig dataSourceConfig, boolean setGlobal) { this.taskQueue = new TaskQueue(dataSourceConfig, applicationName); dataAccessor = new H2DataAccessor(this, postgresListener, taskQueue); - //todo: when we parse UniqueData objects we should build an internal map, and then when we are done auto create the sql if the tables dont exist - //todo: this will be extremely useful for building the internal cache tables - //todo: when we reconnect to postgres, refresh the internal cache from the source //todo: support for CachedValues @@ -129,7 +129,7 @@ public void callUpdateHandlers(List columnNames, String schema, String t Object deserializedOldValue = oldSerializedValues[columnIndex]; //todo: these Object deserializedNewValue = newSerializedValues[columnIndex]; - wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue); + ThreadUtils.submit(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); } } } @@ -174,10 +174,6 @@ public final void load(Class... classes) { } catch (SQLException e) { throw new RuntimeException(e); } - - //todo: the sql builder needs to be altered to spit out the sql for the just walked class - //todo: then we need to create those tables in the cache and source, and then finally load all of that data into the cache - //todo: also, stare listening to events before we start grabbing data, queue them, and then process them after the initial load is done } public void extractMetadata(Class clazz) { @@ -247,6 +243,59 @@ public void delete(List columnNames, String schema, String table, Object }); } + @ApiStatus.Internal + public synchronized void updateIdColumns(List columnNames, String schema, String table, String column, Object[] oldValues, Object[] newValues) { + uniqueDataMetadataMap.values().forEach(uniqueDataMetadata -> { + if (!uniqueDataMetadata.schema().equals(schema) || !uniqueDataMetadata.table().equals(table)) { + return; + } + + boolean idColumnWasUpdated = false; + for (ColumnMetadata idColumn : uniqueDataMetadata.idColumns()) { + if (idColumn.name().equals(column)) { + idColumnWasUpdated = true; + break; + } + } + + if (!idColumnWasUpdated) { + return; + } + + ColumnValuePair[] oldIdColumns = new ColumnValuePair[uniqueDataMetadata.idColumns().size()]; + ColumnValuePair[] newIdColumns = new ColumnValuePair[uniqueDataMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : uniqueDataMetadata.idColumns()) { + boolean found = false; + for (int i = 0; i < columnNames.size(); i++) { + if (idColumn.name().equals(columnNames.get(i))) { + oldIdColumns[uniqueDataMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), oldValues[i]); + newIdColumns[uniqueDataMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), newValues[i]); + found = true; + break; + } + } + Preconditions.checkArgument(found, "Not all ID columns were provided for UniqueData class %s. Required: %s, Provided: %s", uniqueDataMetadata.clazz().getName(), uniqueDataMetadata.idColumns(), Arrays.toString(oldValues)); + } + if (Arrays.equals(oldIdColumns, newIdColumns)) { + return; // no change to id columns here + } + + ColumnValuePairs oldIdCols = new ColumnValuePairs(oldIdColumns); + Map classCache = uniqueDataInstanceCache.get(uniqueDataMetadata.clazz()); + if (classCache == null) { + return; + } + UniqueData instance = classCache.remove(oldIdCols); + if (instance == null) { + return; + } + ColumnValuePairs newIdCols = new ColumnValuePairs(newIdColumns); + instance.setIdColumns(newIdCols); + classCache.put(newIdCols, instance); + }); + + } + @SuppressWarnings("unchecked") public T get(Class clazz, ColumnValuePair... idColumnValues) { ColumnValuePairs idColumns = new ColumnValuePairs(idColumnValues); @@ -280,6 +329,7 @@ public T get(Class clazz, ColumnValuePair... idColumnV if (instance.isDeleted()) { return null; } + return instance; } try { @@ -293,10 +343,24 @@ public T get(Class clazz, ColumnValuePair... idColumnV instance.setDataManager(this); instance.setIdColumns(idColumns); - Data dataAnnotation = clazz.getAnnotation(Data.class); - Preconditions.checkNotNull(dataAnnotation, "UniqueData class %s is missing @Data annotation", clazz.getName()); - String schema = ValueUtils.parseValue(dataAnnotation.schema()); - String table = ValueUtils.parseValue(dataAnnotation.table()); + String schema = metadata.schema(); + String table = metadata.table(); + + 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 "); + } + 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; + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + PersistentValueImpl.delegate(schema, table, instance); ReferenceImpl.delegate(instance); @@ -367,7 +431,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { tables.add(table); }); - //sort tables based on foreign key dependencies. tables who are depended on should come first + //sort tables based on foreign key dependencies. tables who are depended on should come last // Build dependency graph: table -> set of tables it depends on Map> dependencyGraph = new HashMap<>(); @@ -396,6 +460,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { for (SQLTable table : dependencyGraph.keySet()) { topoSort(table, dependencyGraph, visited, orderedTables); } + Collections.reverse(orderedTables); List sqlStatements = new ArrayList<>(); @@ -441,6 +506,54 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { } } + public void set(String schema, String table, String column, ColumnValuePairs idColumns, Map idColumnLinks, Object value) { + StringBuilder sqlBuilder; + if (idColumnLinks.isEmpty()) { + sqlBuilder = new StringBuilder().append("UPDATE \"").append(schema).append("\".\"").append(table).append("\" SET \"").append(column).append("\" = ? WHERE "); + for (ColumnValuePair columnValuePair : idColumns) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append("\"").append(name).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + } else { // we're dealing with a foreign key + sqlBuilder = new StringBuilder().append("MERGE INTO \"").append(schema).append("\".\"").append(table).append("\" target USING (VALUES (?"); + sqlBuilder.append(", ?".repeat(idColumns.getPairs().length)); + sqlBuilder.append(")) AS source (\"").append(column).append("\""); + for (ColumnValuePair columnValuePair : idColumns) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append(", \"").append(name).append("\""); + } + sqlBuilder.append(") ON "); + for (ColumnValuePair columnValuePair : idColumns) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append("target.\"").append(name).append("\" = source.\"").append(name).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + sqlBuilder.append(" WHEN MATCHED THEN UPDATE SET \"").append(column).append("\" = source.\"").append(column).append("\" WHEN NOT MATCHED THEN INSERT (\"").append(column).append("\""); + for (ColumnValuePair columnValuePair : idColumns) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append(", \"").append(name).append("\""); + } + sqlBuilder.append(") VALUES (source.\"").append(column).append("\""); + for (ColumnValuePair columnValuePair : idColumns) { + String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append(", source.\"").append(name).append("\""); + } + sqlBuilder.append(")"); + } + @Language("SQL") String sql = sqlBuilder.toString(); + List values = new ArrayList<>(1 + idColumns.getPairs().length); + values.add(value); + for (ColumnValuePair columnValuePair : idColumns) { + values.add(columnValuePair.value()); + } + try { + dataAccessor.executeUpdate(sql, values); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + private boolean hasCycle(SQLTable table, Map> dependencyGraph, Set visited, Set stack) { if (stack.contains(table)) { return true; diff --git a/src/main/java/net/staticstudios/data/UniqueData.java b/src/main/java/net/staticstudios/data/UniqueData.java index b98682a5..300166c5 100644 --- a/src/main/java/net/staticstudios/data/UniqueData.java +++ b/src/main/java/net/staticstudios/data/UniqueData.java @@ -10,8 +10,6 @@ public abstract class UniqueData { private DataManager dataManager; private volatile boolean isDeleted = false; - //todo: when an update is done to an id column, we need to handle it here. - //todo: when this row is deleted from the database, we should mark this with a deleted flag and throw an error if any operations are attempted on it. more specifically, any pvs referencing this object should throw an error if this has been deleted. @ApiStatus.Internal protected final void setDataManager(DataManager dataManager) { this.dataManager = dataManager; diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index b292d5f8..0dec64b0 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -8,7 +8,10 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class PersistentValueImpl implements PersistentValue { private final DataAccessor dataAccessor; @@ -96,9 +99,6 @@ public static void delegate(String schema, String table, } Preconditions.checkNotNull(columnMetadata, "PersistentValue field %s is missing @Column annotation", pair.field().getName()); - //todo: the primary key gets a bit more complicated when we are dealing with a foreign key. this needs to be handled, and a new ForeignKey created which properly maps my id name to the foreign key name. - //todo: update: what??? - if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { PersistentValueImpl.createAndDelegate(proxyPv, columnMetadata); } else { @@ -167,53 +167,7 @@ public T get() { @Override public void set(T value) { Preconditions.checkArgument(!holder.isDeleted(), "Cannot set value on a deleted UniqueData instance"); - //todo: whenever we set an id name of something, we need to tell the datamanager to update any tracked instance of uniquedata with that id. - T oldValue = get(); - StringBuilder sqlBuilder; - if (idColumnLinks.isEmpty()) { - sqlBuilder = new StringBuilder().append("UPDATE \"").append(schema).append("\".\"").append(table).append("\" SET \"").append(column).append("\" = ? WHERE "); - for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); - sqlBuilder.append("\"").append(name).append("\" = ? AND "); - } - sqlBuilder.setLength(sqlBuilder.length() - 5); - } else { // we're dealing with a foreign key - sqlBuilder = new StringBuilder().append("MERGE INTO \"").append(schema).append("\".\"").append(table).append("\" target USING (VALUES (?"); - sqlBuilder.append(", ?".repeat(holder.getIdColumns().getPairs().length)); - sqlBuilder.append(")) AS source (\"").append(column).append("\""); - for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); - sqlBuilder.append(", \"").append(name).append("\""); - } - sqlBuilder.append(") ON "); - for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); - sqlBuilder.append("target.\"").append(name).append("\" = source.\"").append(name).append("\" AND "); - } - sqlBuilder.setLength(sqlBuilder.length() - 5); - sqlBuilder.append(" WHEN MATCHED THEN UPDATE SET \"").append(column).append("\" = source.\"").append(column).append("\" WHEN NOT MATCHED THEN INSERT (\"").append(column).append("\""); - for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); - sqlBuilder.append(", \"").append(name).append("\""); - } - sqlBuilder.append(") VALUES (source.\"").append(column).append("\""); - for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); - sqlBuilder.append(", source.\"").append(name).append("\""); - } - sqlBuilder.append(")"); - } - @Language("SQL") String sql = sqlBuilder.toString(); - List values = new ArrayList<>(1 + holder.getIdColumns().getPairs().length); - values.add(value); - for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - values.add(columnValuePair.value()); - } - try { - dataAccessor.executeUpdate(sql, values); - } catch (SQLException e) { - throw new RuntimeException(e); - } + holder.getDataManager().set(schema, table, column, holder.getIdColumns(), idColumnLinks, value); } //todo: support set with SetMode, or operationMode (SYNC vs ASYNC) diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index 2cde1100..283dba2f 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -61,7 +61,7 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener SQLTable sqlTable = sqlSchema.getTable(notification.getTable()); Preconditions.checkNotNull(sqlTable, "Table %s.%s not found".formatted(notification.getSchema(), notification.getTable())); switch (notification.getOperation()) { - case UPDATE -> { //todo: when we update an id column, we have to update references to uniquedata objects, and the map. do this logic in the H2Trigger. + case UPDATE -> { List values = new ArrayList<>(); StringBuilder sb = new StringBuilder("UPDATE \"").append(notification.getSchema()).append("\".\"").append(notification.getTable()).append("\" SET "); Pair[] changedValues = new Pair[notification.getData().newDataValueMap().size()]; @@ -375,6 +375,9 @@ public void executeUpdate(@Language("SQL") String sql, List values) thro } logger.debug("[H2] {}", sql); cachePreparedStatement.executeUpdate(); + if (!getConnection().getAutoCommit()) { + getConnection().commit(); + } taskQueue.submitTask(connection -> { PreparedStatement realPreparedStatement = connection.prepareStatement(sql); diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java b/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java index e9ff5eb8..cd3d92ac 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java @@ -81,10 +81,12 @@ private void handleUpdate(Object[] oldRow, Object[] newRow) { } for (String changedColumn : changedColumns) { - dataManager.callUpdateHandlers(columnNames, schema, table, changedColumn, oldRow, newRow); + dataManager.updateIdColumns(columnNames, schema, table, changedColumn, oldRow, newRow); } - //todo: handle id columns being changed + for (String changedColumn : changedColumns) { + dataManager.callUpdateHandlers(columnNames, schema, table, changedColumn, oldRow, newRow); + } } private void handleDelete(Object[] oldRow) { diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index 32e5185e..1a2375e3 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.*; import net.staticstudios.data.util.*; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.Field; @@ -11,6 +12,7 @@ public class SQLBuilder { public static final String INDENT = " "; private final Map parsedSchemas; + private final Map> foreignKeys = new HashMap<>(); private final DataManager dataManager; public SQLBuilder(DataManager dataManager) { @@ -54,6 +56,11 @@ public List parse(Class clazz) { return getDefs(schemas.values()); } + public @NotNull List getForeignKeysReferencingColumn(String schema, String table, String column) { + String key = schema + "." + table + "." + column; + return foreignKeys.getOrDefault(key, Collections.emptyList()); + } + public @Nullable SQLSchema getSchema(String name) { return parsedSchemas.get(name); } @@ -109,43 +116,43 @@ private List getDefs(Collection schemas) { //todo: add if (foreignKey == null) { continue; } - String fKeyName = "fk_" + table.getName() + "_" + String.join("_", foreignKey.getLinkingColumns().keySet()) + "_to_" + foreignKey.getSchema() + "_" + foreignKey.getTable() + "_" + String.join("_", foreignKey.getLinkingColumns().values()); + String fKeyName = "fk_" + foreignKey.getSchema() + "_" + foreignKey.getTable() + "_" + String.join("_", foreignKey.getLinkingColumns().values()) + "_to_" + table.getName() + "_" + String.join("_", foreignKey.getLinkingColumns().keySet()); StringBuilder sb = new StringBuilder(); - sb.append("ALTER TABLE \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" "); + sb.append("ALTER TABLE \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" "); sb.append("ADD CONSTRAINT IF NOT EXISTS ").append(fKeyName).append(" "); sb.append("FOREIGN KEY ("); - for (String localCol : foreignKey.getLinkingColumns().keySet()) { + for (String localCol : foreignKey.getLinkingColumns().values()) { sb.append("\"").append(localCol).append("\", "); } sb.setLength(sb.length() - 2); sb.append(") "); - sb.append("REFERENCES \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" ("); - for (String foreignCol : foreignKey.getLinkingColumns().values()) { + sb.append("REFERENCES \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ("); + for (String foreignCol : foreignKey.getLinkingColumns().keySet()) { sb.append("\"").append(foreignCol).append("\", "); } sb.setLength(sb.length() - 2); - sb.append(") ON DELETE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type + sb.append(") ON DELETE CASCADE ON UPDATE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type String h2 = sb.toString(); sb = new StringBuilder(); sb.append("DO $$ BEGIN "); - sb.append("IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = '").append(fKeyName).append("' AND table_name = '").append(table.getName()).append("' AND constraint_schema = '").append(schema.getName()).append("' AND constraint_type = 'FOREIGN KEY') THEN "); + sb.append("IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = '").append(fKeyName).append("' AND table_name = '").append(foreignKey.getTable()).append("' AND constraint_schema = '").append(foreignKey.getSchema()).append("' AND constraint_type = 'FOREIGN KEY') THEN "); - sb.append("ALTER TABLE \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" "); + sb.append("ALTER TABLE \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" "); sb.append("ADD CONSTRAINT ").append(fKeyName).append(" "); sb.append("FOREIGN KEY ("); - for (String localCol : foreignKey.getLinkingColumns().keySet()) { + for (String localCol : foreignKey.getLinkingColumns().values()) { sb.append("\"").append(localCol).append("\", "); } sb.setLength(sb.length() - 2); sb.append(") "); - sb.append("REFERENCES \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" ("); - for (String foreignCol : foreignKey.getLinkingColumns().values()) { + sb.append("REFERENCES \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ("); + for (String foreignCol : foreignKey.getLinkingColumns().keySet()) { sb.append("\"").append(foreignCol).append("\", "); } sb.setLength(sb.length() - 2); - sb.append(") ON DELETE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type + sb.append(") ON DELETE CASCADE ON UPDATE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type sb.append(" END IF; END $$;"); String pg = sb.toString(); statements.add(DDLStatement.of(h2, pg)); @@ -307,12 +314,13 @@ private void parseIndividual(Class clazz, Map new ArrayList<>()).add(foreignKey); } dataSqlTable.getForeignKeys().add(foreignKey); } - Class type = ReflectionUtils.getGenericType(field); //todo: handle custom types to sql types SQLColumn sqlColumn = new SQLColumn(table, type, columnName, nullable, indexed, defaultValue.isEmpty() ? null : SQLUtils.parseDefaultValue(type, defaultValue)); diff --git a/src/main/java/net/staticstudios/data/parse/SQLTable.java b/src/main/java/net/staticstudios/data/parse/SQLTable.java index e4843935..687dc4d7 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLTable.java +++ b/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -11,7 +11,7 @@ public class SQLTable { private final String name; private final List idColumns; private final Map columns; - private final List foreignKeys; + private final List foreignKeys; //todo: NOTE: these are actually fkeys thet REFER to this table. this implies something else and should probably be adjusted public SQLTable(SQLSchema schema, String name, List idColumns) { this.schema = schema; diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 112a66b0..95a8080b 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -134,15 +134,20 @@ public void testUpdateHandlerRegistration() { mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss //the handler for this pv should not have been registered again assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); + waitForUpdateHandlers(); assertEquals(0, mockUser.getNameUpdates()); mockUser.name.set("new name"); + waitForUpdateHandlers(); assertEquals(1, mockUser.getNameUpdates()); mockUser.name.set("new name"); + waitForUpdateHandlers(); assertEquals(1, mockUser.getNameUpdates()); mockUser.name.set("new name2"); + waitForUpdateHandlers(); assertEquals(2, mockUser.getNameUpdates()); mockUser.name.set("new name"); + waitForUpdateHandlers(); assertEquals(3, mockUser.getNameUpdates()); } @@ -184,15 +189,15 @@ public void testReceiveInsertFromPostgres() { try (PreparedStatement preferencesStatement = getConnection().prepareStatement("INSERT INTO user_preferences (user_id, fav_color) VALUES (?, ?)"); PreparedStatement metadataStatement = getConnection().prepareStatement("INSERT INTO user_metadata (user_id, name_updates) VALUES (?, ?)"); PreparedStatement userStatement = getConnection().prepareStatement("INSERT INTO users (id, name) VALUES (?, ?)")) { + userStatement.setObject(1, id); + userStatement.setString(2, "inserted from pg"); + userStatement.executeUpdate(); preferencesStatement.setObject(1, id); preferencesStatement.setObject(2, 0); preferencesStatement.executeUpdate(); metadataStatement.setObject(1, id); metadataStatement.setInt(2, 0); metadataStatement.executeUpdate(); - userStatement.setObject(1, id); - userStatement.setString(2, "inserted from pg"); - userStatement.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); } @@ -235,4 +240,75 @@ public void testReceiveDeleteFromPostgres() { mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); assertNull(mockUser); } + + @Test + public void testChangeIdColumn() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .favoriteColor("orange") + .nameUpdates(0) + .insert(InsertMode.SYNC); + + assertEquals(id, mockUser.id.get()); + assertEquals(0, mockUser.nameUpdates.get()); + mockUser.name.set("new name"); + waitForUpdateHandlers(); + assertEquals(1, mockUser.nameUpdates.get()); + UUID newId = UUID.randomUUID(); + mockUser.id.set(newId); + assertEquals(newId, mockUser.id.get()); + assertNull(dataManager.get(MockUser.class, ColumnValuePair.of("id", id))); + assertNotNull(dataManager.get(MockUser.class, ColumnValuePair.of("id", newId))); + assertSame(dataManager.get(MockUser.class, ColumnValuePair.of("id", newId)), mockUser); + assertEquals(1, mockUser.nameUpdates.get()); + mockUser.name.set("new name2"); + waitForUpdateHandlers(); + assertEquals(2, mockUser.nameUpdates.get()); + } + + @Test + public void testChangeIdColumnInPostgres() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .favoriteColor("orange") + .nameUpdates(0) + .insert(InsertMode.SYNC); + + assertEquals(id, mockUser.id.get()); + assertEquals(0, mockUser.nameUpdates.get()); + mockUser.name.set("new name"); + + waitForUpdateHandlers(); + + assertEquals(1, mockUser.nameUpdates.get()); + UUID newId = UUID.randomUUID(); + + try (PreparedStatement preparedStatement = getConnection().prepareStatement("UPDATE users SET id = ? WHERE id = ?")) { + preparedStatement.setObject(1, newId); + preparedStatement.setObject(2, id); + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + waitForDataPropagation(); + + assertEquals(newId, mockUser.id.get()); + assertNull(dataManager.get(MockUser.class, ColumnValuePair.of("id", id))); + assertNotNull(dataManager.get(MockUser.class, ColumnValuePair.of("id", newId))); + assertSame(dataManager.get(MockUser.class, ColumnValuePair.of("id", newId)), mockUser); + waitForUpdateHandlers(); + assertEquals(1, mockUser.nameUpdates.get()); + mockUser.name.set("new name2"); + waitForUpdateHandlers(); + assertEquals(2, mockUser.nameUpdates.get()); + } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index 3f934305..76547dd3 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -86,8 +86,8 @@ public void testParse() throws Exception { ADD CONSTRAINT posts_interactions_pkey PRIMARY KEY (post_id); ALTER TABLE ONLY social_media.posts ADD CONSTRAINT posts_pkey PRIMARY KEY (post_id); - ALTER TABLE ONLY social_media.posts - ADD CONSTRAINT fk_posts_post_id_to_social_media_posts_interactions_post_id FOREIGN KEY (post_id) REFERENCES social_media.posts_interactions(post_id) ON DELETE CASCADE; + ALTER TABLE ONLY social_media.posts_interactions + ADD CONSTRAINT fk_social_media_posts_interactions_post_id_to_posts_post_id FOREIGN KEY (post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; """; assertEquals(expected.trim(), cleanedDump.toString().trim()); diff --git a/src/test/java/net/staticstudios/data/misc/DataTest.java b/src/test/java/net/staticstudios/data/misc/DataTest.java index b563e8dd..1e56e362 100644 --- a/src/test/java/net/staticstudios/data/misc/DataTest.java +++ b/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -135,6 +135,18 @@ public int getWaitForDataPropagationTime() { return 500 + (Objects.equals(System.getenv("GITHUB_ACTIONS"), "true") ? 1000 : 0); } + public int getWaitForUpdateHandlersTime() { + return 100 + (Objects.equals(System.getenv("GITHUB_ACTIONS"), "true") ? 500 : 0); + } + + public void waitForUpdateHandlers() { + try { + Thread.sleep(getWaitForUpdateHandlersTime()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + public void waitForDataPropagation() { try { Thread.sleep(getWaitForDataPropagationTime()); diff --git a/src/test/java/net/staticstudios/data/misc/MockThreadProvider.java b/src/test/java/net/staticstudios/data/misc/MockThreadProvider.java index b40bff74..6059b1bc 100644 --- a/src/test/java/net/staticstudios/data/misc/MockThreadProvider.java +++ b/src/test/java/net/staticstudios/data/misc/MockThreadProvider.java @@ -4,6 +4,8 @@ import net.staticstudios.utils.ShutdownTask; import net.staticstudios.utils.ThreadUtilProvider; import net.staticstudios.utils.ThreadUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -11,13 +13,12 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import java.util.logging.Logger; public class MockThreadProvider implements ThreadUtilProvider { private final ExecutorService mainThreadExecutorService; private final List syncOnDisableTasksRunNext = Collections.synchronizedList(new ArrayList<>()); private final List shutdownTasks = Collections.synchronizedList(new ArrayList<>()); - private final Logger logger = Logger.getLogger(MockThreadProvider.class.getName()); + private final Logger logger = LoggerFactory.getLogger(MockThreadProvider.class.getName()); private ExecutorService executorService; private boolean isShuttingDown = false; private boolean doneShuttingDown = false; @@ -104,7 +105,7 @@ public void shutdown() { try { CompletableFuture.allOf(asyncFutures.toArray(new CompletableFuture[0])).get(30, TimeUnit.SECONDS); } catch (Exception e) { - getLogger().severe("Failed to wait for async tasks to finish during shutdown stage " + stage); + getLogger().error("Failed to wait for async tasks to finish during shutdown stage " + stage); e.printStackTrace(); } From bc92c100d3b930366119fbee711af428419e220e Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Thu, 18 Sep 2025 16:14:05 -0400 Subject: [PATCH 09/75] query builder --- .../data/processor/DataProcessor.java | 45 ++- .../ForeignPersistentValueMetadata.java | 4 +- .../data/processor/MetadataUtils.java | 7 +- .../processor/PersistentValueMetadata.java | 9 +- .../data/processor/QueryFactory.java | 259 ++++++++++++++++++ .../processor/SchemaTableColumnStatics.java | 28 ++ .../net/staticstudios/data/DataManager.java | 61 ++++- .../data/impl/data/PersistentValueImpl.java | 31 +-- .../data/impl/data/ReferenceImpl.java | 2 +- .../data/insert/InsertContext.java | 2 +- .../query/AbstractConditionalBuilder.java | 100 +++++++ .../data/query/AbstractQueryBuilder.java | 142 ++++++++++ .../net/staticstudios/data/query/Order.java | 6 + .../net/staticstudios/data/query/Query.java | 12 + .../staticstudios/data/query/QueryLike.java | 40 +++ .../data/query/clause/AndClause.java | 23 ++ .../data/query/clause/BetweenClause.java | 25 ++ .../data/query/clause/Clause.java | 8 + .../data/query/clause/CompositeClause.java | 4 + .../data/query/clause/EqualsClause.java | 23 ++ .../data/query/clause/GreaterThanClause.java | 23 ++ .../clause/GreaterThanOrEqualToClause.java | 23 ++ .../data/query/clause/InClause.java | 34 +++ .../data/query/clause/LessThanClause.java | 23 ++ .../query/clause/LessThanOrEqualToClause.java | 23 ++ .../data/query/clause/LikeClause.java | 23 ++ .../data/query/clause/NotEqualsClause.java | 23 ++ .../data/query/clause/NotInClause.java | 34 +++ .../data/query/clause/NotLikeClause.java | 23 ++ .../data/query/clause/NotNullClause.java | 21 ++ .../data/query/clause/NullClause.java | 21 ++ .../data/query/clause/OrClause.java | 23 ++ .../data/query/clause/ParenthesisClause.java | 19 ++ .../data/query/clause/ValueClause.java | 4 + .../data/PersistentValueTest.java | 125 +++++++-- 35 files changed, 1184 insertions(+), 89 deletions(-) create mode 100644 processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java create mode 100644 processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java create mode 100644 src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java create mode 100644 src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java create mode 100644 src/main/java/net/staticstudios/data/query/Order.java create mode 100644 src/main/java/net/staticstudios/data/query/Query.java create mode 100644 src/main/java/net/staticstudios/data/query/QueryLike.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/AndClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/BetweenClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/Clause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/CompositeClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/EqualsClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/InClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/LessThanClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/LikeClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/NotInClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/NotNullClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/NullClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/OrClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java create mode 100644 src/main/java/net/staticstudios/data/query/clause/ValueClause.java diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index c78fd7dd..eb7d95fb 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -27,7 +27,12 @@ public boolean process(Set annotations, RoundEnvironment if (!(annotated instanceof TypeElement type)) continue; try { - generateFactory(type); + Data dataAnnotation = type.getAnnotation(Data.class); + assert dataAnnotation != null; + List metadataList = MetadataUtils.extractMetadata(type); + + generateFactory(type, dataAnnotation, metadataList); + new QueryFactory(processingEnv, type, dataAnnotation, metadataList).generateQueryBuilder(); } catch (IOException e) { processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, @@ -39,9 +44,8 @@ public boolean process(Set annotations, RoundEnvironment return true; } - private void generateFactory(TypeElement entityType) throws IOException { + private void generateFactory(TypeElement entityType, Data dataAnnotation, List metadataList) throws IOException { if (entityType.getModifiers().contains(Modifier.ABSTRACT)) { - //todo: if abstract, we can ignore the factory, but we still need to generate a queryBuilder or something return; } @@ -55,10 +59,8 @@ private void generateFactory(TypeElement entityType) throws IOException { ClassName insertMode = ClassName.get("net.staticstudios.data", "InsertMode"); ClassName insertContext = ClassName.get("net.staticstudios.data.insert", "InsertContext"); - Data dataAnnotation = entityType.getAnnotation(Data.class); - assert dataAnnotation != null; - List metadataList = MetadataUtils.extractMetadata(entityType); + TypeSpec.Builder factoryBuilder = TypeSpec.classBuilder(factoryName); TypeSpec.Builder builderType = TypeSpec.classBuilder("Builder") .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addField(dataManager, "dataManager", Modifier.PRIVATE, Modifier.FINAL) @@ -77,24 +79,9 @@ private void generateFactory(TypeElement entityType) throws IOException { for (Metadata metadata : metadataList) { if (metadata instanceof PersistentValueMetadata persistentValueMetadata) { - String schemaFieldName = persistentValueMetadata.fieldName() + "$schema"; - String tableFieldName = persistentValueMetadata.fieldName() + "$table"; - String columnFieldName = persistentValueMetadata.fieldName() + "$column"; - - + SchemaTableColumnStatics statics = SchemaTableColumnStatics.generateSchemaTableColumnStatics(builderType, persistentValueMetadata); builderType.addField(persistentValueMetadata.genericType(), persistentValueMetadata.fieldName(), Modifier.PRIVATE); - // since we support env variables in the name, parse these at runtime. - builderType.addField(FieldSpec.builder(String.class, schemaFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.schema()) - .build()); - builderType.addField(FieldSpec.builder(String.class, tableFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.table()) - .build()); - builderType.addField(FieldSpec.builder(String.class, columnFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.column()) - .build()); - builderType.addMethod(MethodSpec.methodBuilder(persistentValueMetadata.fieldName()) .addModifiers(Modifier.PUBLIC) .returns(ClassName.get(packageName, factoryName, "Builder")) @@ -108,9 +95,9 @@ private void generateFactory(TypeElement entityType) throws IOException { } insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N)", - schemaFieldName, - tableFieldName, - columnFieldName, + statics.schemaFieldName(), + statics.tableFieldName(), + statics.columnFieldName(), persistentValueMetadata.fieldName()); if (persistentValueMetadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { @@ -126,7 +113,7 @@ private void generateFactory(TypeElement entityType) throws IOException { .findFirst() .orElseThrow(() -> new IllegalStateException("Could not find local column metadata for link: " + localColumn)); - String columnLinkFieldName = columnFieldName + "$" + localColumnMetadata.fieldName() + "$" + i; + String columnLinkFieldName = statics.columnFieldName() + "$" + localColumnMetadata.fieldName() + "$" + i; builderType.addField(FieldSpec.builder(String.class, columnLinkFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), foreignColumn) @@ -134,8 +121,8 @@ private void generateFactory(TypeElement entityType) throws IOException { insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N)", - schemaFieldName, - tableFieldName, + statics.schemaFieldName(), + statics.tableFieldName(), columnLinkFieldName, localColumnMetadata.fieldName()); i++; @@ -157,7 +144,7 @@ private void generateFactory(TypeElement entityType) throws IOException { builderType.addMethod(insertModeMethod.build()); builderType.addMethod(insertCtxMethod.build()); - TypeSpec factory = TypeSpec.classBuilder(factoryName) + TypeSpec factory = factoryBuilder .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(MethodSpec.constructorBuilder() .addModifiers(Modifier.PRIVATE) diff --git a/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java b/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java index feb57065..2923c3b5 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java +++ b/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java @@ -7,8 +7,8 @@ public class ForeignPersistentValueMetadata extends PersistentValueMetadata { private final Map links; - public ForeignPersistentValueMetadata(String schema, String table, String column, String fieldName, TypeName genericType, Map links) { - super(schema, table, column, fieldName, genericType); + public ForeignPersistentValueMetadata(String schema, String table, String column, String fieldName, TypeName genericType, boolean nullable, Map links) { + super(schema, table, column, fieldName, genericType, nullable); this.links = links; } diff --git a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java index a34108f5..44366786 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java +++ b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java @@ -63,6 +63,7 @@ private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnot String schemaName = null; String tableName = null; String columnName = null; + boolean nullable = false; IdColumn idColumn = field.getAnnotation(IdColumn.class); Column column = field.getAnnotation(Column.class); @@ -76,10 +77,12 @@ private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnot tableName = dataAnnotation.table(); schemaName = dataAnnotation.schema(); columnName = column.name(); + nullable = column.nullable(); } else if (foreignColumn != null) { tableName = foreignColumn.table().isEmpty() ? dataAnnotation.table() : foreignColumn.table(); schemaName = foreignColumn.schema().isEmpty() ? dataAnnotation.schema() : foreignColumn.schema(); columnName = foreignColumn.name(); + nullable = foreignColumn.nullable(); } if (idColumn != null || column != null) { @@ -88,7 +91,8 @@ private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnot tableName, columnName, field.getSimpleName().toString(), - typeName + typeName, + nullable ); } if (foreignColumn != null) { @@ -106,6 +110,7 @@ private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnot columnName, field.getSimpleName().toString(), typeName, + nullable, links ); } diff --git a/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java b/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java index 277e280e..d2630e28 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java +++ b/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java @@ -8,14 +8,17 @@ public class PersistentValueMetadata implements Metadata { private final String column; private final String fieldName; private final TypeName genericType; + private final boolean nullable; + public PersistentValueMetadata(String schema, String table, String column, String fieldName, - TypeName genericType) { + TypeName genericType, boolean nullable) { this.schema = schema; this.table = table; this.column = column; this.fieldName = fieldName; this.genericType = genericType; + this.nullable = nullable; } public String schema() { @@ -34,6 +37,10 @@ public String fieldName() { return fieldName; } + public boolean nullable() { + return nullable; + } + public TypeName genericType() { return genericType; } diff --git a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java new file mode 100644 index 00000000..c17f130d --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java @@ -0,0 +1,259 @@ +package net.staticstudios.data.processor; + +import com.palantir.javapoet.*; +import net.staticstudios.data.Data; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import java.io.IOException; +import java.util.List; + +public class QueryFactory { + private static final ClassName DATA_MANAGER_CLASS_NAME = ClassName.get("net.staticstudios.data", "DataManager"); + private static final ClassName ABSTRACT_QUERY_BUILDER_CLASS_NAME = ClassName.get("net.staticstudios.data.query", "AbstractQueryBuilder"); + private final ProcessingEnvironment processingEnv; + private final TypeElement entityType; + private final Data dataAnnotation; + private final List metadataList; + private final String entityName; + private final String queryName; + private final String packageName; + private final ClassName entityClass; + private final ClassName builderClassName; + private final ClassName conditionalBuilderClassName; + + public QueryFactory(ProcessingEnvironment processingEnv, TypeElement entityType, Data dataAnnotation, List metadataList) { + this.processingEnv = processingEnv; + this.entityType = entityType; + this.dataAnnotation = dataAnnotation; + this.metadataList = metadataList; + this.entityName = entityType.getSimpleName().toString(); + this.queryName = entityName + "Query"; + PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(entityType); + this.packageName = packageElement.isUnnamed() ? "" : packageElement.getQualifiedName().toString(); + this.entityClass = ClassName.get(packageName, entityName); + this.builderClassName = ClassName.get(packageName, queryName, "Builder"); + this.conditionalBuilderClassName = ClassName.get(packageName, queryName, "ConditionalBuilder"); + } + + public void generateQueryBuilder() throws IOException { + TypeSpec.Builder queryBuilder = TypeSpec.classBuilder(queryName); + TypeSpec.Builder conditionalBuilderType = TypeSpec.classBuilder("ConditionalBuilder") + .superclass(ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query", "AbstractConditionalBuilder"), builderClassName, conditionalBuilderClassName, entityClass)) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addMethod(MethodSpec.constructorBuilder() + .addParameter(ParameterizedTypeName.get(ABSTRACT_QUERY_BUILDER_CLASS_NAME, builderClassName, conditionalBuilderClassName, entityClass), "queryBuilder") + .addStatement("super(queryBuilder)") + .build()); + + TypeSpec.Builder builderType = TypeSpec.classBuilder("Builder") + .superclass(ParameterizedTypeName.get(ABSTRACT_QUERY_BUILDER_CLASS_NAME, builderClassName, conditionalBuilderClassName, entityClass)) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addMethod(MethodSpec.constructorBuilder() + .addParameter(DATA_MANAGER_CLASS_NAME, "dataManager") + .addStatement("super(dataManager, $N.class)", entityClass.simpleName()) + .build()) + .addMethod(MethodSpec.methodBuilder("createInstance") + .addModifiers(Modifier.PROTECTED) + .returns(ParameterizedTypeName.get(ABSTRACT_QUERY_BUILDER_CLASS_NAME, builderClassName, conditionalBuilderClassName, entityClass)) + .addStatement("return new Builder(this.dataManager)") + .build()) + .addMethod(MethodSpec.methodBuilder("createConditionalInstance") + .addModifiers(Modifier.PROTECTED) + .returns(conditionalBuilderClassName) + .addStatement("return new ConditionalBuilder(this)") + .build()); + + for (Metadata metadata : metadataList) { + if (metadata instanceof PersistentValueMetadata persistentValueMetadata) { + if (metadata instanceof ForeignPersistentValueMetadata) { + continue; //todo: for now we dont support these queries. we will need to tho and integrate joins + } + SchemaTableColumnStatics.generateSchemaTableColumnStatics(queryBuilder, persistentValueMetadata); + makeEqualsClause(builderType, persistentValueMetadata); + makeNotEqualsClause(builderType, persistentValueMetadata); + makeInClause(builderType, persistentValueMetadata); + makeNotInClause(builderType, persistentValueMetadata); + + if (persistentValueMetadata.nullable()) { + makeNullClause(builderType, persistentValueMetadata); + makeNotNullClause(builderType, persistentValueMetadata); + } + + if (TypeName.FLOAT.box().equals(persistentValueMetadata.genericType()) //todo: support timestampts and dates + || TypeName.DOUBLE.box().equals(persistentValueMetadata.genericType()) + || TypeName.LONG.box().equals(persistentValueMetadata.genericType()) + || TypeName.SHORT.box().equals(persistentValueMetadata.genericType()) + || TypeName.BYTE.box().equals(persistentValueMetadata.genericType()) + || TypeName.INT.box().equals(persistentValueMetadata.genericType()) + ) { + makeLessThanClause(builderType, persistentValueMetadata); + makeLessThanOrEqualToClause(builderType, persistentValueMetadata); + makeGreaterThanClause(builderType, persistentValueMetadata); + makeGreaterThanOrEqualToClause(builderType, persistentValueMetadata); + makeBetweenClause(builderType, persistentValueMetadata); + } + + if (TypeName.get(String.class).equals(persistentValueMetadata.genericType())) { + makeLikeClause(builderType, persistentValueMetadata); + makeNotLikeClause(builderType, persistentValueMetadata); + } + + makeOrderByClause(builderType, persistentValueMetadata, builderClassName); + makeOrderByClause(conditionalBuilderType, persistentValueMetadata, conditionalBuilderClassName); + } + } + + TypeSpec query = queryBuilder + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addMethod(MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .build()) + .addMethod(MethodSpec.methodBuilder("where") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(ClassName.get(packageName, queryName, "Builder")) + .addParameter(DATA_MANAGER_CLASS_NAME, "dataManager") + .addStatement("return new Builder(dataManager)") + .build()) + .addMethod(MethodSpec.methodBuilder("where") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(ClassName.get(packageName, queryName, "Builder")) + .addStatement("return new Builder(DataManager.getInstance())") + .build()) + .addType(builderType.build()) + .addType(conditionalBuilderType.build()) + .build(); + + JavaFile.builder(packageName, query) + .indent(" ") + .build() + .writeTo(processingEnv.getFiler()); + } + + + private void makeNotEqualsClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotEqualsClause"), "IsNot"); + } + + private void makeEqualsClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "EqualsClause"), "Is"); + } + + private void makeInClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "InClause"), "IsIn", true); + makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "InClause"), "IsIn", false, ParameterizedTypeName.get(ClassName.get(List.class), persistentValueMetadata.genericType())); + } + + private void makeNotInClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotInClause"), "IsNotIn", true); + makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotInClause"), "IsNotIn", false, ParameterizedTypeName.get(ClassName.get(List.class), persistentValueMetadata.genericType())); + } + + private void makeLessThanClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, + ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "LessThanClause"), persistentValueMetadata.genericType()), + "IsLessThan"); + } + + private void makeLessThanOrEqualToClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, + ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "LessThanOrEqualToClause"), persistentValueMetadata.genericType()), + "IsLessThanOrEqualTo"); + } + + private void makeGreaterThanClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, + ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "GreaterThanClause"), persistentValueMetadata.genericType()), + "IsGreaterThan"); + } + + private void makeGreaterThanOrEqualToClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, + ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "GreaterThanOrEqualToClause"), persistentValueMetadata.genericType()), + "IsGreaterThanOrEqualTo"); + } + + private void makeNullClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeNonValuedClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NullClause"), "IsNull"); + } + + private void makeNotNullClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeNonValuedClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotNullClause"), "IsNotNull"); + } + + private void makeLikeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "LikeClause"), "IsLike"); + } + + private void makeNotLikeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotLikeClause"), "IsNotLike"); + } + + private void makeBetweenClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + builderType.addMethod(MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + "IsBetween") + .addModifiers(Modifier.PUBLIC) + .returns(conditionalBuilderClassName) + .addParameter(persistentValueMetadata.genericType(), "start") + .addParameter(persistentValueMetadata.genericType(), "end") + .addStatement("return set(new $T($N, $N, $N, start, end))", + ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "BetweenClause"), persistentValueMetadata.genericType()), + persistentValueMetadata.fieldName() + "$schema", + persistentValueMetadata.fieldName() + "$table", + persistentValueMetadata.fieldName() + "$column" + ) + .build()); + } + + private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix) { + makeClause(builderType, persistentValueMetadata, clauseTypeName, suffix, false); + } + + private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix, boolean varargs) { + makeClause(builderType, persistentValueMetadata, clauseTypeName, suffix, varargs, persistentValueMetadata.genericType()); + } + + private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix, boolean varargs, TypeName parameterType) { + builderType.addMethod(MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + suffix) + .addModifiers(Modifier.PUBLIC) + .returns(conditionalBuilderClassName) + .addParameter(varargs ? ArrayTypeName.of(parameterType) : parameterType, persistentValueMetadata.fieldName()) + .varargs(varargs) + .addStatement("return set(new $T($N, $N, $N, $N))", + clauseTypeName, + persistentValueMetadata.fieldName() + "$schema", + persistentValueMetadata.fieldName() + "$table", + persistentValueMetadata.fieldName() + "$column", + persistentValueMetadata.fieldName()) + .build()); + } + + private void makeNonValuedClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix) { + builderType.addMethod(MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + suffix) + .addModifiers(Modifier.PUBLIC) + .returns(conditionalBuilderClassName) + .addStatement("return set(new $T($N, $N, $N))", + clauseTypeName, + persistentValueMetadata.fieldName() + "$schema", + persistentValueMetadata.fieldName() + "$table", + persistentValueMetadata.fieldName() + "$column" + ) + .build()); + } + + private void makeOrderByClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, ClassName returnType) { + String methodName = "orderBy" + persistentValueMetadata.fieldName().substring(0, 1).toUpperCase() + persistentValueMetadata.fieldName().substring(1); + builderType.addMethod(MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC) + .returns(returnType) + .addParameter(ClassName.get("net.staticstudios.data.query", "Order"), "order") + .addStatement("orderBy($N, $N, $N, order)", + persistentValueMetadata.fieldName() + "$schema", + persistentValueMetadata.fieldName() + "$table", + persistentValueMetadata.fieldName() + "$column" + ) + .addStatement("return this") + .build()); + } +} diff --git a/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java b/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java new file mode 100644 index 00000000..74b3e66c --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java @@ -0,0 +1,28 @@ +package net.staticstudios.data.processor; + +import com.palantir.javapoet.ClassName; +import com.palantir.javapoet.FieldSpec; +import com.palantir.javapoet.TypeSpec; + +import javax.lang.model.element.Modifier; + +record SchemaTableColumnStatics(String schemaFieldName, String tableFieldName, String columnFieldName) { + public static SchemaTableColumnStatics generateSchemaTableColumnStatics(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { + String schemaFieldName = persistentValueMetadata.fieldName() + "$schema"; + String tableFieldName = persistentValueMetadata.fieldName() + "$table"; + String columnFieldName = persistentValueMetadata.fieldName() + "$column"; + + // since we support env variables in the name, parse these at runtime. + builderType.addField(FieldSpec.builder(String.class, schemaFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.schema()) + .build()); + builderType.addField(FieldSpec.builder(String.class, tableFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.table()) + .build()); + builderType.addField(FieldSpec.builder(String.class, columnFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.column()) + .build()); + + return new SchemaTableColumnStatics(schemaFieldName, tableFieldName, columnFieldName); + } +} diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 93bd9883..69971154 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -123,7 +123,7 @@ public void callUpdateHandlers(List columnNames, String schema, String t throw new IllegalArgumentException("Not all ID columns were provided for UniqueData class " + holderClass.getName() + ". Required: " + metadata.idColumns() + ", Provided: " + columnNames); } } - UniqueData instance = get(holderClass, idColumns); + UniqueData instance = getInstance(holderClass, idColumns); for (ValueUpdateHandlerWrapper wrapper : handlers) { Class dataType = wrapper.getDataType(); Object deserializedOldValue = oldSerializedValues[columnIndex]; //todo: these @@ -293,11 +293,43 @@ public synchronized void updateIdColumns(List columnNames, String schema instance.setIdColumns(newIdCols); classCache.put(newIdCols, instance); }); + } + + public List query(Class clazz, String where, List values) { + UniqueDataMetadata metadata = getMetadata(clazz); + Preconditions.checkNotNull(metadata, "UniqueData class %s has not been parsed yet", clazz.getName()); + StringBuilder sb = new StringBuilder(); + sb.append("SELECT "); + for (ColumnMetadata idColumn : metadata.idColumns()) { + sb.append("\"").append(idColumn.name()).append("\", "); + } + sb.setLength(sb.length() - 2); + sb.append(" FROM \"").append(metadata.schema()).append("\".\"").append(metadata.table()).append("\" "); + sb.append(where); + @Language("SQL") String sql = sb.toString(); + List results = new ArrayList<>(); + try (ResultSet rs = dataAccessor.executeQuery(sql, values)) { + while (rs.next()) { + ColumnValuePair[] idColumns = new ColumnValuePair[metadata.idColumns().size()]; + for (int i = 0; i < metadata.idColumns().size(); i++) { + ColumnMetadata idColumn = metadata.idColumns().get(i); + Object value = rs.getObject(idColumn.name()); + idColumns[i] = new ColumnValuePair(idColumn.name(), value); + } + T instance = getInstance(clazz, idColumns); + if (instance != null) { + results.add(instance); + } + } + return results; + } catch (SQLException e) { + throw new RuntimeException(e); + } } @SuppressWarnings("unchecked") - public T get(Class clazz, ColumnValuePair... idColumnValues) { + public T getInstance(Class clazz, ColumnValuePair... idColumnValues) { ColumnValuePairs idColumns = new ColumnValuePairs(idColumnValues); UniqueDataMetadata metadata = getMetadata(clazz); Preconditions.checkNotNull(metadata, "UniqueData class %s has not been parsed yet", clazz.getName()); @@ -506,6 +538,31 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { } } + public T get(String schema, String table, String column, ColumnValuePairs idColumns, Map 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 = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + sqlBuilder.append("\"").append(name).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())) { + Object serializedValue = null; + if (rs.next()) { + serializedValue = rs.getObject(column); + } + if (serializedValue != null) { + //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 + T deserialized = (T) serializedValue; //todo: this + return deserialized; + } + return null; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + public void set(String schema, String table, String column, ColumnValuePairs idColumns, Map idColumnLinks, Object value) { StringBuilder sqlBuilder; if (idColumnLinks.isEmpty()) { diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index 0dec64b0..af1c5c5d 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -3,11 +3,8 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.*; import net.staticstudios.data.util.*; -import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -122,13 +119,6 @@ public Class getDataType() { return dataType; } -// @Override -// public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { -// updateHandlers.add(updateHandler); -// return this; -// } - - @Override public PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler) { throw new UnsupportedOperationException("Dynamically adding update handlers is not supported"); @@ -142,26 +132,7 @@ public Map getIdColumnLinks() { @Override public T get() { Preconditions.checkArgument(!holder.isDeleted(), "Cannot get value from a deleted UniqueData instance"); - StringBuilder sqlBuilder = new StringBuilder().append("SELECT \"").append(column).append("\" FROM \"").append(schema).append("\".\"").append(table).append("\" WHERE "); - for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); - sqlBuilder.append("\"").append(name).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())) { - Object serializedValue = null; - if (rs.next()) { - serializedValue = rs.getObject(column); - } - if (serializedValue != null) { - T deserialized = (T) serializedValue; //todo: this - return deserialized; - } - return null; - } catch (SQLException e) { - throw new RuntimeException(e); - } + return holder.getDataManager().get(schema, table, column, holder.getIdColumns(), idColumnLinks, dataType); } @Override diff --git a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java index 48babc21..05234f63 100644 --- a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -111,7 +111,7 @@ public T get() { } } - return holder.getDataManager().get(type, idColumns); + return holder.getDataManager().getInstance(type, idColumns); } @Override diff --git a/src/main/java/net/staticstudios/data/insert/InsertContext.java b/src/main/java/net/staticstudios/data/insert/InsertContext.java index 2cd6f10f..154e4bae 100644 --- a/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -97,6 +97,6 @@ public T get(Class holderClass) { for (int i = 0; i < metadata.idColumns().size(); i++) { idColumnValues[i] = new ColumnValuePair(metadata.idColumns().get(i).name(), entries.get(new SimpleColumnMetadata(metadata.idColumns().get(i)))); } - return dataManager.get(holderClass, idColumnValues); + return dataManager.getInstance(holderClass, idColumnValues); } } diff --git a/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java b/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java new file mode 100644 index 00000000..6960202e --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java @@ -0,0 +1,100 @@ +package net.staticstudios.data.query; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.query.clause.AndClause; +import net.staticstudios.data.query.clause.OrClause; +import net.staticstudios.data.query.clause.ParenthesisClause; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Consumer; + +public abstract class AbstractConditionalBuilder, C extends AbstractConditionalBuilder, T extends UniqueData> implements QueryLike { + private final AbstractQueryBuilder queryBuilder; + + public AbstractConditionalBuilder(AbstractQueryBuilder queryBuilder) { + this.queryBuilder = queryBuilder; + } + + /** + * AND ... + */ + public Q and() { + queryBuilder.state = AbstractQueryBuilder.State.AND; + return queryBuilder.self(); + } + + /** + * OR ... + */ + public Q or() { + queryBuilder.state = AbstractQueryBuilder.State.OR; + return queryBuilder.self(); + } + + /** + * OR (...) + */ + public Q or(Consumer conditional) { + AbstractQueryBuilder inner = queryBuilder.createInstance(); + inner.temp = true; + conditional.accept(inner.self()); + Preconditions.checkState(inner.state == AbstractQueryBuilder.State.NONE, "Clause not completed"); + + queryBuilder.set(new OrClause(queryBuilder.clause, new ParenthesisClause(inner.clause))); + + return queryBuilder.self(); + } + + /** + * AND (...) + */ + public Q and(Consumer conditional) { + AbstractQueryBuilder inner = queryBuilder.createInstance(); + inner.temp = true; + conditional.accept(inner.self()); + Preconditions.checkState(inner.state == AbstractQueryBuilder.State.NONE, "Clause not completed"); + + queryBuilder.set(new AndClause(queryBuilder.clause, new ParenthesisClause(inner.clause))); + + return queryBuilder.self(); + } + + @SuppressWarnings("unchecked") + private C self() { + return (C) this; + } + + @Override + public C limit(int limit) { + queryBuilder.limit(limit); + return self(); + } + + @Override + public C offset(int offset) { + queryBuilder.offset(offset); + return self(); + } + + @Override + public @Nullable T findOne() { + return queryBuilder.findOne(); + } + + @Override + public @NotNull List findAll() { + return queryBuilder.findAll(); + } + + protected void orderBy(String schema, String table, String column, Order order) { + queryBuilder.orderBy(schema, table, column, order); + } + + @Override + public String toString() { + return queryBuilder.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java b/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java new file mode 100644 index 00000000..602237a8 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java @@ -0,0 +1,142 @@ +package net.staticstudios.data.query; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.query.clause.AndClause; +import net.staticstudios.data.query.clause.Clause; +import net.staticstudios.data.query.clause.OrClause; +import net.staticstudios.data.query.clause.ValueClause; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public abstract class AbstractQueryBuilder, + C extends AbstractConditionalBuilder, + T extends UniqueData> + implements QueryLike { + protected final DataManager dataManager; + private final Class type; + protected boolean temp = false; + protected State state = State.NONE; + protected Clause clause = null; + private int limit = -1; + private int offset = -1; + private String orderBySchema; + private String orderByTable; + private String orderByColumn; + private Order order = Order.ASCENDING; + + protected AbstractQueryBuilder(DataManager dataManager, Class type) { + this.dataManager = dataManager; + this.type = type; + } + + public Q and() { + state = State.AND; + return self(); + } + + public Q or() { + state = State.OR; + return self(); + } + + private ComputedClause compute(int limit, int offset) { + Preconditions.checkState(!temp, "Cannot call compute on a temporary query builder"); + Preconditions.checkNotNull(clause, "No clause defined"); + StringBuilder sb = new StringBuilder("WHERE "); + List parameters = clause.append(sb); + if (limit > 0) { + sb.append(" LIMIT ").append(limit); + } + if (offset > 0) { + sb.append(" OFFSET ").append(offset); + } + if (orderByColumn != null) { + sb.append(" ORDER BY \"").append(orderBySchema).append("\".\"").append(orderByTable).append("\".\"").append(orderByColumn).append("\" ").append(order == Order.ASCENDING ? "ASC" : "DESC"); + } + return new ComputedClause(sb.toString(), parameters); + } + + public String toString() { + return compute(limit, offset).sql(); + } + + protected void orderBy(String schema, String table, String column, Order order) { + this.orderBySchema = schema; + this.orderByTable = table; + this.orderByColumn = column; + this.order = order; + } + + @SuppressWarnings("unchecked") + protected Q self() { + return (Q) this; + } + + @Override + public @Nullable T findOne() { + ComputedClause computed = compute(1, -1); + List result = dataManager.query(type, computed.sql(), computed.parameters()); + if (result.isEmpty()) { + return null; + } + return result.getFirst(); + } + + @Override + public @NotNull List findAll() { + ComputedClause computed = compute(limit, offset); + return dataManager.query(type, computed.sql(), computed.parameters()); + } + + @Override + public Q limit(int limit) { + this.limit = limit; + return self(); + } + + @Override + public Q offset(int offset) { + this.offset = offset; + return self(); + } + + + protected C set(Clause clause) { + switch (state) { + case NONE -> this.clause = clause; + case AND -> { + if (!(clause instanceof ValueClause valueClause)) { + throw new IllegalStateException("AND clause must be a ValueClause"); + } + this.clause = new AndClause(this.clause, valueClause); + } + case OR -> { + if (!(clause instanceof ValueClause valueClause)) { + throw new IllegalStateException("OR clause must be a ValueClause"); + } + this.clause = new OrClause(this.clause, valueClause); + } + } + + state = State.NONE; + return createConditionalInstance(); + } + + protected abstract AbstractQueryBuilder createInstance(); + + protected abstract C createConditionalInstance(); + + protected enum State { + NONE, + AND, + OR + } + + record ComputedClause(String sql, List parameters) { + } + +} diff --git a/src/main/java/net/staticstudios/data/query/Order.java b/src/main/java/net/staticstudios/data/query/Order.java new file mode 100644 index 00000000..b3318c7b --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/Order.java @@ -0,0 +1,6 @@ +package net.staticstudios.data.query; + +public enum Order { + ASCENDING, + DESCENDING +} diff --git a/src/main/java/net/staticstudios/data/query/Query.java b/src/main/java/net/staticstudios/data/query/Query.java new file mode 100644 index 00000000..db5c1a9e --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/Query.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.query; + +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.query.clause.Clause; + +public class Query { + private final Clause clause; + + protected Query(Clause clause) { + this.clause = clause; + } +} diff --git a/src/main/java/net/staticstudios/data/query/QueryLike.java b/src/main/java/net/staticstudios/data/query/QueryLike.java new file mode 100644 index 00000000..31bc5a87 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/QueryLike.java @@ -0,0 +1,40 @@ +package net.staticstudios.data.query; + +import net.staticstudios.data.UniqueData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +interface QueryLike { + /** + * Sets the maximum number of results to return. + * + * @param limit the maximum number of results to return + * @return the current query builder + */ + QueryLike limit(int limit); + + /** + * Sets the offset of the first result to return. + * + * @param offset the offset of the first result to return + * @return the current query builder + */ + QueryLike offset(int offset); + + /** + * Find one result. + * + * @return the found result, or null if none found + */ + @Nullable T findOne(); + + /** + * Find all results. + * This will respect the limit and offset set. + * + * @return the list of found results + */ + @NotNull List findAll(); +} diff --git a/src/main/java/net/staticstudios/data/query/clause/AndClause.java b/src/main/java/net/staticstudios/data/query/clause/AndClause.java new file mode 100644 index 00000000..aec87c71 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/AndClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.ArrayList; +import java.util.List; + +public class AndClause implements CompositeClause { + private final Clause left; + private final Clause right; + + public AndClause(Clause left, Clause right) { + this.left = left; + this.right = right; + } + + @Override + public List append(StringBuilder sb) { + List values = new ArrayList<>(left.append(sb)); + sb.append(" AND "); + values.addAll(right.append(sb)); + + return values; + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java b/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java new file mode 100644 index 00000000..863c22dc --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java @@ -0,0 +1,25 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class BetweenClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final N min; + private final N max; + + public BetweenClause(String schema, String table, String column, N min, N max) { + this.schema = schema; + this.table = table; + this.column = column; + this.min = min; + this.max = max; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" BETWEEN ? AND ?"); + return List.of(min, max); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/Clause.java b/src/main/java/net/staticstudios/data/query/clause/Clause.java new file mode 100644 index 00000000..91b2a950 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/Clause.java @@ -0,0 +1,8 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public interface Clause { + + List append(StringBuilder sb); +} diff --git a/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java b/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java new file mode 100644 index 00000000..a7eb50cc --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.query.clause; + +public interface CompositeClause extends Clause { +} diff --git a/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java b/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java new file mode 100644 index 00000000..652dd2e5 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class EqualsClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final Object value; + + public EqualsClause(String schema, String table, String column, Object value) { + this.schema = schema; + this.table = table; + this.column = column; + this.value = value; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" = ?"); + return List.of(value); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java b/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java new file mode 100644 index 00000000..cb57498c --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class GreaterThanClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final N value; + + public GreaterThanClause(String schema, String table, String column, N value) { + this.schema = schema; + this.table = table; + this.column = column; + this.value = value; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" > ?"); + return List.of(value); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java b/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java new file mode 100644 index 00000000..940f2f13 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class GreaterThanOrEqualToClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final N value; + + public GreaterThanOrEqualToClause(String schema, String table, String column, N value) { + this.schema = schema; + this.table = table; + this.column = column; + this.value = value; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" >= ?"); + return List.of(value); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/InClause.java b/src/main/java/net/staticstudios/data/query/clause/InClause.java new file mode 100644 index 00000000..64dd75f2 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/InClause.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class InClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final Object[] values; + + public InClause(String schema, String table, String column, Object[] values) { + this.schema = schema; + this.table = table; + this.column = column; + this.values = values; + } + + public InClause(String schema, String table, String column, List values) { + this(schema, table, column, values.toArray()); + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" IN ("); + for (int i = 0; i < values.length; i++) { + sb.append("?"); + if (i < values.length - 1) { + sb.append(", "); + } + } + sb.append(")"); + return List.of(values); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java b/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java new file mode 100644 index 00000000..22221bc4 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class LessThanClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final N value; + + public LessThanClause(String schema, String table, String column, N value) { + this.schema = schema; + this.table = table; + this.column = column; + this.value = value; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" < ?"); + return List.of(value); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java b/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java new file mode 100644 index 00000000..3a9546f8 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class LessThanOrEqualToClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final N value; + + public LessThanOrEqualToClause(String schema, String table, String column, N value) { + this.schema = schema; + this.table = table; + this.column = column; + this.value = value; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" <= ?"); + return List.of(value); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/LikeClause.java b/src/main/java/net/staticstudios/data/query/clause/LikeClause.java new file mode 100644 index 00000000..f849dd2c --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/LikeClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class LikeClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final String format; + + public LikeClause(String schema, String table, String column, String format) { + this.schema = schema; + this.table = table; + this.column = column; + this.format = format; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" LIKE ?"); + return List.of(format); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java b/src/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java new file mode 100644 index 00000000..507423e3 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class NotEqualsClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final Object value; + + public NotEqualsClause(String schema, String table, String column, Object value) { + this.schema = schema; + this.table = table; + this.column = column; + this.value = value; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" <> ?"); + return List.of(value); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/NotInClause.java b/src/main/java/net/staticstudios/data/query/clause/NotInClause.java new file mode 100644 index 00000000..c15474aa --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/NotInClause.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class NotInClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final Object[] values; + + public NotInClause(String schema, String table, String column, Object[] values) { + this.schema = schema; + this.table = table; + this.column = column; + this.values = values; + } + + public NotInClause(String schema, String table, String column, List values) { + this(schema, table, column, values.toArray()); + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" NOT IN ("); + for (int i = 0; i < values.length; i++) { + sb.append("?"); + if (i < values.length - 1) { + sb.append(", "); + } + } + sb.append(")"); + return List.of(values); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java b/src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java new file mode 100644 index 00000000..8914e818 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class NotLikeClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final String format; + + public NotLikeClause(String schema, String table, String column, String format) { + this.schema = schema; + this.table = table; + this.column = column; + this.format = format; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" NOT LIKE ?"); + return List.of(format); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/NotNullClause.java b/src/main/java/net/staticstudios/data/query/clause/NotNullClause.java new file mode 100644 index 00000000..8f09fae2 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/NotNullClause.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class NotNullClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + + public NotNullClause(String schema, String table, String column) { + this.schema = schema; + this.table = table; + this.column = column; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" IS NOT NULL"); + return List.of(); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/NullClause.java b/src/main/java/net/staticstudios/data/query/clause/NullClause.java new file mode 100644 index 00000000..19f118b6 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/NullClause.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class NullClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + + public NullClause(String schema, String table, String column) { + this.schema = schema; + this.table = table; + this.column = column; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" IS NULL"); + return List.of(); + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/OrClause.java b/src/main/java/net/staticstudios/data/query/clause/OrClause.java new file mode 100644 index 00000000..06c45a5a --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/OrClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.ArrayList; +import java.util.List; + +public class OrClause implements CompositeClause { + private final Clause left; + private final Clause right; + + public OrClause(Clause left, Clause right) { + this.left = left; + this.right = right; + } + + @Override + public List append(StringBuilder sb) { + List values = new ArrayList<>(left.append(sb)); + sb.append(" OR "); + values.addAll(right.append(sb)); + + return values; + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java b/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java new file mode 100644 index 00000000..718b7c9d --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java @@ -0,0 +1,19 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class ParenthesisClause implements ValueClause { + private final Clause inner; + + public ParenthesisClause(Clause clause) { + this.inner = clause; + } + + @Override + public List append(StringBuilder sb) { + sb.append("("); + List values = inner.append(sb); + sb.append(")"); + return values; + } +} diff --git a/src/main/java/net/staticstudios/data/query/clause/ValueClause.java b/src/main/java/net/staticstudios/data/query/clause/ValueClause.java new file mode 100644 index 00000000..0cc3eef5 --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/clause/ValueClause.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.query.clause; + +public interface ValueClause extends Clause { +} diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 95a8080b..bd1fcc98 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -4,14 +4,14 @@ import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.mock.MockUser; import net.staticstudios.data.mock.MockUserFactory; +import net.staticstudios.data.mock.MockUserQuery; import net.staticstudios.data.mock.MockUserSettings; +import net.staticstudios.data.query.Order; import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Test; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -36,15 +36,8 @@ public void testReadData() throws SQLException { .insert(InsertMode.SYNC); } - try (Statement statement = getH2Connection(dataManager).createStatement()) { - ResultSet rs = statement.executeQuery("SCRIPT"); - while (rs.next()) { - System.out.println(rs.getString(1)); - } - } - for (UUID id : userIds) { - MockUser user = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); + MockUser user = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); assertEquals("user " + id, user.name.get()); assertNull(user.age.get()); } @@ -55,7 +48,7 @@ public void testReadData() throws SQLException { DataManager dataManager2 = environment2.dataManager(); dataManager2.load(MockUser.class); for (UUID id : userIds) { - MockUser user = dataManager2.get(MockUser.class, ColumnValuePair.of("id", id)); + MockUser user = dataManager2.getInstance(MockUser.class, ColumnValuePair.of("id", id)); assertEquals("user " + id, user.name.get()); assertNull(user.age.get()); } @@ -79,10 +72,10 @@ public void test() throws SQLException { mockUser.age.set(25); assertEquals(25, mockUser.age.get()); - mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); + mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); mockUser = null; // remove strong reference System.gc(); - mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss + mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss assertNull(mockUser.favoriteColor.get()); mockUser.favoriteColor.set("blue"); @@ -131,7 +124,7 @@ public void testUpdateHandlerRegistration() { assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); mockUser = null; // remove strong reference System.gc(); - mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss + mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss //the handler for this pv should not have been registered again assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); waitForUpdateHandlers(); @@ -204,7 +197,7 @@ public void testReceiveInsertFromPostgres() { waitForDataPropagation(); - MockUser mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); //todo: i want a type safe version of this. query builder? + MockUser mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); //todo: i want a type safe version of this. query builder? assertEquals("inserted from pg", mockUser.name.get()); assertEquals(0, mockUser.getNameUpdates()); @@ -237,7 +230,7 @@ public void testReceiveDeleteFromPostgres() { assertTrue(mockUser.isDeleted()); - mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); + mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); assertNull(mockUser); } @@ -261,9 +254,9 @@ public void testChangeIdColumn() { UUID newId = UUID.randomUUID(); mockUser.id.set(newId); assertEquals(newId, mockUser.id.get()); - assertNull(dataManager.get(MockUser.class, ColumnValuePair.of("id", id))); - assertNotNull(dataManager.get(MockUser.class, ColumnValuePair.of("id", newId))); - assertSame(dataManager.get(MockUser.class, ColumnValuePair.of("id", newId)), mockUser); + assertNull(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id))); + assertNotNull(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", newId))); + assertSame(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", newId)), mockUser); assertEquals(1, mockUser.nameUpdates.get()); mockUser.name.set("new name2"); waitForUpdateHandlers(); @@ -302,13 +295,101 @@ public void testChangeIdColumnInPostgres() { waitForDataPropagation(); assertEquals(newId, mockUser.id.get()); - assertNull(dataManager.get(MockUser.class, ColumnValuePair.of("id", id))); - assertNotNull(dataManager.get(MockUser.class, ColumnValuePair.of("id", newId))); - assertSame(dataManager.get(MockUser.class, ColumnValuePair.of("id", newId)), mockUser); + assertNull(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id))); + assertNotNull(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", newId))); + assertSame(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", newId)), mockUser); waitForUpdateHandlers(); assertEquals(1, mockUser.nameUpdates.get()); mockUser.name.set("new name2"); waitForUpdateHandlers(); assertEquals(2, mockUser.nameUpdates.get()); } + + @Test + public void testFindOneEquals() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser original = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); + + MockUser got = MockUserQuery.where(dataManager) + .idIs(id) + .findOne(); + assertSame(original, got); + } + + @Test + public void testFindAllLike() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + MockUser original1 = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .age(0) + .insert(InsertMode.SYNC); + MockUser original2 = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user2") + .age(5) + .insert(InsertMode.SYNC); + + List got = MockUserQuery.where(dataManager) + .nameIsLike("%test user%") + .orderByAge(Order.ASCENDING) + .findAll(); + + assertEquals(2, got.size()); + assertTrue(got.contains(original1)); + assertTrue(got.contains(original2)); + + assertSame(original1, got.get(0)); + assertSame(original2, got.get(1)); + + got = MockUserQuery.where(dataManager) + .nameIsLike("%test user%") + .orderByAge(Order.DESCENDING) + .findAll(); + + assertEquals(2, got.size()); + assertTrue(got.contains(original1)); + assertTrue(got.contains(original2)); + + assertSame(original2, got.get(0)); + assertSame(original1, got.get(1)); + } + + @Test + public void testQuery() { //todo: test each clause and ensure it outputs the proper sql + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + +// String where = MockUserQuery.where(dataManager) +// .idIs(UUID.randomUUID()) +// .or() +// .nameIs("user name") +// .or(q -> q.ageIsLessThan(10) +// .and() +// .ageIsLessThanOrEqualTo(5) +// ) +// .or() +// .nameIsIn("name1", "name2", "name3") +// .or() +// .nameIsIn(List.of("name4", "name5", "name6")) +// .limit(10) +// .offset(2) +// .and() +// .ageIsBetween(0, 3) +// .and() +// .ageIsNotNull() +// .and() +// .nameIsLike("%something%") +// .and() +// .nameIsNotLike("%nothing%") +// .orderByAge(Order.ASCENDING) +// .toString(); +// System.out.println(where); + } } \ No newline at end of file From f9d38ece069847cc72c219473c4de72cf29f6869 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Fri, 19 Sep 2025 19:21:31 -0400 Subject: [PATCH 10/75] support for querying on foreign columns --- .../data/processor/DataProcessor.java | 28 ++------ .../data/processor/ForeignLink.java | 4 ++ .../data/processor/MetadataUtils.java | 29 ++++++++ .../data/processor/QueryFactory.java | 68 ++++++++++++++++--- .../processor/SchemaTableColumnStatics.java | 2 +- .../net/staticstudios/data/DataManager.java | 3 +- .../data/query/AbstractQueryBuilder.java | 20 +++++- .../staticstudios/data/query/InnerJoin.java | 5 ++ .../data/PersistentValueTest.java | 51 ++++++++++++-- .../net/staticstudios/data/mock/MockUser.java | 3 +- 10 files changed, 169 insertions(+), 44 deletions(-) create mode 100644 processor/src/main/java/net/staticstudios/data/processor/ForeignLink.java create mode 100644 src/main/java/net/staticstudios/data/query/InnerJoin.java diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index eb7d95fb..c7fbf23f 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -15,7 +15,6 @@ import javax.tools.Diagnostic; import java.io.IOException; import java.util.List; -import java.util.Map; import java.util.Set; @SupportedAnnotationTypes("net.staticstudios.data.Data") @@ -100,36 +99,17 @@ private void generateFactory(TypeElement entityType, Data dataAnnotation, List link : foreignPersistentValueMetadata.links().entrySet()) { - String localColumn = link.getKey(); - String foreignColumn = link.getValue(); - - PersistentValueMetadata localColumnMetadata = metadataList.stream() - .filter(m -> m instanceof PersistentValueMetadata) - .map(m -> (PersistentValueMetadata) m) - .filter(m -> m.column().equals(localColumn) && m.table().equals(dataAnnotation.table()) && m.schema().equals(dataAnnotation.schema())) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Could not find local column metadata for link: " + localColumn)); - - String columnLinkFieldName = statics.columnFieldName() + "$" + localColumnMetadata.fieldName() + "$" + i; - - builderType.addField(FieldSpec.builder(String.class, columnLinkFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), foreignColumn) - .build()); - + if (persistentValueMetadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { + for (ForeignLink link : MetadataUtils.makeFPVStatics(builderType, foreignPersistentValueMetadata, metadataList, dataAnnotation, statics)) { insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N)", statics.schemaFieldName(), statics.tableFieldName(), - columnLinkFieldName, - localColumnMetadata.fieldName()); - i++; + link.foreignColumnFieldName(), + link.localColumnMetadata().fieldName()); } insertCtxMethod.endControlFlow(); } - } } diff --git a/processor/src/main/java/net/staticstudios/data/processor/ForeignLink.java b/processor/src/main/java/net/staticstudios/data/processor/ForeignLink.java new file mode 100644 index 00000000..daade031 --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/processor/ForeignLink.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.processor; + +public record ForeignLink(String foreignColumnFieldName, PersistentValueMetadata localColumnMetadata) { +} diff --git a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java index 44366786..fc4b813b 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java +++ b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java @@ -1,6 +1,9 @@ package net.staticstudios.data.processor; +import com.palantir.javapoet.ClassName; +import com.palantir.javapoet.FieldSpec; import com.palantir.javapoet.TypeName; +import com.palantir.javapoet.TypeSpec; import net.staticstudios.data.Column; import net.staticstudios.data.Data; import net.staticstudios.data.ForeignColumn; @@ -117,4 +120,30 @@ private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnot throw new IllegalStateException("Field " + field.getSimpleName() + " is not annotated with @IdColumn, @Column, or @ForeignColumn"); } + public static List makeFPVStatics(TypeSpec.Builder builder, ForeignPersistentValueMetadata foreignPersistentValueMetadata, List metadataList, Data dataAnnotation, SchemaTableColumnStatics statics) { + List staticNames = new ArrayList<>(); + int i = 0; + for (Map.Entry link : foreignPersistentValueMetadata.links().entrySet()) { + String localColumn = link.getKey(); + String foreignColumn = link.getValue(); + + PersistentValueMetadata localColumnMetadata = metadataList.stream() + .filter(m -> m instanceof PersistentValueMetadata) + .map(m -> (PersistentValueMetadata) m) + .filter(m -> m.column().equals(localColumn) && m.table().equals(dataAnnotation.table()) && m.schema().equals(dataAnnotation.schema())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find local column metadata for link: " + localColumn)); + + String columnLinkFieldName = statics.columnFieldName() + "$" + localColumnMetadata.fieldName() + "$" + i; + + builder.addField(FieldSpec.builder(String.class, columnLinkFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), foreignColumn) + .build()); + + staticNames.add(new ForeignLink(columnLinkFieldName, localColumnMetadata)); + i++; + } + return staticNames; + } + } diff --git a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java index c17f130d..641db45c 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java +++ b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java @@ -8,7 +8,10 @@ import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; public class QueryFactory { private static final ClassName DATA_MANAGER_CLASS_NAME = ClassName.get("net.staticstudios.data", "DataManager"); @@ -23,6 +26,8 @@ public class QueryFactory { private final ClassName entityClass; private final ClassName builderClassName; private final ClassName conditionalBuilderClassName; + private final Map> foreignPersistentValueLinkFieldNames = new HashMap<>(); + private final Map persistentValueStatics = new HashMap<>(); public QueryFactory(ProcessingEnvironment processingEnv, TypeElement entityType, Data dataAnnotation, List metadataList) { this.processingEnv = processingEnv; @@ -40,6 +45,20 @@ public QueryFactory(ProcessingEnvironment processingEnv, TypeElement entityType, public void generateQueryBuilder() throws IOException { TypeSpec.Builder queryBuilder = TypeSpec.classBuilder(queryName); + + foreignPersistentValueLinkFieldNames.clear(); + for (Metadata metadata : metadataList) { + if (!(metadata instanceof PersistentValueMetadata persistentValueMetadata)) { + continue; + } + SchemaTableColumnStatics statics = SchemaTableColumnStatics.generateSchemaTableColumnStatics(queryBuilder, persistentValueMetadata); + persistentValueStatics.put(persistentValueMetadata, statics); + if (metadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { + foreignPersistentValueLinkFieldNames.put(foreignPersistentValueMetadata, MetadataUtils.makeFPVStatics(queryBuilder, foreignPersistentValueMetadata, metadataList, dataAnnotation, statics)); + } + } + + TypeSpec.Builder conditionalBuilderType = TypeSpec.classBuilder("ConditionalBuilder") .superclass(ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query", "AbstractConditionalBuilder"), builderClassName, conditionalBuilderClassName, entityClass)) .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) @@ -68,10 +87,6 @@ public void generateQueryBuilder() throws IOException { for (Metadata metadata : metadataList) { if (metadata instanceof PersistentValueMetadata persistentValueMetadata) { - if (metadata instanceof ForeignPersistentValueMetadata) { - continue; //todo: for now we dont support these queries. we will need to tho and integrate joins - } - SchemaTableColumnStatics.generateSchemaTableColumnStatics(queryBuilder, persistentValueMetadata); makeEqualsClause(builderType, persistentValueMetadata); makeNotEqualsClause(builderType, persistentValueMetadata); makeInClause(builderType, persistentValueMetadata); @@ -215,18 +230,53 @@ private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata pe } private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix, boolean varargs, TypeName parameterType) { - builderType.addMethod(MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + suffix) + MethodSpec.Builder builder = MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + suffix) .addModifiers(Modifier.PUBLIC) .returns(conditionalBuilderClassName) - .addParameter(varargs ? ArrayTypeName.of(parameterType) : parameterType, persistentValueMetadata.fieldName()) - .varargs(varargs) + .addParameter(varargs ? ArrayTypeName.of(parameterType) : parameterType, persistentValueMetadata.fieldName()); + + if (persistentValueMetadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { + AtomicReference localStatics = new AtomicReference<>(); + String[] columns = foreignPersistentValueMetadata.links().keySet().stream() + .map(column -> metadataList.stream() + .filter(m -> m instanceof PersistentValueMetadata) + .map(m -> (PersistentValueMetadata) m) + .filter(m -> m.column().equals(column) && m.table().equals(dataAnnotation.table()) && m.schema().equals(dataAnnotation.schema())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find local column metadata for link: " + column))) + .peek(m -> { + if (localStatics.get() == null) { + localStatics.set(persistentValueStatics.get(m)); + } + }) + .map(m -> persistentValueStatics.get(m).columnFieldName()) + .toArray(String[]::new); + String[] foreignColumns = foreignPersistentValueLinkFieldNames.get(foreignPersistentValueMetadata).stream() + .map(ForeignLink::foreignColumnFieldName) + .toArray(String[]::new); + + SchemaTableColumnStatics foreignStatics = persistentValueStatics.get(persistentValueMetadata); + + builder.addStatement("super.innerJoin($N, $N, $L, $N, $N, $L)", + localStatics.get().schemaFieldName(), + localStatics.get().tableFieldName(), + CodeBlock.of("new String[]{ $L }", String.join(", ", columns)), + foreignStatics.schemaFieldName(), + foreignStatics.tableFieldName(), + CodeBlock.of("new String[]{ $L }", String.join(", ", foreignColumns)) + ); + } + + builder.varargs(varargs) .addStatement("return set(new $T($N, $N, $N, $N))", clauseTypeName, persistentValueMetadata.fieldName() + "$schema", persistentValueMetadata.fieldName() + "$table", persistentValueMetadata.fieldName() + "$column", - persistentValueMetadata.fieldName()) - .build()); + persistentValueMetadata.fieldName()); + + + builderType.addMethod(builder.build()); } private void makeNonValuedClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix) { diff --git a/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java b/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java index 74b3e66c..cef32120 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java +++ b/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java @@ -6,7 +6,7 @@ import javax.lang.model.element.Modifier; -record SchemaTableColumnStatics(String schemaFieldName, String tableFieldName, String columnFieldName) { +public record SchemaTableColumnStatics(String schemaFieldName, String tableFieldName, String columnFieldName) { public static SchemaTableColumnStatics generateSchemaTableColumnStatics(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { String schemaFieldName = persistentValueMetadata.fieldName() + "$schema"; String tableFieldName = persistentValueMetadata.fieldName() + "$table"; diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 69971154..c7a13b90 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -34,10 +34,9 @@ public class DataManager { private final SQLBuilder sqlBuilder; private final TaskQueue taskQueue; private final ConcurrentHashMap, UniqueDataMetadata> uniqueDataMetadataMap = new ConcurrentHashMap<>(); - private final ConcurrentHashMap, Map> uniqueDataInstanceCache = new ConcurrentHashMap<>(); //todo: weak reference map + private final ConcurrentHashMap, Map> uniqueDataInstanceCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap, List>>> updateHandlers = new ConcurrentHashMap<>(); private final PostgresListener postgresListener; - //todo: use the class as a key since we will need to pass the instance as a param on each update private final Set registeredUpdateHandlersForColumns = Collections.synchronizedSet(new HashSet<>()); public DataManager(DataSourceConfig dataSourceConfig) { diff --git a/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java b/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java index 602237a8..67ef0c8c 100644 --- a/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java +++ b/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java @@ -10,7 +10,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.HashSet; import java.util.List; +import java.util.Set; public abstract class AbstractQueryBuilder, C extends AbstractConditionalBuilder, @@ -27,6 +29,7 @@ public abstract class AbstractQueryBuilder innerJoins = new HashSet<>(); protected AbstractQueryBuilder(DataManager dataManager, Class type) { this.dataManager = dataManager; @@ -46,7 +49,19 @@ public Q or() { private ComputedClause compute(int limit, int offset) { Preconditions.checkState(!temp, "Cannot call compute on a temporary query builder"); Preconditions.checkNotNull(clause, "No clause defined"); - StringBuilder sb = new StringBuilder("WHERE "); + StringBuilder sb = new StringBuilder(); + for (InnerJoin join : innerJoins) { + sb.append("INNER JOIN \"").append(join.foreignSchema()).append("\".\"").append(join.foreignTable()).append("\" ON "); + for (int i = 0; i < join.columns().length; i++) { + sb.append("\"").append(join.schema()).append("\".\"").append(join.table()).append("\".\"").append(join.columns()[i]).append("\" = \"") + .append(join.foreignSchema()).append("\".\"").append(join.foreignTable()).append("\".\"").append(join.foreignColumns()[i]).append("\""); + if (i < join.columns().length - 1) { + sb.append(" AND "); + } + } + sb.append(" "); + } + sb.append("WHERE "); List parameters = clause.append(sb); if (limit > 0) { sb.append(" LIMIT ").append(limit); @@ -104,6 +119,9 @@ public Q offset(int offset) { return self(); } + protected void innerJoin(String schema, String table, String[] columns, String foreignSchema, String foreignTable, String[] foreignColumns) { + innerJoins.add(new InnerJoin(schema, table, columns, foreignSchema, foreignTable, foreignColumns)); + } protected C set(Clause clause) { switch (state) { diff --git a/src/main/java/net/staticstudios/data/query/InnerJoin.java b/src/main/java/net/staticstudios/data/query/InnerJoin.java new file mode 100644 index 00000000..4a6d4e5e --- /dev/null +++ b/src/main/java/net/staticstudios/data/query/InnerJoin.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.query; + +public record InnerJoin(String schema, String table, String[] columns, String foreignSchema, String foreignTable, + String[] foreignColumns) { +} diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index bd1fcc98..057de003 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -197,7 +197,7 @@ public void testReceiveInsertFromPostgres() { waitForDataPropagation(); - MockUser mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); //todo: i want a type safe version of this. query builder? + MockUser mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); assertEquals("inserted from pg", mockUser.name.get()); assertEquals(0, mockUser.getNameUpdates()); @@ -362,13 +362,50 @@ public void testFindAllLike() { assertSame(original1, got.get(1)); } + @Test + public void testQueryOnForeignColumn() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + MockUser likesRed = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("Likes Red") + .favoriteColor("red") + .insert(InsertMode.SYNC); + MockUser likesGreen = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("Likes Green") + .favoriteColor("green") + .insert(InsertMode.SYNC); + + assertNull(MockUserQuery.where(dataManager) + .favoriteColorIs("blue") + .findOne()); + + assertSame(likesRed, MockUserQuery.where(dataManager) + .favoriteColorIs("red") + .findOne()); + + assertSame(likesGreen, MockUserQuery.where(dataManager) + .favoriteColorIs("green") + .findOne()); + + List users = MockUserQuery.where(dataManager) + .favoriteColorIsIn("red", "green") + .orderByName(Order.ASCENDING) + .findAll(); + assertEquals(2, users.size()); + assertSame(likesGreen, users.get(0)); + assertSame(likesRed, users.get(1)); + } + @Test public void testQuery() { //todo: test each clause and ensure it outputs the proper sql DataManager dataManager = getMockEnvironments().getFirst().dataManager(); -// String where = MockUserQuery.where(dataManager) -// .idIs(UUID.randomUUID()) -// .or() + String where = MockUserQuery.where(dataManager) + .idIs(UUID.randomUUID()) + .or() // .nameIs("user name") // .or(q -> q.ageIsLessThan(10) // .and() @@ -389,7 +426,9 @@ public void testQuery() { //todo: test each clause and ensure it outputs the pro // .and() // .nameIsNotLike("%nothing%") // .orderByAge(Order.ASCENDING) -// .toString(); -// System.out.println(where); +// .and() + .nameUpdatesIs(4) + .toString(); + System.out.println(where); } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/MockUser.java index eb69be86..214d27ae 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/MockUser.java @@ -20,7 +20,8 @@ public class MockUser extends UniqueData { public PersistentValue age; @ForeignColumn(name = "fav_color", table = "user_preferences", nullable = true, link = "id=user_id") public PersistentValue favoriteColor; - @OneToOne(link = "settings_id=user_id") //todo: this should generate an fkey + @OneToOne(link = "settings_id=user_id") + //todo: this should generate an fkey (add an option to control what happens on update/delete) public Reference settings; @ForeignColumn(name = "name_updates", table = "user_metadata", link = "id=user_id", defaultValue = "0") public PersistentValue nameUpdates; From 7b56e68dfee32d8934a9e381acfc40ebf633b720 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Fri, 19 Sep 2025 19:40:36 -0400 Subject: [PATCH 11/75] dont expose innerJoin(... --- .../data/processor/QueryFactory.java | 90 +++++++++++-------- .../query/AbstractConditionalBuilder.java | 6 +- .../staticstudios/data/query/InnerJoin.java | 21 +++++ .../data/PersistentValueTest.java | 8 ++ 4 files changed, 85 insertions(+), 40 deletions(-) diff --git a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java index 641db45c..f3495255 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java +++ b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java @@ -235,6 +235,57 @@ private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata pe .returns(conditionalBuilderClassName) .addParameter(varargs ? ArrayTypeName.of(parameterType) : parameterType, persistentValueMetadata.fieldName()); + handleForeignPersistentValue(builder, persistentValueMetadata); + + builder.varargs(varargs) + .addStatement("return set(new $T($N, $N, $N, $N))", + clauseTypeName, + persistentValueMetadata.fieldName() + "$schema", + persistentValueMetadata.fieldName() + "$table", + persistentValueMetadata.fieldName() + "$column", + persistentValueMetadata.fieldName()); + + + builderType.addMethod(builder.build()); + } + + private void makeNonValuedClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix) { + MethodSpec.Builder builder = MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + suffix) + .addModifiers(Modifier.PUBLIC) + .returns(conditionalBuilderClassName); + + handleForeignPersistentValue(builder, persistentValueMetadata); + + builder.addStatement("return set(new $T($N, $N, $N))", + clauseTypeName, + persistentValueMetadata.fieldName() + "$schema", + persistentValueMetadata.fieldName() + "$table", + persistentValueMetadata.fieldName() + "$column" + ); + + builderType.addMethod(builder.build()); + } + + private void makeOrderByClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, ClassName returnType) { + String methodName = "orderBy" + persistentValueMetadata.fieldName().substring(0, 1).toUpperCase() + persistentValueMetadata.fieldName().substring(1); + MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC) + .returns(returnType) + .addParameter(ClassName.get("net.staticstudios.data.query", "Order"), "order"); + + handleForeignPersistentValue(builder, persistentValueMetadata); + + builder.addStatement("orderBy($N, $N, $N, order)", + persistentValueMetadata.fieldName() + "$schema", + persistentValueMetadata.fieldName() + "$table", + persistentValueMetadata.fieldName() + "$column" + ) + .addStatement("return this"); + + builderType.addMethod(builder.build()); + } + + private void handleForeignPersistentValue(MethodSpec.Builder builder, PersistentValueMetadata persistentValueMetadata) { if (persistentValueMetadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { AtomicReference localStatics = new AtomicReference<>(); String[] columns = foreignPersistentValueMetadata.links().keySet().stream() @@ -266,44 +317,5 @@ private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata pe CodeBlock.of("new String[]{ $L }", String.join(", ", foreignColumns)) ); } - - builder.varargs(varargs) - .addStatement("return set(new $T($N, $N, $N, $N))", - clauseTypeName, - persistentValueMetadata.fieldName() + "$schema", - persistentValueMetadata.fieldName() + "$table", - persistentValueMetadata.fieldName() + "$column", - persistentValueMetadata.fieldName()); - - - builderType.addMethod(builder.build()); - } - - private void makeNonValuedClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix) { - builderType.addMethod(MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + suffix) - .addModifiers(Modifier.PUBLIC) - .returns(conditionalBuilderClassName) - .addStatement("return set(new $T($N, $N, $N))", - clauseTypeName, - persistentValueMetadata.fieldName() + "$schema", - persistentValueMetadata.fieldName() + "$table", - persistentValueMetadata.fieldName() + "$column" - ) - .build()); - } - - private void makeOrderByClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, ClassName returnType) { - String methodName = "orderBy" + persistentValueMetadata.fieldName().substring(0, 1).toUpperCase() + persistentValueMetadata.fieldName().substring(1); - builderType.addMethod(MethodSpec.methodBuilder(methodName) - .addModifiers(Modifier.PUBLIC) - .returns(returnType) - .addParameter(ClassName.get("net.staticstudios.data.query", "Order"), "order") - .addStatement("orderBy($N, $N, $N, order)", - persistentValueMetadata.fieldName() + "$schema", - persistentValueMetadata.fieldName() + "$table", - persistentValueMetadata.fieldName() + "$column" - ) - .addStatement("return this") - .build()); } } diff --git a/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java b/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java index 6960202e..095cb97d 100644 --- a/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java +++ b/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java @@ -12,7 +12,7 @@ import java.util.function.Consumer; public abstract class AbstractConditionalBuilder, C extends AbstractConditionalBuilder, T extends UniqueData> implements QueryLike { - private final AbstractQueryBuilder queryBuilder; + protected final AbstractQueryBuilder queryBuilder; public AbstractConditionalBuilder(AbstractQueryBuilder queryBuilder) { this.queryBuilder = queryBuilder; @@ -93,6 +93,10 @@ protected void orderBy(String schema, String table, String column, Order order) queryBuilder.orderBy(schema, table, column, order); } + protected void innerJoin(String schema, String table, String[] columns, String foreignSchema, String foreignTable, String[] foreignColumns) { + queryBuilder.innerJoin(schema, table, columns, foreignSchema, foreignTable, foreignColumns); + } + @Override public String toString() { return queryBuilder.toString(); diff --git a/src/main/java/net/staticstudios/data/query/InnerJoin.java b/src/main/java/net/staticstudios/data/query/InnerJoin.java index 4a6d4e5e..e3b3fbad 100644 --- a/src/main/java/net/staticstudios/data/query/InnerJoin.java +++ b/src/main/java/net/staticstudios/data/query/InnerJoin.java @@ -1,5 +1,26 @@ package net.staticstudios.data.query; +import java.util.Arrays; +import java.util.Objects; + public record InnerJoin(String schema, String table, String[] columns, String foreignSchema, String foreignTable, String[] foreignColumns) { + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + InnerJoin that = (InnerJoin) obj; + return Objects.equals(schema, that.schema) && + Objects.equals(table, that.table) && + Arrays.equals(columns, that.columns) && + Objects.equals(foreignSchema, that.foreignSchema) && + Objects.equals(foreignTable, that.foreignTable) && + Arrays.equals(foreignColumns, that.foreignColumns); + } + + @Override + public int hashCode() { + return Objects.hash(schema, table, Arrays.hashCode(columns), foreignSchema, foreignTable, Arrays.hashCode(foreignColumns)); + } } diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 057de003..633e3bae 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -397,6 +397,14 @@ public void testQueryOnForeignColumn() { assertEquals(2, users.size()); assertSame(likesGreen, users.get(0)); assertSame(likesRed, users.get(1)); + + users = MockUserQuery.where(dataManager) + .favoriteColorIsNotNull() + .orderByFavoriteColor(Order.DESCENDING) + .findAll(); + assertEquals(2, users.size()); + assertSame(likesRed, users.get(0)); + assertSame(likesGreen, users.get(1)); } @Test From 8d54e8aa7636df97c4da2e7290944d28f9a35727 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 20 Sep 2025 20:25:09 -0400 Subject: [PATCH 12/75] clarify fkeys --- .../net/staticstudios/data/DataManager.java | 15 +- .../staticstudios/data/parse/ForeignKey.java | 36 +++-- .../staticstudios/data/parse/SQLBuilder.java | 28 ++-- .../staticstudios/data/parse/SQLTable.java | 8 +- .../data/PersistentValueTest.java | 137 ---------------- .../net/staticstudios/data/QueryTest.java | 150 ++++++++++++++++++ 6 files changed, 199 insertions(+), 175 deletions(-) create mode 100644 src/test/java/net/staticstudios/data/QueryTest.java diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index c7a13b90..8ce2f016 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -91,11 +91,9 @@ public void addUpdateHandler(String schema, String table, String column, ValueUp .add(handler); } - //todo: when a row is updated, provide the entire row to this method (so we can grab id cols). this is the responsibility of the data accessor impl @ApiStatus.Internal public void callUpdateHandlers(List columnNames, String schema, String table, String column, Object[] oldSerializedValues, Object[] newSerializedValues) { - logger.trace("Calling update handlers for {}/{}/{} with old values {} and new values {}", schema, table, column, Arrays.toString(oldSerializedValues), Arrays.toString(newSerializedValues)); - //todo: submit to somewhere for where to run these, configured during setup. default to thread utils + logger.trace("Calling update handlers for {}.{}.{} with old values {} and new values {}", schema, table, column, Arrays.toString(oldSerializedValues), Arrays.toString(newSerializedValues)); Map, List>> handlersForColumn = updateHandlers.get(schema + "." + table + "." + column); if (handlersForColumn == null) { return; @@ -128,6 +126,7 @@ public void callUpdateHandlers(List columnNames, String schema, String t Object deserializedOldValue = oldSerializedValues[columnIndex]; //todo: these Object deserializedNewValue = newSerializedValues[columnIndex]; + //todo: submit to somewhere for where to run these, configured during setup. default to thread utils ThreadUtils.submit(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); } } @@ -444,12 +443,12 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { // handle foreign keys. for each foreign key, if we have a value for the local column, we need to set the value for the foreign column. this consists of linking ids mostly. for (SQLTable table : tables) { - for (ForeignKey fKey : table.getForeignKeys()) { + for (ForeignKey fKey : table.getForeignKeysThatReferenceThisTable()) { SQLSchema otherSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getSchema())); SQLTable otherTable = Objects.requireNonNull(otherSchema.getTable(fKey.getTable())); - for (Map.Entry link : fKey.getLinkingColumns().entrySet()) { - String myColumnName = link.getKey(); - String otherColumnName = link.getValue(); + for (ForeignKey.Link link : fKey.getLinkingColumns()) { + String myColumnName = link.columnInReferencedTable(); + String otherColumnName = link.columnInReferringTable(); SQLColumn otherColumn = Objects.requireNonNull(otherTable.getColumn(otherColumnName)); insertContext.set(fKey.getSchema(), fKey.getTable(), otherColumn.getName(), insertContext.getEntries().get(new SimpleColumnMetadata(table.getSchema().getName(), table.getName(), myColumnName, otherColumn.getType()))); } @@ -468,7 +467,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { Map> dependencyGraph = new HashMap<>(); for (SQLTable table : tables) { Set dependsOn = new HashSet<>(); - for (ForeignKey fKey : table.getForeignKeys()) { + for (ForeignKey fKey : table.getForeignKeysThatReferenceThisTable()) { SQLSchema otherSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getSchema())); SQLTable otherTable = Objects.requireNonNull(otherSchema.getTable(fKey.getTable())); dependsOn.add(otherTable); diff --git a/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/src/main/java/net/staticstudios/data/parse/ForeignKey.java index 4efaa052..c8fb95b2 100644 --- a/src/main/java/net/staticstudios/data/parse/ForeignKey.java +++ b/src/main/java/net/staticstudios/data/parse/ForeignKey.java @@ -1,27 +1,26 @@ package net.staticstudios.data.parse; -import java.util.HashMap; -import java.util.Map; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; public class ForeignKey { - // my column -> foreign schema.table.column - private final String column; - private final Map linkingColumns = new HashMap<>(); + private final Set links = new LinkedHashSet<>(); private final String schema; private final String table; - public ForeignKey(String schema, String table, String column) { + public ForeignKey(String schema, String table) { this.schema = schema; this.table = table; - this.column = column; } - public void addColumnMapping(String myColumn, String foreignColumn) { - linkingColumns.put(myColumn, foreignColumn); + public void addColumnMapping(Link link) { + links.add(link); } - public Map getLinkingColumns() { - return linkingColumns; + public Set getLinkingColumns() { + return Collections.unmodifiableSet(links); } public String getSchema() { @@ -32,7 +31,18 @@ public String getTable() { return table; } - public String getColumn() { - return column; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ForeignKey that)) return false; + return Objects.equals(links, that.links) && Objects.equals(schema, that.schema) && Objects.equals(table, that.table); + } + + @Override + public int hashCode() { + return Objects.hash(links, schema, table); + } + + public record Link(String columnInReferencedTable, String columnInReferringTable) { } } diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index 1a2375e3..c523e8b9 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -112,23 +112,25 @@ private List getDefs(Collection schemas) { //todo: add // define fkeys after table creation, to ensure all tables exist before adding fkeys for (SQLSchema schema : schemas) { for (SQLTable table : schema.getTables()) { - for (ForeignKey foreignKey : table.getForeignKeys()) { + for (ForeignKey foreignKey : table.getForeignKeysThatReferenceThisTable()) { if (foreignKey == null) { continue; } - String fKeyName = "fk_" + foreignKey.getSchema() + "_" + foreignKey.getTable() + "_" + String.join("_", foreignKey.getLinkingColumns().values()) + "_to_" + table.getName() + "_" + String.join("_", foreignKey.getLinkingColumns().keySet()); + String fKeyName = "fk_" + foreignKey.getSchema() + "_" + foreignKey.getTable() + "_" + + String.join("_", foreignKey.getLinkingColumns().stream().map(ForeignKey.Link::columnInReferringTable).toList()) + + "_to_" + table.getName() + "_" + String.join("_", foreignKey.getLinkingColumns().stream().map(ForeignKey.Link::columnInReferencedTable).toList()); StringBuilder sb = new StringBuilder(); sb.append("ALTER TABLE \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" "); sb.append("ADD CONSTRAINT IF NOT EXISTS ").append(fKeyName).append(" "); sb.append("FOREIGN KEY ("); - for (String localCol : foreignKey.getLinkingColumns().values()) { - sb.append("\"").append(localCol).append("\", "); + for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { + sb.append("\"").append(link.columnInReferringTable()).append("\", "); } sb.setLength(sb.length() - 2); sb.append(") "); sb.append("REFERENCES \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ("); - for (String foreignCol : foreignKey.getLinkingColumns().keySet()) { - sb.append("\"").append(foreignCol).append("\", "); + for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { + sb.append("\"").append(link.columnInReferencedTable()).append("\", "); } sb.setLength(sb.length() - 2); sb.append(") ON DELETE CASCADE ON UPDATE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type @@ -142,14 +144,14 @@ private List getDefs(Collection schemas) { //todo: add sb.append("ALTER TABLE \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" "); sb.append("ADD CONSTRAINT ").append(fKeyName).append(" "); sb.append("FOREIGN KEY ("); - for (String localCol : foreignKey.getLinkingColumns().values()) { - sb.append("\"").append(localCol).append("\", "); + for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { + sb.append("\"").append(link.columnInReferringTable()).append("\", "); } sb.setLength(sb.length() - 2); sb.append(") "); sb.append("REFERENCES \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ("); - for (String foreignCol : foreignKey.getLinkingColumns().keySet()) { - sb.append("\"").append(foreignCol).append("\", "); + for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { + sb.append("\"").append(link.columnInReferencedTable()).append("\", "); } sb.setLength(sb.length() - 2); sb.append(") ON DELETE CASCADE ON UPDATE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type @@ -306,19 +308,19 @@ private void parseIndividual(Class clazz, Map new ArrayList<>()).add(foreignKey); } - dataSqlTable.getForeignKeys().add(foreignKey); + dataSqlTable.getForeignKeysThatReferenceThisTable().add(foreignKey); } Class type = ReflectionUtils.getGenericType(field); //todo: handle custom types to sql types diff --git a/src/main/java/net/staticstudios/data/parse/SQLTable.java b/src/main/java/net/staticstudios/data/parse/SQLTable.java index 687dc4d7..03c13e73 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLTable.java +++ b/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -11,14 +11,14 @@ public class SQLTable { private final String name; private final List idColumns; private final Map columns; - private final List foreignKeys; //todo: NOTE: these are actually fkeys thet REFER to this table. this implies something else and should probably be adjusted + private final Set foreignKeysThatReferenceThisTable; public SQLTable(SQLSchema schema, String name, List idColumns) { this.schema = schema; this.name = name; this.idColumns = idColumns; this.columns = new HashMap<>(); - this.foreignKeys = new ArrayList<>(); + this.foreignKeysThatReferenceThisTable = new LinkedHashSet<>(); } public SQLSchema getSchema() { @@ -37,8 +37,8 @@ public Set getColumns() { return columns.get(columnName); } - public List getForeignKeys() { - return foreignKeys; + public Set getForeignKeysThatReferenceThisTable() { + return foreignKeysThatReferenceThisTable; } public List getIdColumns() { diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 633e3bae..1abd9abc 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -4,9 +4,7 @@ import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.mock.MockUser; import net.staticstudios.data.mock.MockUserFactory; -import net.staticstudios.data.mock.MockUserQuery; import net.staticstudios.data.mock.MockUserSettings; -import net.staticstudios.data.query.Order; import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Test; @@ -304,139 +302,4 @@ public void testChangeIdColumnInPostgres() { waitForUpdateHandlers(); assertEquals(2, mockUser.nameUpdates.get()); } - - @Test - public void testFindOneEquals() { - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - dataManager.load(MockUser.class); - UUID id = UUID.randomUUID(); - MockUser original = MockUserFactory.builder(dataManager) - .id(id) - .name("test user") - .insert(InsertMode.SYNC); - - MockUser got = MockUserQuery.where(dataManager) - .idIs(id) - .findOne(); - assertSame(original, got); - } - - @Test - public void testFindAllLike() { - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - dataManager.load(MockUser.class); - - MockUser original1 = MockUserFactory.builder(dataManager) - .id(UUID.randomUUID()) - .name("test user") - .age(0) - .insert(InsertMode.SYNC); - MockUser original2 = MockUserFactory.builder(dataManager) - .id(UUID.randomUUID()) - .name("test user2") - .age(5) - .insert(InsertMode.SYNC); - - List got = MockUserQuery.where(dataManager) - .nameIsLike("%test user%") - .orderByAge(Order.ASCENDING) - .findAll(); - - assertEquals(2, got.size()); - assertTrue(got.contains(original1)); - assertTrue(got.contains(original2)); - - assertSame(original1, got.get(0)); - assertSame(original2, got.get(1)); - - got = MockUserQuery.where(dataManager) - .nameIsLike("%test user%") - .orderByAge(Order.DESCENDING) - .findAll(); - - assertEquals(2, got.size()); - assertTrue(got.contains(original1)); - assertTrue(got.contains(original2)); - - assertSame(original2, got.get(0)); - assertSame(original1, got.get(1)); - } - - @Test - public void testQueryOnForeignColumn() { - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - dataManager.load(MockUser.class); - - MockUser likesRed = MockUserFactory.builder(dataManager) - .id(UUID.randomUUID()) - .name("Likes Red") - .favoriteColor("red") - .insert(InsertMode.SYNC); - MockUser likesGreen = MockUserFactory.builder(dataManager) - .id(UUID.randomUUID()) - .name("Likes Green") - .favoriteColor("green") - .insert(InsertMode.SYNC); - - assertNull(MockUserQuery.where(dataManager) - .favoriteColorIs("blue") - .findOne()); - - assertSame(likesRed, MockUserQuery.where(dataManager) - .favoriteColorIs("red") - .findOne()); - - assertSame(likesGreen, MockUserQuery.where(dataManager) - .favoriteColorIs("green") - .findOne()); - - List users = MockUserQuery.where(dataManager) - .favoriteColorIsIn("red", "green") - .orderByName(Order.ASCENDING) - .findAll(); - assertEquals(2, users.size()); - assertSame(likesGreen, users.get(0)); - assertSame(likesRed, users.get(1)); - - users = MockUserQuery.where(dataManager) - .favoriteColorIsNotNull() - .orderByFavoriteColor(Order.DESCENDING) - .findAll(); - assertEquals(2, users.size()); - assertSame(likesRed, users.get(0)); - assertSame(likesGreen, users.get(1)); - } - - @Test - public void testQuery() { //todo: test each clause and ensure it outputs the proper sql - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - - String where = MockUserQuery.where(dataManager) - .idIs(UUID.randomUUID()) - .or() -// .nameIs("user name") -// .or(q -> q.ageIsLessThan(10) -// .and() -// .ageIsLessThanOrEqualTo(5) -// ) -// .or() -// .nameIsIn("name1", "name2", "name3") -// .or() -// .nameIsIn(List.of("name4", "name5", "name6")) -// .limit(10) -// .offset(2) -// .and() -// .ageIsBetween(0, 3) -// .and() -// .ageIsNotNull() -// .and() -// .nameIsLike("%something%") -// .and() -// .nameIsNotLike("%nothing%") -// .orderByAge(Order.ASCENDING) -// .and() - .nameUpdatesIs(4) - .toString(); - System.out.println(where); - } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/QueryTest.java b/src/test/java/net/staticstudios/data/QueryTest.java new file mode 100644 index 00000000..34c1a246 --- /dev/null +++ b/src/test/java/net/staticstudios/data/QueryTest.java @@ -0,0 +1,150 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.mock.MockUser; +import net.staticstudios.data.mock.MockUserFactory; +import net.staticstudios.data.mock.MockUserQuery; +import net.staticstudios.data.query.Order; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class QueryTest extends DataTest { + @Test + public void testFindOneEquals() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser original = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); + + MockUser got = MockUserQuery.where(dataManager) + .idIs(id) + .findOne(); + assertSame(original, got); + } + + @Test + public void testFindAllLike() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + MockUser original1 = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .age(0) + .insert(InsertMode.SYNC); + MockUser original2 = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user2") + .age(5) + .insert(InsertMode.SYNC); + + List got = MockUserQuery.where(dataManager) + .nameIsLike("%test user%") + .orderByAge(Order.ASCENDING) + .findAll(); + + assertEquals(2, got.size()); + assertTrue(got.contains(original1)); + assertTrue(got.contains(original2)); + + assertSame(original1, got.get(0)); + assertSame(original2, got.get(1)); + + got = MockUserQuery.where(dataManager) + .nameIsLike("%test user%") + .orderByAge(Order.DESCENDING) + .findAll(); + + assertEquals(2, got.size()); + assertTrue(got.contains(original1)); + assertTrue(got.contains(original2)); + + assertSame(original2, got.get(0)); + assertSame(original1, got.get(1)); + } + + @Test + public void testQueryOnForeignColumn() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + MockUser likesRed = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("Likes Red") + .favoriteColor("red") + .insert(InsertMode.SYNC); + MockUser likesGreen = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("Likes Green") + .favoriteColor("green") + .insert(InsertMode.SYNC); + + assertNull(MockUserQuery.where(dataManager) + .favoriteColorIs("blue") + .findOne()); + + assertSame(likesRed, MockUserQuery.where(dataManager) + .favoriteColorIs("red") + .findOne()); + + assertSame(likesGreen, MockUserQuery.where(dataManager) + .favoriteColorIs("green") + .findOne()); + + List users = MockUserQuery.where(dataManager) + .favoriteColorIsIn("red", "green") + .orderByName(Order.ASCENDING) + .findAll(); + assertEquals(2, users.size()); + assertSame(likesGreen, users.get(0)); + assertSame(likesRed, users.get(1)); + + users = MockUserQuery.where(dataManager) + .favoriteColorIsNotNull() + .orderByFavoriteColor(Order.DESCENDING) + .findAll(); + assertEquals(2, users.size()); + assertSame(likesRed, users.get(0)); + assertSame(likesGreen, users.get(1)); + } + + @Test + public void testQuery() { //todo: test each clause and ensure it outputs the proper sql + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + + String where = MockUserQuery.where(dataManager) + .idIs(UUID.randomUUID()) + .or() +// .nameIs("user name") +// .or(q -> q.ageIsLessThan(10) +// .and() +// .ageIsLessThanOrEqualTo(5) +// ) +// .or() +// .nameIsIn("name1", "name2", "name3") +// .or() +// .nameIsIn(List.of("name4", "name5", "name6")) +// .limit(10) +// .offset(2) +// .and() +// .ageIsBetween(0, 3) +// .and() +// .ageIsNotNull() +// .and() +// .nameIsLike("%something%") +// .and() +// .nameIsNotLike("%nothing%") +// .orderByAge(Order.ASCENDING) +// .and() + .nameUpdatesIs(4) + .toString(); + System.out.println(where); + } +} \ No newline at end of file From a6b534795c50ce1179be0eb9ce2ec98eab592d95 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 20 Sep 2025 23:43:21 -0400 Subject: [PATCH 13/75] define fkeys when working with references --- .../java/net/staticstudios/data/Column.java | 2 + .../java/net/staticstudios/data/OneToOne.java | 2 + .../staticstudios/data/UpdateStrategy.java | 6 + .../net/staticstudios/data/DataManager.java | 48 ++- .../net/staticstudios/data/Reference.java | 4 +- .../data/impl/data/ReferenceImpl.java | 23 +- .../staticstudios/data/parse/ForeignKey.java | 27 +- .../staticstudios/data/parse/SQLBuilder.java | 353 +++++++++++------- .../staticstudios/data/parse/SQLColumn.java | 12 +- .../staticstudios/data/parse/SQLTable.java | 13 +- .../net/staticstudios/data/util/OnDelete.java | 18 + .../net/staticstudios/data/util/OnUpdate.java | 17 + .../data/PersistentValueTest.java | 6 - .../net/staticstudios/data/ReferenceTest.java | 115 ++++++ .../net/staticstudios/data/SQLParseTest.java | 11 +- .../net/staticstudios/data/mock/MockUser.java | 4 +- .../data/mock/MockUserSettings.java | 9 +- .../data/mock/{ => post}/MockPost.java | 6 +- .../data/mock/post/MockPostMetadata.java | 14 + 19 files changed, 515 insertions(+), 175 deletions(-) create mode 100644 annotations/src/main/java/net/staticstudios/data/UpdateStrategy.java create mode 100644 src/main/java/net/staticstudios/data/util/OnDelete.java create mode 100644 src/main/java/net/staticstudios/data/util/OnUpdate.java create mode 100644 src/test/java/net/staticstudios/data/ReferenceTest.java rename src/test/java/net/staticstudios/data/mock/{ => post}/MockPost.java (81%) create mode 100644 src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java diff --git a/annotations/src/main/java/net/staticstudios/data/Column.java b/annotations/src/main/java/net/staticstudios/data/Column.java index 8ea0fd71..8078a2ba 100644 --- a/annotations/src/main/java/net/staticstudios/data/Column.java +++ b/annotations/src/main/java/net/staticstudios/data/Column.java @@ -15,5 +15,7 @@ boolean nullable() default false; + boolean unique() default false; + String defaultValue() default ""; } diff --git a/annotations/src/main/java/net/staticstudios/data/OneToOne.java b/annotations/src/main/java/net/staticstudios/data/OneToOne.java index bcd16a6b..fddd7d41 100644 --- a/annotations/src/main/java/net/staticstudios/data/OneToOne.java +++ b/annotations/src/main/java/net/staticstudios/data/OneToOne.java @@ -15,4 +15,6 @@ String link(); DeleteStrategy deleteStrategy() default DeleteStrategy.NO_ACTION; + + UpdateStrategy updateStrategy() default UpdateStrategy.CASCADE; } diff --git a/annotations/src/main/java/net/staticstudios/data/UpdateStrategy.java b/annotations/src/main/java/net/staticstudios/data/UpdateStrategy.java new file mode 100644 index 00000000..3c991d95 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/UpdateStrategy.java @@ -0,0 +1,6 @@ +package net.staticstudios.data; + +public enum UpdateStrategy { + CASCADE, + NO_ACTION; +} diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 8ce2f016..4a4dd364 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -450,6 +450,12 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { String myColumnName = link.columnInReferencedTable(); String otherColumnName = link.columnInReferringTable(); SQLColumn otherColumn = Objects.requireNonNull(otherTable.getColumn(otherColumnName)); + + // if its nullable and we don't have a value, skip it. + if (otherColumn.isNullable()) { + continue; + } + insertContext.set(fKey.getSchema(), fKey.getTable(), otherColumn.getName(), insertContext.getEntries().get(new SimpleColumnMetadata(table.getSchema().getName(), table.getName(), myColumnName, otherColumn.getType()))); } } @@ -464,21 +470,45 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { //sort tables based on foreign key dependencies. tables who are depended on should come last // Build dependency graph: table -> set of tables it depends on - Map> dependencyGraph = new HashMap<>(); + Map> dependencyGraph = new HashMap<>(); for (SQLTable table : tables) { Set dependsOn = new HashSet<>(); for (ForeignKey fKey : table.getForeignKeysThatReferenceThisTable()) { SQLSchema otherSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getSchema())); SQLTable otherTable = Objects.requireNonNull(otherSchema.getTable(fKey.getTable())); - dependsOn.add(otherTable); + + boolean addDependency = true; + // if one of the linking columns is not present in the insert context, we can't add the dependency + for (ForeignKey.Link link : fKey.getLinkingColumns()) { + Object value = insertContext.getEntries().entrySet().stream() + .filter(entry -> { + SimpleColumnMetadata key = entry.getKey(); + return key.schema().equals(fKey.getSchema()) && + key.table().equals(fKey.getTable()) && + key.name().equals(link.columnInReferringTable()); + }) + .findFirst() + .orElse(null); + + if (value == null) { + addDependency = false; + break; + } + } + + if (addDependency) { + dependsOn.add(otherTable); + } + } + if (!dependsOn.isEmpty()) { + dependencyGraph.put(table.getName(), dependsOn); } - dependencyGraph.put(table, dependsOn); } // DFS to detect cycles Set visited = new HashSet<>(); Set stack = new HashSet<>(); - for (SQLTable table : dependencyGraph.keySet()) { + for (SQLTable table : tables) { if (hasCycle(table, dependencyGraph, visited, stack)) { throw new IllegalStateException(String.format("Cycle detected in foreign key dependencies involving table %s.%s", table.getSchema().getName(), table.getName())); } @@ -487,7 +517,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { // Topological sort for insert order List orderedTables = new ArrayList<>(); visited.clear(); - for (SQLTable table : dependencyGraph.keySet()) { + for (SQLTable table : tables) { topoSort(table, dependencyGraph, visited, orderedTables); } Collections.reverse(orderedTables); @@ -609,7 +639,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC } } - private boolean hasCycle(SQLTable table, Map> dependencyGraph, Set visited, Set stack) { + private boolean hasCycle(SQLTable table, Map> dependencyGraph, Set visited, Set stack) { if (stack.contains(table)) { return true; } @@ -618,7 +648,7 @@ private boolean hasCycle(SQLTable table, Map> dependency } visited.add(table); stack.add(table); - for (SQLTable dep : dependencyGraph.get(table)) { + for (SQLTable dep : dependencyGraph.getOrDefault(table.getName(), Collections.emptySet())) { if (hasCycle(dep, dependencyGraph, visited, stack)) { return true; } @@ -627,12 +657,12 @@ private boolean hasCycle(SQLTable table, Map> dependency return false; } - private void topoSort(SQLTable table, Map> dependencyGraph, Set visited, List ordered) { + private void topoSort(SQLTable table, Map> dependencyGraph, Set visited, List ordered) { if (visited.contains(table)) { return; } visited.add(table); - for (SQLTable dep : dependencyGraph.get(table)) { + for (SQLTable dep : dependencyGraph.getOrDefault(table.getName(), Collections.emptySet())) { topoSort(dep, dependencyGraph, visited, ordered); } ordered.add(table); diff --git a/src/main/java/net/staticstudios/data/Reference.java b/src/main/java/net/staticstudios/data/Reference.java index 3f7f290b..a5da1b68 100644 --- a/src/main/java/net/staticstudios/data/Reference.java +++ b/src/main/java/net/staticstudios/data/Reference.java @@ -9,9 +9,9 @@ public interface Reference extends Relation { Class getReferenceType(); - T get(); + @Nullable T get(); - void set(T value); + void set(@Nullable T value); class ProxyReference implements Reference { private final UniqueData holder; diff --git a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java index 05234f63..3b1762ca 100644 --- a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -17,15 +17,13 @@ import java.util.Map; public class ReferenceImpl implements Reference { - private final UniqueDataMetadata referenceMetadata; private final UniqueData holder; private final Class type; private final Map link; - public ReferenceImpl(UniqueData holder, Class type, UniqueDataMetadata referenceMetadata, Map link) { + public ReferenceImpl(UniqueData holder, Class type, Map link) { this.holder = holder; this.type = type; - this.referenceMetadata = referenceMetadata; this.link = link; } @@ -33,14 +31,13 @@ public static void createAndDelegate(Reference.ProxyRefer ReferenceImpl delegate = new ReferenceImpl<>( proxy.getHolder(), proxy.getReferenceType(), - proxy.getHolder().getDataManager().getMetadata(proxy.getReferenceType()), link ); proxy.setDelegate(delegate); } - public static ReferenceImpl create(UniqueData holder, Class type, UniqueDataMetadata referenceMetadata, Map link) { - return new ReferenceImpl<>(holder, type, referenceMetadata, link); + public static ReferenceImpl create(UniqueData holder, Class type, Map link) { + return new ReferenceImpl<>(holder, type, link); } public static void delegate(T instance) { @@ -49,7 +46,6 @@ public static void delegate(T instance) { Preconditions.checkNotNull(oneToOneAnnotation, "Field %s in class %s is missing @OneToOne annotation".formatted(pair.field().getName(), instance.getClass().getName())); Class referencedClass = ReflectionUtils.getGenericType(pair.field()); Preconditions.checkNotNull(referencedClass, "Field %s in class %s is not parameterized".formatted(pair.field().getName(), instance.getClass().getName())); - UniqueDataMetadata referenceMetadata = instance.getDataManager().getMetadata((Class) referencedClass); Map link = new HashMap<>(); for (String l : StringUtils.parseCommaSeperatedList(oneToOneAnnotation.link())) { String[] split = l.split("="); @@ -62,7 +58,7 @@ public static void delegate(T instance) { } else { pair.field().setAccessible(true); try { - pair.field().set(instance, create(instance, (Class) referencedClass, referenceMetadata, link)); + pair.field().set(instance, create(instance, (Class) referencedClass, link)); } catch (IllegalAccessException e) { throw new RuntimeException(e); } @@ -81,7 +77,7 @@ public Class getReferenceType() { } @Override - public T get() { + public @Nullable T get() { ColumnValuePair[] idColumns = new ColumnValuePair[link.size()]; int i = 0; UniqueDataMetadata holderMetadata = holder.getMetadata(); @@ -115,14 +111,18 @@ public T get() { } @Override - public void set(T value) { - //todo: set local columns to the value's id columns + public void set(@Nullable T value) { UniqueDataMetadata holderMetadata = holder.getMetadata(); List values = new ArrayList<>(); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("UPDATE \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" SET "); for (Map.Entry entry : link.entrySet()) { String myColumn = entry.getKey(); + if (value == null) { + sqlBuilder.append("\"").append(myColumn).append("\" = NULL, "); + continue; + } + String theirColumn = entry.getValue(); Object theirValue = null; for (ColumnValuePair columnValuePair : value.getIdColumns()) { @@ -147,7 +147,6 @@ public void set(T value) { values.add(columnValuePair.value()); } - try { holder.getDataManager().getDataAccessor().executeUpdate(sqlBuilder.toString(), values); } catch (SQLException e) { diff --git a/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/src/main/java/net/staticstudios/data/parse/ForeignKey.java index c8fb95b2..74037357 100644 --- a/src/main/java/net/staticstudios/data/parse/ForeignKey.java +++ b/src/main/java/net/staticstudios/data/parse/ForeignKey.java @@ -1,5 +1,8 @@ package net.staticstudios.data.parse; +import net.staticstudios.data.util.OnDelete; +import net.staticstudios.data.util.OnUpdate; + import java.util.Collections; import java.util.LinkedHashSet; import java.util.Objects; @@ -9,13 +12,17 @@ public class ForeignKey { private final Set links = new LinkedHashSet<>(); private final String schema; private final String table; + private final OnDelete onDelete; + private final OnUpdate onUpdate; - public ForeignKey(String schema, String table) { + public ForeignKey(String schema, String table, OnDelete onDelete, OnUpdate onUpdate) { this.schema = schema; this.table = table; + this.onDelete = onDelete; + this.onUpdate = onUpdate; } - public void addColumnMapping(Link link) { + public void addLink(Link link) { links.add(link); } @@ -31,16 +38,28 @@ public String getTable() { return table; } + public OnDelete getOnDelete() { + return onDelete; + } + + public OnUpdate getOnUpdate() { + return onUpdate; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ForeignKey that)) return false; - return Objects.equals(links, that.links) && Objects.equals(schema, that.schema) && Objects.equals(table, that.table); + return Objects.equals(onDelete, that.onDelete) && + Objects.equals(onUpdate, that.onUpdate) && + Objects.equals(links, that.links) && + Objects.equals(schema, that.schema) && + Objects.equals(table, that.table); } @Override public int hashCode() { - return Objects.hash(links, schema, table); + return Objects.hash(links, schema, table, onDelete, onUpdate); } public record Link(String columnInReferencedTable, String columnInReferringTable) { diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index c523e8b9..eaa52403 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -3,16 +3,17 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.*; import net.staticstudios.data.util.*; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.lang.reflect.Field; import java.util.*; public class SQLBuilder { public static final String INDENT = " "; + private static final Logger logger = LoggerFactory.getLogger(SQLBuilder.class); private final Map parsedSchemas; - private final Map> foreignKeys = new HashMap<>(); private final DataManager dataManager; public SQLBuilder(DataManager dataManager) { @@ -26,7 +27,10 @@ public List parse(Class clazz) { Set> visited = walk(clazz); Map schemas = new HashMap<>(); for (Class visitedClass : visited) { - parseIndividual(visitedClass, schemas); + parseIndividualColumns(visitedClass, schemas); + } + for (Class visitedClass : visited) { + parseIndividualRelations(visitedClass, schemas); } @@ -56,16 +60,11 @@ public List parse(Class clazz) { return getDefs(schemas.values()); } - public @NotNull List getForeignKeysReferencingColumn(String schema, String table, String column) { - String key = schema + "." + table + "." + column; - return foreignKeys.getOrDefault(key, Collections.emptyList()); - } - public @Nullable SQLSchema getSchema(String name) { return parsedSchemas.get(name); } - private List getDefs(Collection schemas) { //todo: add indexes, uniques, + private List getDefs(Collection schemas) { //todo: add indexes List statements = new ArrayList<>(); for (SQLSchema schema : schemas) { statements.add(DDLStatement.both("CREATE SCHEMA IF NOT EXISTS \"" + schema.getName() + "\";")); @@ -102,6 +101,10 @@ private List getDefs(Collection schemas) { //todo: add //todo: this is not valid sql, need to create index separately } + if (column.isUnique()) { + sb.append(" UNIQUE"); + } + sb.append(";"); statements.add(DDLStatement.both(sb.toString())); } @@ -133,7 +136,7 @@ private List getDefs(Collection schemas) { //todo: add sb.append("\"").append(link.columnInReferencedTable()).append("\", "); } sb.setLength(sb.length() - 2); - sb.append(") ON DELETE CASCADE ON UPDATE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type + sb.append(") ON DELETE ").append(foreignKey.getOnDelete()).append(" ON UPDATE ").append(foreignKey.getOnUpdate()).append(";"); String h2 = sb.toString(); @@ -154,7 +157,7 @@ private List getDefs(Collection schemas) { //todo: add sb.append("\"").append(link.columnInReferencedTable()).append("\", "); } sb.setLength(sb.length() - 2); - sb.append(") ON DELETE CASCADE ON UPDATE CASCADE;"); //todo: on delete cascade or no action or set null? depends on type + sb.append(") ON DELETE ").append(foreignKey.getOnDelete()).append(" ON UPDATE ").append(foreignKey.getOnUpdate()).append(";"); sb.append(" END IF; END $$;"); String pg = sb.toString(); statements.add(DDLStatement.of(h2, pg)); @@ -188,7 +191,8 @@ private void walk(Class clazz, Set clazz, Map schemas) { + private void parseIndividualColumns(Class clazz, Map schemas) { + logger.trace("Parsing columns 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"); @@ -198,141 +202,240 @@ private void parseIndividual(Class clazz, Map clazz, Map schemas) { + logger.trace("Parsing relations 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"); + } - schemaName = schemaName.isEmpty() ? dataSchema : schemaName; - tableName = tableName.isEmpty() ? dataTable : tableName; + Data dataAnnotation = clazz.getAnnotation(Data.class); + Preconditions.checkNotNull(dataAnnotation, "Data annotation is null for class " + clazz.getName()); - if (foreignColumn != null) { - Preconditions.checkArgument(!(schemaName.equals(dataSchema) && tableName.equals(dataTable)), "ForeignColumn field %s in class %s cannot reference its own table", field.getName(), clazz.getName()); - } + for (Field field : ReflectionUtils.getFields(clazz)) { + parseReference(clazz, schemas, dataAnnotation, metadata, field); + } + } - SQLSchema schema = schemas.computeIfAbsent(schemaName, SQLSchema::new); - SQLTable table = schema.getTable(tableName); - if (table == null) { - List idColumns = metadata.idColumns(); - - if (foreignColumn != null) { - idColumns = new ArrayList<>(); - List links = StringUtils.parseCommaSeperatedList(foreignColumn.link()); - for (String link : links) { - String[] parts = link.split("="); - Preconditions.checkArgument(parts.length == 2, "Invalid link format in OneToOne annotation on field %s in class %s. Expected format: localColumn=foreignColumn", field.getName(), clazz.getName()); - String localColumn = ValueUtils.parseValue(parts[0].trim()); - String otherColumn = ValueUtils.parseValue(parts[1].trim()); - - ColumnMetadata found = null; - for (ColumnMetadata idCol : metadata.idColumns()) { - if (idCol.name().equals(localColumn)) { - found = idCol; - break; - } - } - Preconditions.checkNotNull(found, "Link name %s in OneToOne annotation on field %s in class %s is not an ID name", localColumn, field.getName(), clazz.getName()); + private void parseColumn(Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { + if (!field.getType().equals(PersistentValue.class)) { + return; + } - idColumns.add(new ColumnMetadata(schemaName, tableName, otherColumn, found.type(), false, false, "")); - } - } + IdColumn idColumn = field.getAnnotation(IdColumn.class); + Column columnAnnotation = field.getAnnotation(Column.class); + ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); + + int annotationsCount = 0; + if (idColumn != null) annotationsCount++; + if (columnAnnotation != null) annotationsCount++; + if (foreignColumn != null) annotationsCount++; + Preconditions.checkArgument(annotationsCount <= 1, "Field " + field.getName() + " in class " + clazz.getName() + " has multiple column annotations. Only one of @IdColumn, @Column, or @ForeignColumn is allowed."); + + String schemaName; + String tableName; + String columnName; + boolean nullable; + boolean indexed; + boolean unique; + String defaultValue; + if (idColumn != null) { + schemaName = ValueUtils.parseValue(dataAnnotation.schema()); + tableName = ValueUtils.parseValue(dataAnnotation.table()); + columnName = ValueUtils.parseValue(idColumn.name()); + nullable = false; + indexed = false; + unique = true; + defaultValue = ""; + } else if (columnAnnotation != null) { + schemaName = ValueUtils.parseValue(columnAnnotation.schema()); + tableName = ValueUtils.parseValue(columnAnnotation.table()); + columnName = ValueUtils.parseValue(columnAnnotation.name()); + nullable = columnAnnotation.nullable(); + indexed = columnAnnotation.index(); + unique = columnAnnotation.unique(); + defaultValue = columnAnnotation.defaultValue(); + } else if (foreignColumn != null) { + schemaName = ValueUtils.parseValue(foreignColumn.schema()); + tableName = ValueUtils.parseValue(foreignColumn.table()); + columnName = ValueUtils.parseValue(foreignColumn.name()); + nullable = foreignColumn.nullable(); + indexed = foreignColumn.index(); + unique = false; + defaultValue = foreignColumn.defaultValue(); + } else { + return; + } + + String dataSchema = ValueUtils.parseValue(dataAnnotation.schema()); + String dataTable = ValueUtils.parseValue(dataAnnotation.table()); - table = new SQLTable(schema, tableName, idColumns); - schema.addTable(table); + schemaName = schemaName.isEmpty() ? dataSchema : schemaName; + tableName = tableName.isEmpty() ? dataTable : tableName; - if (foreignColumn != null) { - for (ColumnMetadata idCol : table.getIdColumns()) { - Preconditions.checkState(table.getColumn(idCol.name()) == null, "ID column name " + idCol.name() + " in table " + tableName + " is duplicated!"); - SQLColumn sqlColumn = new SQLColumn(table, idCol.type(), idCol.name(), false, false, null); - table.addColumn(sqlColumn); + if (foreignColumn != null) { + Preconditions.checkArgument(!(schemaName.equals(dataSchema) && tableName.equals(dataTable)), "ForeignColumn field %s in class %s cannot reference its own table", field.getName(), clazz.getName()); + } + + SQLSchema schema = schemas.computeIfAbsent(schemaName, SQLSchema::new); + SQLTable table = schema.getTable(tableName); + if (table == null) { + List idColumns; + + if (foreignColumn == null) { + idColumns = metadata.idColumns(); + } else { + idColumns = new ArrayList<>(); + List links = StringUtils.parseCommaSeperatedList(foreignColumn.link()); + for (String link : links) { + String[] parts = link.split("="); + Preconditions.checkArgument(parts.length == 2, "Invalid link format in OneToOne annotation on field %s in class %s. Expected format: localColumn=foreignColumn", field.getName(), clazz.getName()); + String localColumn = ValueUtils.parseValue(parts[0].trim()); + String otherColumn = ValueUtils.parseValue(parts[1].trim()); + + ColumnMetadata found = null; + for (ColumnMetadata idCol : metadata.idColumns()) { + if (idCol.name().equals(localColumn)) { + found = idCol; + break; + } } + Preconditions.checkNotNull(found, "Link name %s in OneToOne annotation on field %s in class %s is not an ID name", localColumn, field.getName(), clazz.getName()); + + idColumns.add(new ColumnMetadata(schemaName, tableName, otherColumn, found.type(), false, false, "")); } } - if (foreignColumn != null) { - SQLSchema dataSqlSchema = schemas.computeIfAbsent(dataSchema, SQLSchema::new); - SQLTable dataSqlTable = dataSqlSchema.getTable(dataTable); - if (dataSqlTable == null) { - dataSqlTable = new SQLTable(dataSqlSchema, dataTable, metadata.idColumns()); - dataSqlSchema.addTable(dataSqlTable); - } + table = new SQLTable(schema, tableName, idColumns); + schema.addTable(table); - String otherSchema = ValueUtils.parseValue(foreignColumn.schema()); - if (otherSchema.isEmpty()) { - otherSchema = schemaName; + if (foreignColumn != null) { + for (ColumnMetadata idCol : table.getIdColumns()) { + Preconditions.checkState(table.getColumn(idCol.name()) == null, "ID column name " + idCol.name() + " in table " + tableName + " is duplicated!"); + SQLColumn sqlColumn = new SQLColumn(table, idCol.type(), idCol.name(), false, false, true, null); + table.addColumn(sqlColumn); } - String otherTable = ValueUtils.parseValue(foreignColumn.table()); - if (otherTable.isEmpty()) { - otherTable = tableName; + } + } else if (foreignColumn != null) { + List links = parseLinks(foreignColumn.link()); + for (ForeignKey.Link link : links) { + ColumnMetadata found = null; + for (ColumnMetadata idCol : metadata.idColumns()) { + if (idCol.name().equals(link.columnInReferringTable())) { + found = idCol; + break; + } } + Preconditions.checkNotNull(found, "Link name %s in ForeignColumn annotation on field %s in class %s is not an ID name", link.columnInReferringTable(), field.getName(), clazz.getName()); + } + } - ForeignKey foreignKey = new ForeignKey(otherSchema, otherTable); - for (String link : StringUtils.parseCommaSeperatedList(foreignColumn.link())) { - String[] parts = link.split("="); - Preconditions.checkArgument(parts.length == 2, "Invalid link format in ForeignColumn annotation on field %s in class %s. Expected format: localColumn=foreignColumn", field.getName(), clazz.getName()); + if (foreignColumn != null) { + SQLSchema dataSqlSchema = schemas.computeIfAbsent(dataSchema, SQLSchema::new); + SQLTable dataSqlTable = dataSqlSchema.getTable(dataTable); + if (dataSqlTable == null) { + dataSqlTable = new SQLTable(dataSqlSchema, dataTable, metadata.idColumns()); + dataSqlSchema.addTable(dataSqlTable); + } - String localColumn = ValueUtils.parseValue(parts[0].trim()); - String otherColumn = ValueUtils.parseValue(parts[1].trim()); - foreignKey.addColumnMapping(new ForeignKey.Link(localColumn, otherColumn)); - String schemaTableColumn = schemaName + "." + tableName + "." + localColumn; - foreignKeys.computeIfAbsent(schemaTableColumn, k -> new ArrayList<>()).add(foreignKey); - } + String otherSchema = ValueUtils.parseValue(foreignColumn.schema()); + if (otherSchema.isEmpty()) { + otherSchema = schemaName; + } + String otherTable = ValueUtils.parseValue(foreignColumn.table()); + if (otherTable.isEmpty()) { + otherTable = tableName; + } - dataSqlTable.getForeignKeysThatReferenceThisTable().add(foreignKey); + ForeignKey foreignKey = new ForeignKey(otherSchema, otherTable, OnDelete.CASCADE, OnUpdate.CASCADE); + try { + parseLinks(foreignKey, foreignColumn.link()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error parsing @ForeignColumn link on field " + field.getName() + " in class " + clazz.getName() + ": " + e.getMessage(), e); } + dataSqlTable.addForeignKeyThatReferencesThisTable(foreignKey); + } - Class type = ReflectionUtils.getGenericType(field); //todo: handle custom types to sql types - SQLColumn sqlColumn = new SQLColumn(table, type, columnName, nullable, indexed, defaultValue.isEmpty() ? null : SQLUtils.parseDefaultValue(type, defaultValue)); + Class type = ReflectionUtils.getGenericType(field); //todo: handle custom types to sql types + SQLColumn sqlColumn = new SQLColumn(table, type, columnName, nullable, indexed, unique, defaultValue.isEmpty() ? null : SQLUtils.parseDefaultValue(type, defaultValue)); - SQLColumn existingColumn = table.getColumn(columnName); - if (existingColumn != null) { - Preconditions.checkState(existingColumn.equals(sqlColumn), "Column " + columnName + " in table " + tableName + " has conflicting definitions! Existing: " + existingColumn + ", New: " + sqlColumn); - continue; + SQLColumn existingColumn = table.getColumn(columnName); + if (existingColumn != null) { + Preconditions.checkState(existingColumn.equals(sqlColumn), "Column " + columnName + " in table " + tableName + " 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; + } + Class genericType = ReflectionUtils.getGenericType(field); + Preconditions.checkArgument(genericType != null && UniqueData.class.isAssignableFrom(genericType), "Field " + field.getName() + " in class " + clazz.getName() + " is not parameterized with a UniqueData type! Generic type: " + genericType); + UniqueDataMetadata referencedMetadata = dataManager.getMetadata(genericType.asSubclass(UniqueData.class)); + Preconditions.checkNotNull(referencedMetadata, "No metadata found for referenced class " + genericType.getName()); + OneToOne oneToOne = field.getAnnotation(OneToOne.class); + if (oneToOne == null) { + return; + } + + String dataSchema = ValueUtils.parseValue(dataAnnotation.schema()); + String dataTable = ValueUtils.parseValue(dataAnnotation.table()); + + 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); + + } + + SQLSchema referencedSchema = Objects.requireNonNull(schemas.get(referencedMetadata.schema())); + SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(referencedMetadata.table())); + + OnDelete onDelete = switch (oneToOne.deleteStrategy()) { + case CASCADE -> OnDelete.CASCADE; + case NO_ACTION -> OnDelete.NO_ACTION; + default -> throw new IllegalStateException("Unexpected value: " + oneToOne.deleteStrategy()); + }; + + OnUpdate onUpdate = switch (oneToOne.updateStrategy()) { + case CASCADE -> OnUpdate.CASCADE; + case NO_ACTION -> OnUpdate.NO_ACTION; + }; + + ForeignKey foreignKey = new ForeignKey(schema.getName(), table.getName(), onDelete, onUpdate); + try { + for (ForeignKey.Link link : parseLinks(oneToOne.link())) { //reverse, since the fkey is on our table + foreignKey.addLink(new ForeignKey.Link(link.columnInReferringTable(), link.columnInReferencedTable())); } + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error parsing @OneToOne link on field " + field.getName() + " in class " + clazz.getName() + ": " + e.getMessage(), e); + } //todo: all columns in the link must be unique in our table, and must be id columns in the referenced table. this will be enforced by H2 but check here for better errors + referencedTable.addForeignKeyThatReferencesThisTable(foreignKey); + } + + private void parseLinks(ForeignKey foreignKey, String links) { + List parsedLinks = parseLinks(links); + for (ForeignKey.Link link : parsedLinks) { + foreignKey.addLink(link); + } + } - table.addColumn(sqlColumn); + private List parseLinks(String links) { + List mappings = new ArrayList<>(); + for (String link : StringUtils.parseCommaSeperatedList(links)) { + String[] parts = link.split("="); + Preconditions.checkArgument(parts.length == 2, "Invalid link format! Expected format: localColumn=foreignColumn, got: " + link); + mappings.add(new ForeignKey.Link(ValueUtils.parseValue(parts[0].trim()), ValueUtils.parseValue(parts[1].trim()))); } + return mappings; } } diff --git a/src/main/java/net/staticstudios/data/parse/SQLColumn.java b/src/main/java/net/staticstudios/data/parse/SQLColumn.java index cc7ca1c7..85cac6f6 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLColumn.java +++ b/src/main/java/net/staticstudios/data/parse/SQLColumn.java @@ -10,14 +10,16 @@ public class SQLColumn { private final String name; private final boolean nullable; private final boolean indexed; + private final boolean unique; private final @Nullable String defaultValue; - public SQLColumn(SQLTable table, Class type, String name, boolean nullable, boolean indexed, @Nullable String defaultValue) { + public SQLColumn(SQLTable table, Class type, String name, boolean nullable, boolean indexed, boolean unique, @Nullable String defaultValue) { this.table = table; this.type = type; this.name = name; this.nullable = nullable; this.indexed = indexed; + this.unique = unique; this.defaultValue = defaultValue; } @@ -41,13 +43,17 @@ public boolean isIndexed() { return indexed; } + public boolean isUnique() { + return unique; + } + public @Nullable String getDefaultValue() { return defaultValue; } @Override public int hashCode() { - return Objects.hash(table, type, name, nullable, indexed, defaultValue); + return Objects.hash(table, type, name, nullable, indexed, unique, defaultValue); } @Override @@ -57,6 +63,7 @@ public boolean equals(Object obj) { SQLColumn other = (SQLColumn) obj; return nullable == other.nullable && indexed == other.indexed && + unique == other.unique && Objects.equals(defaultValue, other.defaultValue) && Objects.equals(type, other.type) && Objects.equals(name, other.name); @@ -69,6 +76,7 @@ public String toString() { ", name='" + name + '\'' + ", nullable=" + nullable + ", indexed=" + indexed + + ", unique=" + unique + ", defaultValue='" + defaultValue + '\'' + '}'; } diff --git a/src/main/java/net/staticstudios/data/parse/SQLTable.java b/src/main/java/net/staticstudios/data/parse/SQLTable.java index 03c13e73..62fb43ad 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLTable.java +++ b/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -38,7 +38,18 @@ public Set getColumns() { } public Set getForeignKeysThatReferenceThisTable() { - return foreignKeysThatReferenceThisTable; + return Collections.unmodifiableSet(foreignKeysThatReferenceThisTable); + } + + public void addForeignKeyThatReferencesThisTable(ForeignKey foreignKey) { + ForeignKey existingKey = foreignKeysThatReferenceThisTable.stream() + .filter(fk -> fk.getSchema().equals(foreignKey.getSchema()) && fk.getTable().equals(foreignKey.getTable())) + .findFirst() + .orElse(null); + if (existingKey != null && !Objects.equals(existingKey, foreignKey)) { + throw new IllegalArgumentException("Foreign key from " + foreignKey.getSchema() + "." + foreignKey.getTable() + " already exists and is different from the one being added! Existing: " + existingKey + ", New: " + foreignKey); + } + foreignKeysThatReferenceThisTable.add(foreignKey); } public List getIdColumns() { diff --git a/src/main/java/net/staticstudios/data/util/OnDelete.java b/src/main/java/net/staticstudios/data/util/OnDelete.java new file mode 100644 index 00000000..119d2854 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/OnDelete.java @@ -0,0 +1,18 @@ +package net.staticstudios.data.util; + +public enum OnDelete { + CASCADE("CASCADE"), + SET_NULL("SET NULL"), + NO_ACTION("NO ACTION"); + + private final String sql; + + OnDelete(String sql) { + this.sql = sql; + } + + @Override + public String toString() { + return sql; + } +} diff --git a/src/main/java/net/staticstudios/data/util/OnUpdate.java b/src/main/java/net/staticstudios/data/util/OnUpdate.java new file mode 100644 index 00000000..98373748 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/OnUpdate.java @@ -0,0 +1,17 @@ +package net.staticstudios.data.util; + +public enum OnUpdate { + CASCADE("CASCADE"), + NO_ACTION("NO ACTION"); + + private final String sql; + + OnUpdate(String sql) { + this.sql = sql; + } + + @Override + public String toString() { + return sql; + } +} diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 1abd9abc..e65782df 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -4,7 +4,6 @@ import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.mock.MockUser; import net.staticstudios.data.mock.MockUserFactory; -import net.staticstudios.data.mock.MockUserSettings; import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Test; @@ -98,11 +97,6 @@ public void test() throws SQLException { // } waitForDataPropagation(); - - MockUserSettings settings = MockUserSettings.create(dataManager, UUID.randomUUID()); - assertNull(mockUser.settings.get()); - mockUser.settings.set(settings); - assertEquals(settings, mockUser.settings.get()); } @Test diff --git a/src/test/java/net/staticstudios/data/ReferenceTest.java b/src/test/java/net/staticstudios/data/ReferenceTest.java new file mode 100644 index 00000000..565a6e54 --- /dev/null +++ b/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -0,0 +1,115 @@ +package net.staticstudios.data; + +import net.staticstudios.data.insert.InsertContext; +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.mock.MockUser; +import net.staticstudios.data.mock.MockUserFactory; +import net.staticstudios.data.mock.MockUserSettings; +import net.staticstudios.data.mock.MockUserSettingsFactory; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class ReferenceTest extends DataTest { + @Test + public void testCreateSettingsWithoutReference() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + MockUserSettings settings = MockUserSettingsFactory.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + assertNotNull(settings); + } + + @Test + public void testCreateSettingsThenReference() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + MockUserSettings settings = MockUserSettingsFactory.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + assertNotNull(settings); + + MockUser user = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .settingsId(settings.id.get()) + .insert(InsertMode.SYNC); + + assertNotNull(user); + assertSame(settings, user.settings.get()); + } + + @Test + public void testCreateUserAndReferenceInSingleInsert() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + UUID settingsId = UUID.randomUUID(); + InsertContext ctx = dataManager.createInsertContext(); + MockUserSettingsFactory.builder(dataManager) + .id(settingsId) + .insert(ctx); + + MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .settingsId(settingsId) + .insert(ctx); + + ctx.insert(InsertMode.SYNC); + MockUserSettings settings = ctx.get(MockUserSettings.class); + MockUser user = ctx.get(MockUser.class); + + assertNotNull(settings); + assertNotNull(user); + assertSame(settings, user.settings.get()); + } + + @Test + public void testChangeReference() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + MockUserSettings settings = MockUserSettingsFactory.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + assertNotNull(settings); + + MockUser user = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .settingsId(settings.id.get()) + .insert(InsertMode.SYNC); + + assertNotNull(user); + assertSame(settings, user.settings.get()); + + MockUserSettings settings2 = MockUserSettingsFactory.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + assertNotNull(settings2); + user.settingsId.set(settings2.id.get()); + assertSame(settings2, user.settings.get()); + + user.settings.set(null); + assertNull(user.settings.get()); + assertNull(user.settingsId.get()); + + user.settings.set(settings); + assertSame(settings, user.settings.get()); + assertEquals(settings.id.get(), user.settingsId.get()); + + user.settings.set(settings2); + assertSame(settings2, user.settings.get()); + assertEquals(settings2.id.get(), user.settingsId.get()); + } +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index 76547dd3..2128f1ba 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -1,7 +1,7 @@ package net.staticstudios.data; import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.mock.MockPost; +import net.staticstudios.data.mock.post.MockPost; import net.staticstudios.data.parse.DDLStatement; import net.staticstudios.data.util.EnvironmentVariableAccessor; import net.staticstudios.data.util.ValueUtils; @@ -82,14 +82,23 @@ public void testParse() throws Exception { post_id integer NOT NULL, interactions integer DEFAULT 0 NOT NULL ); + CREATE TABLE social_media.posts_metadata ( + metadata_id integer NOT NULL, + flag boolean NOT NULL + ); ALTER TABLE ONLY social_media.posts_interactions ADD CONSTRAINT posts_interactions_pkey PRIMARY KEY (post_id); + ALTER TABLE ONLY social_media.posts_metadata + ADD CONSTRAINT posts_metadata_pkey PRIMARY KEY (metadata_id); ALTER TABLE ONLY social_media.posts ADD CONSTRAINT posts_pkey PRIMARY KEY (post_id); ALTER TABLE ONLY social_media.posts_interactions ADD CONSTRAINT fk_social_media_posts_interactions_post_id_to_posts_post_id FOREIGN KEY (post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; + ALTER TABLE ONLY social_media.posts + ADD CONSTRAINT fk_social_media_posts_post_id_to_posts_metadata_metadata_id FOREIGN KEY (post_id) REFERENCES social_media.posts_metadata(metadata_id) ON UPDATE CASCADE; """; assertEquals(expected.trim(), cleanedDump.toString().trim()); + //todo: test with a reference } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/MockUser.java index 214d27ae..96e85da3 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/MockUser.java @@ -14,13 +14,13 @@ public class MockUser extends UniqueData { //todo: note - maybe PC's add and remove handlers can be implemented using update handlers @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); - @Column(name = "settings_id", nullable = true) + @Column(name = "settings_id", nullable = true, unique = true) public PersistentValue settingsId = PersistentValue.of(this, UUID.class); @Column(name = "age", nullable = true) public PersistentValue age; @ForeignColumn(name = "fav_color", table = "user_preferences", nullable = true, link = "id=user_id") public PersistentValue favoriteColor; - @OneToOne(link = "settings_id=user_id") + @OneToOne(link = "settings_id=user_id", deleteStrategy = DeleteStrategy.CASCADE) //todo: this should generate an fkey (add an option to control what happens on update/delete) public Reference settings; @ForeignColumn(name = "name_updates", table = "user_metadata", link = "id=user_id", defaultValue = "0") diff --git a/src/test/java/net/staticstudios/data/mock/MockUserSettings.java b/src/test/java/net/staticstudios/data/mock/MockUserSettings.java index b820da46..0b483771 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUserSettings.java +++ b/src/test/java/net/staticstudios/data/mock/MockUserSettings.java @@ -9,12 +9,5 @@ public class MockUserSettings extends UniqueData { @IdColumn(name = "user_id") public PersistentValue id; @Column(name = "font_size", defaultValue = "10") - public PersistentValue fontSide; - - public static MockUserSettings create(DataManager dataManager, UUID id) { - return dataManager.createInsertContext() - .set(MockUserSettings.class, "user_id", id) - .insert(InsertMode.SYNC) - .get(MockUserSettings.class); - } + public PersistentValue fontSize; } diff --git a/src/test/java/net/staticstudios/data/mock/MockPost.java b/src/test/java/net/staticstudios/data/mock/post/MockPost.java similarity index 81% rename from src/test/java/net/staticstudios/data/mock/MockPost.java rename to src/test/java/net/staticstudios/data/mock/post/MockPost.java index 31a6f086..9d7ced74 100644 --- a/src/test/java/net/staticstudios/data/mock/MockPost.java +++ b/src/test/java/net/staticstudios/data/mock/post/MockPost.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.mock; +package net.staticstudios.data.mock.post; import net.staticstudios.data.*; @@ -9,6 +9,8 @@ public class MockPost extends UniqueData { @IdColumn(name = "${POST_ID_COLUMN}") public PersistentValue id; + @OneToOne(link = "${POST_ID_COLUMN}=metadata_id") + public Reference metadata; @Column(name = "text_content") public PersistentValue textContent; @@ -16,6 +18,4 @@ public class MockPost extends UniqueData { public PersistentValue likes; @ForeignColumn(name = "interactions", table = "${POST_TABLE}_interactions", link = "${POST_ID_COLUMN}=post_id", defaultValue = "0") public PersistentValue interactions; - - //todo: test relationships } diff --git a/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java b/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java new file mode 100644 index 00000000..a7708e6d --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java @@ -0,0 +1,14 @@ +package net.staticstudios.data.mock.post; + +import net.staticstudios.data.*; + +/** + * Used to validate schema generation. + */ +@Data(schema = "${POST_SCHEMA}", table = "${POST_TABLE}_metadata") +public class MockPostMetadata extends UniqueData { + @IdColumn(name = "metadata_id") + public PersistentValue id; + @Column(name = "flag") + public PersistentValue flag; +} From e63969adc7ebab22c62960d75643dc910c28b627 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 21 Sep 2025 13:04:39 -0400 Subject: [PATCH 14/75] add index support --- .../java/net/staticstudios/data/Column.java | 2 +- .../staticstudios/data/parse/SQLBuilder.java | 22 ++++++++++++------- .../net/staticstudios/data/SQLParseTest.java | 2 +- .../net/staticstudios/data/mock/MockUser.java | 3 --- .../data/mock/post/MockPost.java | 2 +- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/annotations/src/main/java/net/staticstudios/data/Column.java b/annotations/src/main/java/net/staticstudios/data/Column.java index 8078a2ba..b38573fb 100644 --- a/annotations/src/main/java/net/staticstudios/data/Column.java +++ b/annotations/src/main/java/net/staticstudios/data/Column.java @@ -11,7 +11,7 @@ String table() default ""; - boolean index() default false; //todo: this + boolean index() default false; boolean nullable() default false; diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index eaa52403..c8cfdfc7 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.*; import net.staticstudios.data.util.*; +import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,13 +65,12 @@ public List parse(Class clazz) { return parsedSchemas.get(name); } - private List getDefs(Collection schemas) { //todo: add indexes + private List getDefs(Collection schemas) { List statements = new ArrayList<>(); for (SQLSchema schema : schemas) { statements.add(DDLStatement.both("CREATE SCHEMA IF NOT EXISTS \"" + schema.getName() + "\";")); StringBuilder sb; for (SQLTable table : schema.getTables()) { -// if (metadata.table().equals(table.getName()) && metadata.schema().equals(schema.getName())) { sb = new StringBuilder(); sb.append("CREATE TABLE IF NOT EXISTS \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" (\n"); for (ColumnMetadata idColumn : table.getIdColumns()) { @@ -84,7 +84,6 @@ private List getDefs(Collection schemas) { //todo: add sb.append(")\n"); sb.append(");"); statements.add(DDLStatement.both(sb.toString())); -// } if (!table.getColumns().isEmpty()) { for (SQLColumn column : table.getColumns()) { sb = new StringBuilder(); @@ -96,11 +95,6 @@ private List getDefs(Collection schemas) { //todo: add sb.append(" DEFAULT ").append(column.getDefaultValue()); } - if (column.isIndexed()) { -// sb.append(" INDEXED"); - //todo: this is not valid sql, need to create index separately - } - if (column.isUnique()) { sb.append(" UNIQUE"); } @@ -112,6 +106,18 @@ private List getDefs(Collection schemas) { //todo: add } } + for (SQLSchema schema : schemas) { + for (SQLTable table : schema.getTables()) { + for (SQLColumn column : table.getColumns()) { + 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)); + } + } + } + } + // define fkeys after table creation, to ensure all tables exist before adding fkeys for (SQLSchema schema : schemas) { for (SQLTable table : schema.getTables()) { diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index 2128f1ba..641910df 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -92,6 +92,7 @@ public void testParse() throws Exception { ADD CONSTRAINT posts_metadata_pkey PRIMARY KEY (metadata_id); ALTER TABLE ONLY social_media.posts ADD CONSTRAINT posts_pkey PRIMARY KEY (post_id); + CREATE INDEX idx_social_media_posts_text_content ON social_media.posts USING btree (text_content); ALTER TABLE ONLY social_media.posts_interactions ADD CONSTRAINT fk_social_media_posts_interactions_post_id_to_posts_post_id FOREIGN KEY (post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY social_media.posts @@ -99,6 +100,5 @@ public void testParse() throws Exception { """; assertEquals(expected.trim(), cleanedDump.toString().trim()); - //todo: test with a reference } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/MockUser.java index 96e85da3..f2148784 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/MockUser.java @@ -21,7 +21,6 @@ public class MockUser extends UniqueData { @ForeignColumn(name = "fav_color", table = "user_preferences", nullable = true, link = "id=user_id") public PersistentValue favoriteColor; @OneToOne(link = "settings_id=user_id", deleteStrategy = DeleteStrategy.CASCADE) - //todo: this should generate an fkey (add an option to control what happens on update/delete) public Reference settings; @ForeignColumn(name = "name_updates", table = "user_metadata", link = "id=user_id", defaultValue = "0") public PersistentValue nameUpdates; @@ -31,8 +30,6 @@ public class MockUser extends UniqueData { user.nameUpdates.set(user.getNameUpdates() + 1); }); - //todo: add support for unique constraints and test them. - public int getNameUpdates() { return nameUpdates.get(); } diff --git a/src/test/java/net/staticstudios/data/mock/post/MockPost.java b/src/test/java/net/staticstudios/data/mock/post/MockPost.java index 9d7ced74..e9944198 100644 --- a/src/test/java/net/staticstudios/data/mock/post/MockPost.java +++ b/src/test/java/net/staticstudios/data/mock/post/MockPost.java @@ -12,7 +12,7 @@ public class MockPost extends UniqueData { @OneToOne(link = "${POST_ID_COLUMN}=metadata_id") public Reference metadata; - @Column(name = "text_content") + @Column(name = "text_content", index = true) public PersistentValue textContent; @Column(name = "likes", defaultValue = "0") public PersistentValue likes; From 6259320b2d601598bb940ce4e9162642a0655c09 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 22 Sep 2025 20:46:24 -0400 Subject: [PATCH 15/75] add tests --- .../data/impl/data/PersistentValueImpl.java | 2 - .../net/staticstudios/data/QueryTest.java | 156 ++++++++++++++---- 2 files changed, 126 insertions(+), 32 deletions(-) diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index af1c5c5d..8417ebed 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -140,6 +140,4 @@ public void set(T value) { Preconditions.checkArgument(!holder.isDeleted(), "Cannot set value on a deleted UniqueData instance"); holder.getDataManager().set(schema, table, column, holder.getIdColumns(), idColumnLinks, value); } - - //todo: support set with SetMode, or operationMode (SYNC vs ASYNC) } diff --git a/src/test/java/net/staticstudios/data/QueryTest.java b/src/test/java/net/staticstudios/data/QueryTest.java index 34c1a246..6756b4d4 100644 --- a/src/test/java/net/staticstudios/data/QueryTest.java +++ b/src/test/java/net/staticstudios/data/QueryTest.java @@ -116,35 +116,131 @@ public void testQueryOnForeignColumn() { } @Test - public void testQuery() { //todo: test each clause and ensure it outputs the proper sql - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - - String where = MockUserQuery.where(dataManager) - .idIs(UUID.randomUUID()) - .or() -// .nameIs("user name") -// .or(q -> q.ageIsLessThan(10) -// .and() -// .ageIsLessThanOrEqualTo(5) -// ) -// .or() -// .nameIsIn("name1", "name2", "name3") -// .or() -// .nameIsIn(List.of("name4", "name5", "name6")) -// .limit(10) -// .offset(2) -// .and() -// .ageIsBetween(0, 3) -// .and() -// .ageIsNotNull() -// .and() -// .nameIsLike("%something%") -// .and() -// .nameIsNotLike("%nothing%") -// .orderByAge(Order.ASCENDING) -// .and() - .nameUpdatesIs(4) - .toString(); - System.out.println(where); + public void testEqualsClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"id\" = ?", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).toString()); + } + + @Test + public void testBetweenClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"age\" BETWEEN ? AND ?", MockUserQuery.where(dataManager).ageIsBetween(0, 0).toString()); + } + + @Test + public void testAgeIsLessThanClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"age\" < ?", MockUserQuery.where(dataManager).ageIsLessThan(0).toString()); + } + + @Test + public void testAgeIsLessThanOrEqualToClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"age\" <= ?", MockUserQuery.where(dataManager).ageIsLessThanOrEqualTo(0).toString()); + } + + @Test + public void testAgeIsGreaterThanClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"age\" > ?", MockUserQuery.where(dataManager).ageIsGreaterThan(0).toString()); + } + + @Test + public void testAgeIsGreaterThanOrEqualToClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"age\" >= ?", MockUserQuery.where(dataManager).ageIsGreaterThanOrEqualTo(0).toString()); + } + + @Test + public void testAgeIsNullClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"age\" IS NULL", MockUserQuery.where(dataManager).ageIsNull().toString()); + } + + @Test + public void testAgeIsNotNullClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"age\" IS NOT NULL", MockUserQuery.where(dataManager).ageIsNotNull().toString()); + } + + @Test + public void testNameIsLikeClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"name\" LIKE ?", MockUserQuery.where(dataManager).nameIsLike("%test%").toString()); + } + + @Test + public void testNameIsNotLikeClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"name\" NOT LIKE ?", MockUserQuery.where(dataManager).nameIsNotLike("%test%").toString()); + } + + @Test + public void testNameIsInClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"name\" IN (?, ?, ?)", MockUserQuery.where(dataManager).nameIsIn("name1", "name2", "name3").toString()); + } + + @Test + public void testNameIsInListClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"name\" IN (?, ?, ?)", MockUserQuery.where(dataManager).nameIsIn(List.of("name1", "name2", "name3")).toString()); + } + + @Test + public void testLimitClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"id\" = ? LIMIT 10", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).limit(10).toString()); + } + + @Test + public void testOffsetClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"id\" = ? OFFSET 5", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).offset(5).toString()); + } + + @Test + public void testOrderByClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"id\" = ? ORDER BY \"public\".\"users\".\"age\" ASC", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).orderByAge(Order.ASCENDING).toString()); + } + + @Test + public void testAndClauseWithoutParentheses() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"id\" = ? AND \"public\".\"users\".\"age\" BETWEEN ? AND ?", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).and().ageIsBetween(0, 5).toString()); + } + + @Test + public void testOrClauseWithoutParentheses() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"id\" = ? OR \"public\".\"users\".\"age\" BETWEEN ? AND ?", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).or().ageIsBetween(0, 5).toString()); + } + + @Test + public void testAndClauseWithParentheses() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"id\" = ? AND (\"public\".\"users\".\"age\" BETWEEN ? AND ?)", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).and(q -> q.ageIsBetween(0, 5)).toString()); + } + + @Test + public void testOrClauseWithParentheses() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + assertEquals("WHERE \"public\".\"users\".\"id\" = ? OR (\"public\".\"users\".\"age\" BETWEEN ? AND ?)", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).or(q -> q.ageIsBetween(0, 5)).toString()); + } + + @Test + public void testComplexClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + 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", + MockUserQuery.where(dataManager) + .idIs(UUID.randomUUID()) + .or(q -> q.ageIsBetween(0, 5)) + .and() + .nameIsLike("%test%") + .orderByAge(Order.DESCENDING) + .limit(10) + .offset(5) + .toString()); } } \ No newline at end of file From eda4d7d38bf320fe53bf1d152a855fdaf8797dd6 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 22 Sep 2025 21:38:56 -0400 Subject: [PATCH 16/75] custom types --- .../net/staticstudios/data/DataManager.java | 80 ++++++++++++++++--- .../staticstudios/data/ValueSerializer.java | 41 ++++++++++ .../net/staticstudios/data/delete/Delete.java | 11 --- .../data/impl/data/PersistentValueImpl.java | 4 +- .../data/impl/data/ReferenceImpl.java | 4 +- .../net/staticstudios/data/insert/Insert.java | 11 --- .../staticstudios/data/parse/SQLBuilder.java | 2 +- .../data/primative/Primitive.java | 13 +-- .../data/primative/PrimitiveBuilder.java | 19 +---- .../data/primative/Primitives.java | 23 +----- .../staticstudios/data/CustomTypeTest.java | 68 ++++++++++++++++ .../data/PersistentValueTest.java | 4 +- .../net/staticstudios/data/QueryTest.java | 6 +- .../net/staticstudios/data/ReferenceTest.java | 8 +- .../data/mock/account/AccountDetails.java | 4 + .../AccountDetailsValueSerializer.java | 34 ++++++++ .../data/mock/account/AccountSettings.java | 4 + .../AccountSettingsValueSerializer.java | 34 ++++++++ .../data/mock/account/MockAccount.java | 13 +++ .../data/mock/{ => user}/MockUser.java | 2 +- .../mock/{ => user}/MockUserSettings.java | 2 +- 21 files changed, 286 insertions(+), 101 deletions(-) create mode 100644 src/main/java/net/staticstudios/data/ValueSerializer.java delete mode 100644 src/main/java/net/staticstudios/data/delete/Delete.java delete mode 100644 src/main/java/net/staticstudios/data/insert/Insert.java create mode 100644 src/test/java/net/staticstudios/data/CustomTypeTest.java create mode 100644 src/test/java/net/staticstudios/data/mock/account/AccountDetails.java create mode 100644 src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/account/AccountSettings.java create mode 100644 src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/account/MockAccount.java rename src/test/java/net/staticstudios/data/mock/{ => user}/MockUser.java (97%) rename src/test/java/net/staticstudios/data/mock/{ => user}/MockUserSettings.java (88%) diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 4a4dd364..15b267c7 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -8,6 +8,7 @@ import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.insert.InsertContext; import net.staticstudios.data.parse.*; +import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; import net.staticstudios.utils.ThreadUtils; @@ -38,6 +39,8 @@ public class DataManager { private final ConcurrentHashMap, List>>> updateHandlers = new ConcurrentHashMap<>(); private final PostgresListener postgresListener; private final Set registeredUpdateHandlersForColumns = Collections.synchronizedSet(new HashSet<>()); + private final List> valueSerializers = new CopyOnWriteArrayList<>(); + //todo: custom types are serialized and deserialized all the time currently, we should have a cache for these. caffeine with time based eviction sounds good. public DataManager(DataSourceConfig dataSourceConfig) { this(dataSourceConfig, true); @@ -123,10 +126,10 @@ public void callUpdateHandlers(List columnNames, String schema, String t UniqueData instance = getInstance(holderClass, idColumns); for (ValueUpdateHandlerWrapper wrapper : handlers) { Class dataType = wrapper.getDataType(); - Object deserializedOldValue = oldSerializedValues[columnIndex]; //todo: these - Object deserializedNewValue = newSerializedValues[columnIndex]; + Object deserializedOldValue = deserialize(dataType, oldSerializedValues[columnIndex]); + Object deserializedNewValue = deserialize(dataType, newSerializedValues[columnIndex]); - //todo: submit to somewhere for where to run these, configured during setup. default to thread utils + //todo: allow configuring where to submit update handlers to. note that we cannot call them immediately since we are inside a transaction. ThreadUtils.submit(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); } } @@ -552,7 +555,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { List values = new ArrayList<>(); for (SimpleColumnMetadata column : columnsInTable) { Object deserializedValue = insertContext.getEntries().get(column); - Object serializedValue = deserializedValue; //todo: serialization + Object serializedValue = serialize(column.type(), deserializedValue); values.add(serializedValue); } sqlStatements.add(new SQlStatement(sql, values)); @@ -580,17 +583,14 @@ public T get(String schema, String table, String column, ColumnValuePairs id if (rs.next()) { serializedValue = rs.getObject(column); } - if (serializedValue != null) { - //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 - T deserialized = (T) serializedValue; //todo: this - return deserialized; - } - return null; + //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); } catch (SQLException e) { throw new RuntimeException(e); } } + @ApiStatus.Internal public void set(String schema, String table, String column, ColumnValuePairs idColumns, Map idColumnLinks, Object value) { StringBuilder sqlBuilder; if (idColumnLinks.isEmpty()) { @@ -628,7 +628,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC } @Language("SQL") String sql = sqlBuilder.toString(); List values = new ArrayList<>(1 + idColumns.getPairs().length); - values.add(value); + values.add(serialize(value.getClass(), value)); for (ColumnValuePair columnValuePair : idColumns) { values.add(columnValuePair.value()); } @@ -667,4 +667,62 @@ private void topoSort(SQLTable table, Map> dependencyGraph } ordered.add(table); } + + public void registerValueSerializer(ValueSerializer serializer) { + if (Primitives.isPrimitive(serializer.getDeserializedType())) { + throw new IllegalArgumentException("Cannot register a serializer for a primitive type"); + } + + if (!Primitives.isPrimitive(serializer.getSerializedType())) { + throw new IllegalArgumentException("Cannot register a ValueSerializer that serializes to a non-primitive type"); + } + + for (ValueSerializer s : valueSerializers) { + if (s.getDeserializedType().isAssignableFrom(serializer.getDeserializedType())) { + throw new IllegalArgumentException("A serializer for " + serializer.getDeserializedType() + " is already registered! (" + s.getClass() + ")"); + } + } + + valueSerializers.add(serializer); + } + + private ValueSerializer getValueSerializer(Class deserializedType) { + for (ValueSerializer s : valueSerializers) { + if (s.getDeserializedType().isAssignableFrom(deserializedType)) { + return s; + } + } + + throw new IllegalStateException("No ValueSerializer registered for type " + deserializedType.getName()); + } + + private T deserialize(Class clazz, Object serialized) { + if (Primitives.isPrimitive(clazz) || serialized == null) { + return (T) serialized; + } + return (T) deserialize(getValueSerializer(clazz), serialized); + } + + private T serialize(Class clazz, Object deserialized) { + if (Primitives.isPrimitive(clazz) || deserialized == null) { + return (T) deserialized; + } + return (T) serialize(getValueSerializer(clazz), deserialized); + } + + private D deserialize(ValueSerializer serializer, Object serialized) { + return serializer.deserialize(serializer.getSerializedType().cast(serialized)); + } + + private S serialize(ValueSerializer serializer, Object deserialized) { + return serializer.serialize(serializer.getDeserializedType().cast(deserialized)); + } + + public Class getSerializedType(Class clazz) { + if (Primitives.isPrimitive(clazz)) { + return clazz; + } + ValueSerializer serializer = getValueSerializer(clazz); + return serializer.getSerializedType(); + } } diff --git a/src/main/java/net/staticstudios/data/ValueSerializer.java b/src/main/java/net/staticstudios/data/ValueSerializer.java new file mode 100644 index 00000000..df52b82c --- /dev/null +++ b/src/main/java/net/staticstudios/data/ValueSerializer.java @@ -0,0 +1,41 @@ +package net.staticstudios.data; + +/** + * A serializer for non-primitive types. + * See {@link net.staticstudios.data.primative.Primitives} for primitive types. + * Nullability depends on the implementation. + * + * @param The deserialized type + * @param The serialized type + */ +public interface ValueSerializer { + /** + * Deserialize the serialized object + * + * @param serialized The serialized object + * @return The deserialized object + */ + D deserialize(S serialized); + + /** + * Serialize the deserialized object + * + * @param deserialized The deserialized object + * @return The serialized object + */ + S serialize(D deserialized); + + /** + * Get the deserialized type + * + * @return The deserialized type + */ + Class getDeserializedType(); + + /** + * Get the serialized type + * + * @return The serialized type + */ + Class getSerializedType(); +} diff --git a/src/main/java/net/staticstudios/data/delete/Delete.java b/src/main/java/net/staticstudios/data/delete/Delete.java deleted file mode 100644 index 6b4fac53..00000000 --- a/src/main/java/net/staticstudios/data/delete/Delete.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.staticstudios.data.delete; - -import net.staticstudios.data.DeleteStrategy; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -public @interface Delete { - DeleteStrategy value() default DeleteStrategy.NO_ACTION; -} diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index 8417ebed..16f49d6a 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -47,7 +47,7 @@ public static PersistentValueImpl create(DataAccessor dataAccessor, Uniqu return new PersistentValueImpl<>(dataAccessor, holder, dataType, schema, table, column, idColumnLinks); } - public static void delegate(String schema, String table, T instance) { + public static void delegate(String schema, String table, T instance) { //todo: cache this info. can we use the uniquedatametadata? for (FieldInstancePair<@Nullable PersistentValue> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentValue.class)) { IdColumn idColumn = pair.field().getAnnotation(IdColumn.class); Column columnAnnotation = pair.field().getAnnotation(Column.class); @@ -101,7 +101,7 @@ public static void delegate(String schema, String table, } else { pair.field().setAccessible(true); try { - pair.field().set(instance, PersistentValueImpl.create(instance.getDataManager().getDataAccessor(), instance, pair.field().getType(), columnMetadata.schema(), columnMetadata.table(), columnMetadata.name(), idColumnLinks)); + pair.field().set(instance, PersistentValueImpl.create(instance.getDataManager().getDataAccessor(), instance, ReflectionUtils.getGenericType(pair.field()), columnMetadata.schema(), columnMetadata.table(), columnMetadata.name(), idColumnLinks)); } catch (IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java index 3b1762ca..eef1a917 100644 --- a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -40,7 +40,7 @@ public static ReferenceImpl create(UniqueData holder, return new ReferenceImpl<>(holder, type, link); } - public static void delegate(T instance) { + public static void delegate(T instance) { //todo: cache this info. can we use the uniquedatametadata? for (FieldInstancePair<@Nullable Reference> pair : ReflectionUtils.getFieldInstancePairs(instance, Reference.class)) { OneToOne oneToOneAnnotation = pair.field().getAnnotation(OneToOne.class); Preconditions.checkNotNull(oneToOneAnnotation, "Field %s in class %s is missing @OneToOne annotation".formatted(pair.field().getName(), instance.getClass().getName())); @@ -122,7 +122,7 @@ public void set(@Nullable T value) { sqlBuilder.append("\"").append(myColumn).append("\" = NULL, "); continue; } - + String theirColumn = entry.getValue(); Object theirValue = null; for (ColumnValuePair columnValuePair : value.getIdColumns()) { diff --git a/src/main/java/net/staticstudios/data/insert/Insert.java b/src/main/java/net/staticstudios/data/insert/Insert.java deleted file mode 100644 index 88b79201..00000000 --- a/src/main/java/net/staticstudios/data/insert/Insert.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.staticstudios.data.insert; - -import net.staticstudios.data.InsertStrategy; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -public @interface Insert { - InsertStrategy value() default InsertStrategy.OVERWRITE_EXISTING; -} diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index c8cfdfc7..c47b6471 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -366,7 +366,7 @@ private void parseColumn(Class clazz, Map type = ReflectionUtils.getGenericType(field); //todo: handle custom types to sql types + Class type = dataManager.getSerializedType(ReflectionUtils.getGenericType(field)); SQLColumn sqlColumn = new SQLColumn(table, type, columnName, nullable, indexed, unique, defaultValue.isEmpty() ? null : SQLUtils.parseDefaultValue(type, defaultValue)); SQLColumn existingColumn = table.getColumn(columnName); diff --git a/src/main/java/net/staticstudios/data/primative/Primitive.java b/src/main/java/net/staticstudios/data/primative/Primitive.java index 2634fd64..96db9d99 100644 --- a/src/main/java/net/staticstudios/data/primative/Primitive.java +++ b/src/main/java/net/staticstudios/data/primative/Primitive.java @@ -6,15 +6,11 @@ public class Primitive { private final Class runtimeType; private final Function decoder; private final Function encoder; - private final boolean nullable; - private final T defaultValue; - public Primitive(Class runtimeType, Function decoder, Function encoder, boolean nullable, T defaultValue) { + public Primitive(Class runtimeType, Function decoder, Function encoder) { this.runtimeType = runtimeType; this.decoder = decoder; this.encoder = encoder; - this.nullable = nullable; - this.defaultValue = defaultValue; } public static PrimitiveBuilder builder(Class runtimeType) { @@ -33,13 +29,6 @@ public String unsafeEncode(Object value) { return encoder.apply(runtimeType.cast(value)); } - public boolean isNullable() { - return nullable; - } - - public T getDefaultValue() { - return defaultValue; - } public Class getRuntimeType() { return runtimeType; diff --git a/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java b/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java index e40bed41..9574564a 100644 --- a/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java +++ b/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java @@ -9,8 +9,6 @@ public class PrimitiveBuilder { private final Class runtimeType; private Function decoder; private Function encoder; - private Boolean nullable; - private T defaultValue; public PrimitiveBuilder(Class runtimeType) { this.runtimeType = runtimeType; @@ -32,27 +30,12 @@ public PrimitiveBuilder encoder(Function encoder) { return this; } - public PrimitiveBuilder nullable(boolean nullable) { - this.nullable = nullable; - return this; - } - - public PrimitiveBuilder defaultValue(T defaultValue) { - this.defaultValue = defaultValue; - return this; - } - public Primitive build(Consumer> consumer) { Preconditions.checkNotNull(decoder, "Decoder is null"); Preconditions.checkNotNull(encoder, "Encoder is null"); Preconditions.checkNotNull(consumer, "Consumer is null"); - Preconditions.checkNotNull(nullable, "Nullable flag is null"); - - if (!nullable) { - Preconditions.checkNotNull(defaultValue, "Default value is null"); - } - Primitive primitive = new Primitive<>(runtimeType, decoder, encoder, nullable, defaultValue); + Primitive primitive = new Primitive<>(runtimeType, decoder, encoder); consumer.accept(primitive); return primitive; diff --git a/src/main/java/net/staticstudios/data/primative/Primitives.java b/src/main/java/net/staticstudios/data/primative/Primitives.java index 8d3bd07c..1e35c250 100644 --- a/src/main/java/net/staticstudios/data/primative/Primitives.java +++ b/src/main/java/net/staticstudios/data/primative/Primitives.java @@ -12,8 +12,7 @@ import java.util.Map; @SuppressWarnings("unused") -public class Primitives { - // General rule of thumb: if the Primitive is a Java primitive, it should not be nullable, everything else should be nullable. +public class Primitives { //todo: test each of these in h2 and pg. private static final DateTimeFormatter TIMESTAMP_FORMATTER = new DateTimeFormatterBuilder() .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) .appendPattern("xxx") @@ -22,65 +21,46 @@ public class Primitives { private static Map, Primitive> primitives; public static final Primitive STRING = Primitive.builder(String.class) - .nullable(true) .encoder(s -> s) .decoder(s -> s) .build(Primitives::register); public static final Primitive CHARACTER = Primitive.builder(Character.class) - .nullable(false) - .defaultValue((char) 0) .encoder(c -> Character.toString(c)) .decoder(s -> s.charAt(0)) .build(Primitives::register); public static final Primitive BYTE = Primitive.builder(Byte.class) - .nullable(false) - .defaultValue((byte) 0) .encoder(b -> Byte.toString(b)) .decoder(Byte::parseByte) .build(Primitives::register); public static final Primitive SHORT = Primitive.builder(Short.class) - .nullable(false) - .defaultValue((short) 0) .encoder(s -> Short.toString(s)) .decoder(Short::parseShort) .build(Primitives::register); public static final Primitive INTEGER = Primitive.builder(Integer.class) - .nullable(false) - .defaultValue(0) .encoder(i -> Integer.toString(i)) .decoder(Integer::parseInt) .build(Primitives::register); public static final Primitive LONG = Primitive.builder(Long.class) - .nullable(false) - .defaultValue(0L) .encoder(l -> Long.toString(l)) .decoder(Long::parseLong) .build(Primitives::register); public static final Primitive FLOAT = Primitive.builder(Float.class) - .nullable(false) - .defaultValue(0.0f) .encoder(f -> Float.toString(f)) .decoder(Float::parseFloat) .build(Primitives::register); public static final Primitive DOUBLE = Primitive.builder(Double.class) - .nullable(false) - .defaultValue(0.0) .encoder(d -> Double.toString(d)) .decoder(Double::parseDouble) .build(Primitives::register); public static final Primitive BOOLEAN = Primitive.builder(Boolean.class) - .nullable(false) - .defaultValue(false) .encoder(b -> Boolean.toString(b)) .decoder(Boolean::parseBoolean) .build(Primitives::register); public static final Primitive UUID = Primitive.builder(java.util.UUID.class) - .nullable(true) .encoder(uuid -> uuid == null ? null : uuid.toString()) .decoder(s -> s == null ? null : java.util.UUID.fromString(s)) .build(Primitives::register); public static final Primitive TIMESTAMP = Primitive.builder(Timestamp.class) - .nullable(true) .encoder(timestamp -> { if (timestamp == null) { return null; @@ -98,7 +78,6 @@ public class Primitives { }) .build(Primitives::register); public static final Primitive BYTE_ARRAY = Primitive.builder(byte[].class) - .nullable(true) .encoder(b -> { if (b == null) { return null; diff --git a/src/test/java/net/staticstudios/data/CustomTypeTest.java b/src/test/java/net/staticstudios/data/CustomTypeTest.java new file mode 100644 index 00000000..2f07dccb --- /dev/null +++ b/src/test/java/net/staticstudios/data/CustomTypeTest.java @@ -0,0 +1,68 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.mock.account.*; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.*; + +public class CustomTypeTest extends DataTest { + @Test + public void testCustomTypesSetGet() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new AccountDetailsValueSerializer()); + dataManager.registerValueSerializer(new AccountSettingsValueSerializer()); + dataManager.load(MockAccount.class); + + MockAccount account = MockAccountFactory.builder(dataManager) + .id(1) + .insert(InsertMode.SYNC); + + assertNull(account.settings.get()); + assertNull(account.details.get()); + + AccountDetails details = new AccountDetails("detail1", "detail2"); + AccountSettings settings = new AccountSettings(true, false); + account.settings.set(settings); + account.details.set(details); + + assertEquals(settings, account.settings.get()); + assertEquals(details, account.details.get()); + } + + @Test + public void testCustomTypesLoad() throws SQLException { + AccountDetailsValueSerializer detailsSerializer = new AccountDetailsValueSerializer(); + AccountSettingsValueSerializer settingsSerializer = new AccountSettingsValueSerializer(); + AccountDetails details = new AccountDetails("detail1", "detail2"); + AccountSettings settings = new AccountSettings(true, false); + + String detailsSerialized = detailsSerializer.serialize(details); + String settingsSerialized = settingsSerializer.serialize(settings); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE \"public\".\"accounts\" (\"id\" INTEGER PRIMARY KEY, \"settings\" TEXT NULL);"); + statement.execute("CREATE TABLE \"public\".\"account_details\" (\"account_id\" INTEGER PRIMARY KEY, \"details\" TEXT NULL, FOREIGN KEY (\"account_id\") REFERENCES \"accounts\"(\"id\") ON DELETE CASCADE);"); + + statement.execute("INSERT INTO \"public\".\"accounts\" (\"id\", \"settings\") VALUES (1, '" + settingsSerialized + "');"); + statement.execute("INSERT INTO \"public\".\"account_details\" (\"account_id\", \"details\") VALUES (1, '" + detailsSerialized + "');"); + } + + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(detailsSerializer); + dataManager.registerValueSerializer(settingsSerializer); + dataManager.load(MockAccount.class); + + MockAccount account = MockAccountQuery.where(dataManager).idIs(1).findOne(); + assertNotNull(account); + assertEquals(settings, account.settings.get()); + assertEquals(details, account.details.get()); + } + + //todo: test postgres listen/notify with custom types +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index e65782df..77479ca4 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -2,8 +2,8 @@ import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.misc.MockEnvironment; -import net.staticstudios.data.mock.MockUser; -import net.staticstudios.data.mock.MockUserFactory; +import net.staticstudios.data.mock.user.MockUser; +import net.staticstudios.data.mock.user.MockUserFactory; import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/staticstudios/data/QueryTest.java b/src/test/java/net/staticstudios/data/QueryTest.java index 6756b4d4..7a0df304 100644 --- a/src/test/java/net/staticstudios/data/QueryTest.java +++ b/src/test/java/net/staticstudios/data/QueryTest.java @@ -1,9 +1,9 @@ package net.staticstudios.data; import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.mock.MockUser; -import net.staticstudios.data.mock.MockUserFactory; -import net.staticstudios.data.mock.MockUserQuery; +import net.staticstudios.data.mock.user.MockUser; +import net.staticstudios.data.mock.user.MockUserFactory; +import net.staticstudios.data.mock.user.MockUserQuery; import net.staticstudios.data.query.Order; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/staticstudios/data/ReferenceTest.java b/src/test/java/net/staticstudios/data/ReferenceTest.java index 565a6e54..453dddf1 100644 --- a/src/test/java/net/staticstudios/data/ReferenceTest.java +++ b/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -2,10 +2,10 @@ import net.staticstudios.data.insert.InsertContext; import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.mock.MockUser; -import net.staticstudios.data.mock.MockUserFactory; -import net.staticstudios.data.mock.MockUserSettings; -import net.staticstudios.data.mock.MockUserSettingsFactory; +import net.staticstudios.data.mock.user.MockUser; +import net.staticstudios.data.mock.user.MockUserFactory; +import net.staticstudios.data.mock.user.MockUserSettings; +import net.staticstudios.data.mock.user.MockUserSettingsFactory; import org.junit.jupiter.api.Test; import java.util.UUID; diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountDetails.java b/src/test/java/net/staticstudios/data/mock/account/AccountDetails.java new file mode 100644 index 00000000..e4b3bdfe --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/account/AccountDetails.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.mock.account; + +public record AccountDetails(String detail1, String detail2) { +} diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java b/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java new file mode 100644 index 00000000..e88ef49f --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.mock.account; + +import com.google.gson.Gson; +import net.staticstudios.data.ValueSerializer; + +public class AccountDetailsValueSerializer implements ValueSerializer { + private static final Gson GSON = new Gson(); + + @Override + public AccountDetails deserialize(String serialized) { + if (serialized == null || serialized.isEmpty()) { + return null; + } + return GSON.fromJson(serialized, AccountDetails.class); + } + + @Override + public String serialize(AccountDetails deserialized) { + if (deserialized == null) { + return null; + } + return GSON.toJson(deserialized); + } + + @Override + public Class getDeserializedType() { + return AccountDetails.class; + } + + @Override + public Class getSerializedType() { + return String.class; + } +} diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountSettings.java b/src/test/java/net/staticstudios/data/mock/account/AccountSettings.java new file mode 100644 index 00000000..8a63a906 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/account/AccountSettings.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.mock.account; + +public record AccountSettings(boolean flag1, boolean flag2) { +} diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java b/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java new file mode 100644 index 00000000..8aaffbc5 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.mock.account; + +import com.google.gson.Gson; +import net.staticstudios.data.ValueSerializer; + +public class AccountSettingsValueSerializer implements ValueSerializer { + private static final Gson GSON = new Gson(); + + @Override + public AccountSettings deserialize(String serialized) { + if (serialized == null || serialized.isEmpty()) { + return null; + } + return GSON.fromJson(serialized, AccountSettings.class); + } + + @Override + public String serialize(AccountSettings deserialized) { + if (deserialized == null) { + return null; + } + return GSON.toJson(deserialized); + } + + @Override + public Class getDeserializedType() { + return AccountSettings.class; + } + + @Override + public Class getSerializedType() { + return String.class; + } +} diff --git a/src/test/java/net/staticstudios/data/mock/account/MockAccount.java b/src/test/java/net/staticstudios/data/mock/account/MockAccount.java new file mode 100644 index 00000000..305f4c51 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/account/MockAccount.java @@ -0,0 +1,13 @@ +package net.staticstudios.data.mock.account; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "accounts") +public class MockAccount extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "settings", nullable = true) + public PersistentValue settings; + @ForeignColumn(name = "details", table = "account_details", nullable = true, link = "id=account_id") + public PersistentValue details; +} diff --git a/src/test/java/net/staticstudios/data/mock/MockUser.java b/src/test/java/net/staticstudios/data/mock/user/MockUser.java similarity index 97% rename from src/test/java/net/staticstudios/data/mock/MockUser.java rename to src/test/java/net/staticstudios/data/mock/user/MockUser.java index f2148784..ad519250 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.mock; +package net.staticstudios.data.mock.user; import net.staticstudios.data.*; diff --git a/src/test/java/net/staticstudios/data/mock/MockUserSettings.java b/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java similarity index 88% rename from src/test/java/net/staticstudios/data/mock/MockUserSettings.java rename to src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java index 0b483771..66562717 100644 --- a/src/test/java/net/staticstudios/data/mock/MockUserSettings.java +++ b/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.mock; +package net.staticstudios.data.mock.user; import net.staticstudios.data.*; From 7ebf803d160759cb3ad036f540ecbca3a5418987 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 22 Sep 2025 23:01:08 -0400 Subject: [PATCH 17/75] cleanup --- .../data/PersistentValueTest.java | 119 ----------------- .../net/staticstudios/data/SQLParseTest.java | 15 --- .../staticstudios/data/ValueParseTest.java | 32 ----- .../net/staticstudios/data/misc/DataTest.java | 112 ---------------- .../data/misc/MockEnvironment.java | 10 -- .../data/misc/MockThreadProvider.java | 125 ------------------ .../staticstudios/data/misc/TestUtils.java | 26 ---- .../net/staticstudios/data/mock/MockUser.java | 61 --------- .../data/mock/MockUserSettings.java | 27 ---- oldsrc/test/resources/log4j.properties | 6 - 10 files changed, 533 deletions(-) delete mode 100644 oldsrc/test/java/net/staticstudios/data/PersistentValueTest.java delete mode 100644 oldsrc/test/java/net/staticstudios/data/SQLParseTest.java delete mode 100644 oldsrc/test/java/net/staticstudios/data/ValueParseTest.java delete mode 100644 oldsrc/test/java/net/staticstudios/data/misc/DataTest.java delete mode 100644 oldsrc/test/java/net/staticstudios/data/misc/MockEnvironment.java delete mode 100644 oldsrc/test/java/net/staticstudios/data/misc/MockThreadProvider.java delete mode 100644 oldsrc/test/java/net/staticstudios/data/misc/TestUtils.java delete mode 100644 oldsrc/test/java/net/staticstudios/data/mock/MockUser.java delete mode 100644 oldsrc/test/java/net/staticstudios/data/mock/MockUserSettings.java delete mode 100644 oldsrc/test/resources/log4j.properties diff --git a/oldsrc/test/java/net/staticstudios/data/PersistentValueTest.java b/oldsrc/test/java/net/staticstudios/data/PersistentValueTest.java deleted file mode 100644 index 8893abc6..00000000 --- a/oldsrc/test/java/net/staticstudios/data/PersistentValueTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.mock.MockUser; -import net.staticstudios.data.mock.MockUserSettings; -import net.staticstudios.data.util.ColumnValuePair; -import org.junit.jupiter.api.Test; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -public class PersistentValueTest extends DataTest { - - @Test - public void testReadData() throws SQLException { - List userIds = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - userIds.add(UUID.randomUUID()); - } - - try (PreparedStatement ps = getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS public.users (id UUID PRIMARY KEY, name TEXT, age INT)")) { - ps.execute(); - } - - Connection connection = getConnection(); - try (PreparedStatement ps = connection.prepareStatement("INSERT INTO public.users (id, name) VALUES (?, ?)")) { - for (UUID id : userIds) { - ps.setObject(1, id); - ps.setString(2, "user " + id); - ps.addBatch(); - } - ps.executeBatch(); - } - - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - dataManager.load(MockUser.class); - for (UUID id : userIds) { - MockUser user = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); - assertEquals("user " + id, user.name.get()); - assertNull(user.age.get()); - } - - waitForDataPropagation(); - } - - @Test - public void test() throws SQLException { - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - dataManager.load(MockUser.class); - UUID id = UUID.randomUUID(); - MockUser mockUser = MockUser.create(dataManager, id, "test user"); - assertEquals("test user", mockUser.name.get()); - mockUser.name.set("updated name"); - assertEquals("updated name", mockUser.name.get()); - - assertNull(mockUser.age.get()); - mockUser.age.set(25); - assertEquals(25, mockUser.age.get()); - - mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); - mockUser = null; // remove strong reference - System.gc(); - mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss - - assertNull(mockUser.favoriteColor.get()); - mockUser.favoriteColor.set("blue"); - assertEquals("blue", mockUser.favoriteColor.get()); - -// long start; -// int count = 10_000; -// for (int j = 0; j < 5; j++) { -// start = System.currentTimeMillis(); -// for (int i = 0; i < count; i++) { -// mockUser.name.set("name " + i); -// } -// -// System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " updates"); -// } -// for (int j = 0; j < 5; j++) { -// start = System.currentTimeMillis(); -// for (int i = 0; i < count; i++) { -// mockUser.name.get(); -// } -// System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " gets"); -// } - - waitForDataPropagation(); - - MockUserSettings settings = MockUserSettings.create(dataManager, UUID.randomUUID()); - assertNull(mockUser.settings.get()); - mockUser.settings.set(settings); - assertEquals(settings, mockUser.settings.get()); - } - - @Test - public void testUpdateHandlerRegistration() { - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - dataManager.load(MockUser.class); - UUID id = UUID.randomUUID(); - assertEquals(0, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); - MockUser mockUser = MockUser.create(dataManager, id, "test user"); - //first instance was created, handler should be registered - assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); - mockUser = null; // remove strong reference - System.gc(); - mockUser = dataManager.get(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss - //the handler for this pv should not have been registered again - assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); - - mockUser.name.set("new name"); - } -} \ No newline at end of file diff --git a/oldsrc/test/java/net/staticstudios/data/SQLParseTest.java b/oldsrc/test/java/net/staticstudios/data/SQLParseTest.java deleted file mode 100644 index 2097e9c0..00000000 --- a/oldsrc/test/java/net/staticstudios/data/SQLParseTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.mock.MockUser; -import org.junit.jupiter.api.Test; - -public class SQLParseTest extends DataTest { - - @Test - public void testParse() { - DataManager dm = getMockEnvironments().getFirst().dataManager(); - dm.extractMetadata(MockUser.class); - dm.getSQLBuilder().parse(MockUser.class).forEach(System.out::println); - } -} \ No newline at end of file diff --git a/oldsrc/test/java/net/staticstudios/data/ValueParseTest.java b/oldsrc/test/java/net/staticstudios/data/ValueParseTest.java deleted file mode 100644 index ddc68a0e..00000000 --- a/oldsrc/test/java/net/staticstudios/data/ValueParseTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.util.EnvironmentVariableAccessor; -import net.staticstudios.data.util.ValueUtils; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class ValueParseTest { - - @BeforeAll - public static void setup() { - ValueUtils.ENVIRONMENT_VARIABLE_ACCESSOR = new EnvironmentVariableAccessor() { - @Override - public String getEnv(String name) { - return switch (name) { - case "env_var" -> "value_here"; - case "ENV_VAR2" -> "VALUE2_HERE"; - default -> null; - }; - } - }; - } - - @Test - public void testParse() { - assertEquals("value", ValueUtils.parseValue("value")); - assertEquals("value_here", ValueUtils.parseValue("${env_var}")); - assertEquals("VALUE2_HERE", ValueUtils.parseValue("${ENV_VAR2}")); - } -} \ No newline at end of file diff --git a/oldsrc/test/java/net/staticstudios/data/misc/DataTest.java b/oldsrc/test/java/net/staticstudios/data/misc/DataTest.java deleted file mode 100644 index 0891cc29..00000000 --- a/oldsrc/test/java/net/staticstudios/data/misc/DataTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package net.staticstudios.data.misc; - -import com.redis.testcontainers.RedisContainer; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.util.DataSourceConfig; -import net.staticstudios.utils.ThreadUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.utility.DockerImageName; -import redis.clients.jedis.Jedis; - -import java.io.IOException; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; - -public class DataTest { - public static final int NUM_ENVIRONMENTS = 1; - public static RedisContainer redis; - public static PostgreSQLContainer postgres = new PostgreSQLContainer<>( - "postgres:16.2" - ); - public static DataSourceConfig dataSourceConfig; - private static Connection connection; - private static Jedis jedis; - private List mockEnvironments; - - @BeforeAll - static void initPostgres() throws IOException, SQLException, InterruptedException { - postgres.start(); - redis = new RedisContainer(DockerImageName.parse("redis:6.2.6")); - redis.start(); - - redis.execInContainer("redis-cli", "config", "set", "notify-keyspace-events", "KEA"); - - dataSourceConfig = new DataSourceConfig( - postgres.getHost(), - postgres.getFirstMappedPort(), - postgres.getDatabaseName(), - postgres.getUsername(), - postgres.getPassword(), - redis.getHost(), - redis.getRedisPort() - ); - - connection = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); - jedis = new Jedis(redis.getHost(), redis.getRedisPort()); - } - - @AfterAll - public static void cleanup() throws IOException { - postgres.stop(); - redis.stop(); - } - - public static Connection getConnection() { - return connection; - } - - public static Jedis getJedis() { - return jedis; - } - - @BeforeEach - public void setupMockEnvironments() { - mockEnvironments = new LinkedList<>(); - ThreadUtils.setProvider(new MockThreadProvider()); - for (int i = 0; i < NUM_ENVIRONMENTS; i++) { - mockEnvironments.add(createMockEnvironment()); - } - } - - protected MockEnvironment createMockEnvironment() { - DataManager dataManager = new DataManager(dataSourceConfig); - - MockEnvironment mockEnvironment = new MockEnvironment(dataSourceConfig, dataManager); - mockEnvironments.add(mockEnvironment); - return mockEnvironment; - } - - @AfterEach - public void teardownThreadUtils() { - ThreadUtils.shutdown(); - } - - public int getNumEnvironments() { - return mockEnvironments.size(); - } - - public List getMockEnvironments() { - return mockEnvironments; - } - - public int getWaitForDataPropagationTime() { - return 500 + (Objects.equals(System.getenv("GITHUB_ACTIONS"), "true") ? 1000 : 0); - } - - public void waitForDataPropagation() { - try { - Thread.sleep(getWaitForDataPropagationTime()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/oldsrc/test/java/net/staticstudios/data/misc/MockEnvironment.java b/oldsrc/test/java/net/staticstudios/data/misc/MockEnvironment.java deleted file mode 100644 index a5b37ed1..00000000 --- a/oldsrc/test/java/net/staticstudios/data/misc/MockEnvironment.java +++ /dev/null @@ -1,10 +0,0 @@ -package net.staticstudios.data.misc; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.util.DataSourceConfig; - -public record MockEnvironment( - DataSourceConfig dataSourceConfig, - DataManager dataManager -) { -} diff --git a/oldsrc/test/java/net/staticstudios/data/misc/MockThreadProvider.java b/oldsrc/test/java/net/staticstudios/data/misc/MockThreadProvider.java deleted file mode 100644 index b40bff74..00000000 --- a/oldsrc/test/java/net/staticstudios/data/misc/MockThreadProvider.java +++ /dev/null @@ -1,125 +0,0 @@ -package net.staticstudios.data.misc; - -import net.staticstudios.utils.ShutdownStage; -import net.staticstudios.utils.ShutdownTask; -import net.staticstudios.utils.ThreadUtilProvider; -import net.staticstudios.utils.ThreadUtils; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; -import java.util.logging.Logger; - -public class MockThreadProvider implements ThreadUtilProvider { - private final ExecutorService mainThreadExecutorService; - private final List syncOnDisableTasksRunNext = Collections.synchronizedList(new ArrayList<>()); - private final List shutdownTasks = Collections.synchronizedList(new ArrayList<>()); - private final Logger logger = Logger.getLogger(MockThreadProvider.class.getName()); - private ExecutorService executorService; - private boolean isShuttingDown = false; - private boolean doneShuttingDown = false; - - - public MockThreadProvider() { - this.mainThreadExecutorService = Executors.newSingleThreadExecutor(); - this.executorService = Executors.newCachedThreadPool((r) -> new Thread(r, "MockThreadProvider")); - } - - @Override - public void submit(Runnable runnable) { - if (doneShuttingDown) { - throw new IllegalStateException("Cannot submit tasks after shutdown"); - } - executorService.submit(() -> { - try { - runnable.run(); - } catch (Exception e) { - e.printStackTrace(); - } - }); - } - - @Override - public void runSync(Runnable runnable) { - if (isShuttingDown) { - syncOnDisableTasksRunNext.add(runnable); - return; - } - - mainThreadExecutorService.submit(runnable); - } - - @Override - public void onShutdownRunSync(ShutdownStage shutdownStage, Runnable runnable) { - shutdownTasks.add(new ShutdownTask(shutdownStage, () -> { - ThreadUtils.safe(runnable); - return null; - }, true)); - } - - @Override - public void onShutdownRunAsync(ShutdownStage shutdownStage, Supplier> task) { - shutdownTasks.add(new ShutdownTask(shutdownStage, task, false)); - - } - - @Override - public boolean isShuttingDown() { - return isShuttingDown; - } - - public void shutdown() { - isShuttingDown = true; - - executorService.shutdown(); - try { - executorService.awaitTermination(30, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - Map> tasks = new HashMap<>(); - shutdownTasks.forEach(task -> tasks.computeIfAbsent(task.stage(), k -> new ArrayList<>()).add(task)); - - ShutdownStage.getStages() - .forEach(stage -> { - if (tasks.containsKey(stage)) { - getLogger().info("Running shutdown tasks for stage " + stage); - - List> asyncFutures = new ArrayList<>(); - List syncTasks = new ArrayList<>(); - - tasks.get(stage).forEach(task -> { - if (task.sync()) { - syncTasks.add(() -> task.task().get()); - } else { - asyncFutures.add(task.task().get()); - } - }); - - //Wait for all async tasks to finish - try { - CompletableFuture.allOf(asyncFutures.toArray(new CompletableFuture[0])).get(30, TimeUnit.SECONDS); - } catch (Exception e) { - getLogger().severe("Failed to wait for async tasks to finish during shutdown stage " + stage); - e.printStackTrace(); - } - - syncTasks.forEach(Runnable::run); - - syncOnDisableTasksRunNext.forEach(Runnable::run); - - syncOnDisableTasksRunNext.clear(); - } - }); - - doneShuttingDown = true; - } - - private Logger getLogger() { - return logger; - } -} \ No newline at end of file diff --git a/oldsrc/test/java/net/staticstudios/data/misc/TestUtils.java b/oldsrc/test/java/net/staticstudios/data/misc/TestUtils.java deleted file mode 100644 index eeb5a95a..00000000 --- a/oldsrc/test/java/net/staticstudios/data/misc/TestUtils.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.misc; - -import java.sql.ResultSet; -import java.sql.SQLException; - -public class TestUtils { - public static int getResultCount(ResultSet rs) throws SQLException { - if (rs.getType() == ResultSet.TYPE_FORWARD_ONLY) { - int count = rs.getRow(); - while (rs.next()) { - count++; - } - return count; - } - - int currentRow = rs.getRow(); - try { - rs.last(); - int rowCount = rs.getRow(); - rs.absolute(currentRow); - return rowCount; - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/oldsrc/test/java/net/staticstudios/data/mock/MockUser.java b/oldsrc/test/java/net/staticstudios/data/mock/MockUser.java deleted file mode 100644 index 8242d874..00000000 --- a/oldsrc/test/java/net/staticstudios/data/mock/MockUser.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.staticstudios.data.mock; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.Reference; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.annotations.Data; -import net.staticstudios.data.insert.InsertMode; -import net.staticstudios.data.parse.Column; -import net.staticstudios.data.util.ForeignColumn; -import net.staticstudios.data.util.IdColumn; -import net.staticstudios.data.util.OneToOne; - -import java.util.UUID; - -@Data(schema = "public", table = "users") -public class MockUser extends UniqueData { - //todo: @OneToMany, @ManyToMany, @ManyToOne - - - @IdColumn(name = "id") - public PersistentValue id = PersistentValue.of(this, UUID.class); - @Column(name = "settings_id") - public PersistentValue settingsId = PersistentValue.of(this, UUID.class); - @Column(name = "age", nullable = true) - public PersistentValue age; - @ForeignColumn(name = "fav_color", table = "user_preferences", nullable = true, link = "id=user_id") - public PersistentValue favoriteColor; - @OneToOne(link = "settings_id=user_id") - public Reference settings; - @Column(name = "name", index = true) - public PersistentValue name = PersistentValue.of(this, String.class) - .onUpdate(MockUser.class, (user, update) -> { -// System.out.println("User " + user.id.get() + " changed name from " + update.oldValue() + " to " + update.newValue()); - }) - .withDefault("Unknown"); - - public static MockUser create(DataManager dataManager, UUID id, String name) { - return dataManager.createInsertContext() - .set(MockUser.class, "id", id) //todo: i dislike that we lose type safety - .set(MockUser.class, "name", name) - //todo: add support for unique constraints and test them. - //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. - .insert(InsertMode.SYNC) - .get(MockUser.class); - - - //TODO: generate the following patterns at compile time from the @Data annotation. - - /* MockUser.builder(datamanager) - * .id(id) - * .name(name) - * .insert(InsertMode.SYNC); //returns the inserted object - */ - /* MockUser.builder(datamanager) - * .id(id) - * .name(name) - * .insert(insertContext); //add the object to an existing insert context, and return void. - */ - } -} diff --git a/oldsrc/test/java/net/staticstudios/data/mock/MockUserSettings.java b/oldsrc/test/java/net/staticstudios/data/mock/MockUserSettings.java deleted file mode 100644 index ccb0a8d3..00000000 --- a/oldsrc/test/java/net/staticstudios/data/mock/MockUserSettings.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.staticstudios.data.mock; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.annotations.Data; -import net.staticstudios.data.insert.InsertMode; -import net.staticstudios.data.parse.Column; -import net.staticstudios.data.util.IdColumn; - -import java.util.UUID; - -@Data(schema = "public", table = "user_settings") -public class MockUserSettings extends UniqueData { - @IdColumn(name = "user_id") - public PersistentValue id; - @Column(name = "font_size") - public PersistentValue fontSide; - - public static MockUserSettings create(DataManager dataManager, UUID id) { - return dataManager.createInsertContext() - .set(MockUserSettings.class, "user_id", id) - //todo: enfore the nullable constraint. actually - it might fail for us via H2. test this. - .insert(InsertMode.SYNC) - .get(MockUserSettings.class); - } -} diff --git a/oldsrc/test/resources/log4j.properties b/oldsrc/test/resources/log4j.properties deleted file mode 100644 index c87dc64f..00000000 --- a/oldsrc/test/resources/log4j.properties +++ /dev/null @@ -1,6 +0,0 @@ -log4j.rootLogger=INFO, STDOUT -log4j.logger.net.staticstudios=TRACE -log4j.logger.deng=INFO -log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender -log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout -log4j.appender.STDOUT.layout.ConversionPattern=%5p %d [%t] (%F:%L) - %m%n \ No newline at end of file From da3123c89cf0e859162b3d3933a02b1a42761ad0 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 23 Sep 2025 12:42:26 -0400 Subject: [PATCH 18/75] extract metadata for pv and references rather than constantly computing it --- .../data/processor/DataProcessor.java | 2 +- .../net/staticstudios/data/DataAccessor.java | 2 +- .../net/staticstudios/data/DataManager.java | 60 +++++++++--- .../staticstudios/data/PersistentValue.java | 16 +--- .../data/impl/data/PersistentValueImpl.java | 95 +++++++++++-------- .../data/impl/data/ReferenceImpl.java | 69 ++++++++------ .../data/impl/h2/DelayedDatabaseTask.java | 4 + .../data/impl/h2/H2DataAccessor.java | 56 ++++++++--- .../util/ForeignPersistentValueMetadata.java | 40 ++++++++ .../data/util/PersistentValueMetadata.java | 34 ++----- .../data/util/ReferenceMetadata.java | 43 +++++++++ .../data/util/UniqueDataMetadata.java | 6 +- 12 files changed, 301 insertions(+), 126 deletions(-) create mode 100644 src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java create mode 100644 src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java create mode 100644 src/main/java/net/staticstudios/data/util/ReferenceMetadata.java diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index c7fbf23f..49ef76b3 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -19,7 +19,7 @@ @SupportedAnnotationTypes("net.staticstudios.data.Data") @SupportedSourceVersion(SourceVersion.RELEASE_21) -public class DataProcessor extends AbstractProcessor { +public class DataProcessor extends AbstractProcessor { //todo: this seems to be in the classpath of the main project. address this. @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { for (Element annotated : roundEnv.getElementsAnnotatedWith(Data.class)) { diff --git a/src/main/java/net/staticstudios/data/DataAccessor.java b/src/main/java/net/staticstudios/data/DataAccessor.java index f14e4f25..67c04bfe 100644 --- a/src/main/java/net/staticstudios/data/DataAccessor.java +++ b/src/main/java/net/staticstudios/data/DataAccessor.java @@ -15,7 +15,7 @@ public interface DataAccessor { ResultSet executeQuery(@Language("SQL") String sql, List values) throws SQLException; - void executeUpdate(@Language("SQL") String sql, List values) throws SQLException; + void executeUpdate(@Language("SQL") String sql, List values, int delay) throws SQLException; void insert(List sqlStatements, InsertMode insertMode) throws SQLException; diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 15b267c7..a678432d 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -199,7 +199,9 @@ public void extractMetadata(Class clazz) { )); } Preconditions.checkArgument(!idColumns.isEmpty(), "UniqueData class %s must have at least one @IdColumn annotated PersistentValue field", clazz.getName()); - UniqueDataMetadata metadata = new UniqueDataMetadata(clazz, ValueUtils.parseValue(dataAnnotation.schema()), ValueUtils.parseValue(dataAnnotation.table()), idColumns); + String schema = ValueUtils.parseValue(dataAnnotation.schema()); + String table = ValueUtils.parseValue(dataAnnotation.table()); + UniqueDataMetadata metadata = new UniqueDataMetadata(clazz, schema, table, idColumns, PersistentValueImpl.extractMetadata(schema, table, clazz), ReferenceImpl.extractMetadata(clazz)); uniqueDataMetadataMap.put(clazz, metadata); for (Field field : ReflectionUtils.getFields(clazz, Relation.class)) { @@ -394,7 +396,7 @@ public T getInstance(Class clazz, ColumnValuePair... i throw new RuntimeException(e); } - PersistentValueImpl.delegate(schema, table, instance); + PersistentValueImpl.delegate(instance); ReferenceImpl.delegate(instance); uniqueDataInstanceCache.computeIfAbsent(clazz, k -> new MapMaker().weakValues().makeMap()) @@ -569,11 +571,17 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { } } - public T get(String schema, String table, String column, ColumnValuePairs idColumns, Map idColumnLinks, Class dataType) { + 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 = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + String name = columnValuePair.column(); + for (ForeignKey.Link link : idColumnLinks) { + if (link.columnInReferringTable().equals(columnValuePair.column())) { + name = link.columnInReferencedTable(); + break; + } + } sqlBuilder.append("\"").append(name).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); @@ -591,12 +599,18 @@ public T get(String schema, String table, String column, ColumnValuePairs id } @ApiStatus.Internal - public void set(String schema, String table, String column, ColumnValuePairs idColumns, Map idColumnLinks, Object value) { + public void set(String schema, String table, String column, ColumnValuePairs idColumns, List idColumnLinks, Object value, int delay) { StringBuilder sqlBuilder; if (idColumnLinks.isEmpty()) { sqlBuilder = new StringBuilder().append("UPDATE \"").append(schema).append("\".\"").append(table).append("\" SET \"").append(column).append("\" = ? WHERE "); for (ColumnValuePair columnValuePair : idColumns) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + String name = columnValuePair.column(); + for (ForeignKey.Link link : idColumnLinks) { + if (link.columnInReferringTable().equals(columnValuePair.column())) { + name = link.columnInReferencedTable(); + break; + } + } sqlBuilder.append("\"").append(name).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); @@ -605,23 +619,47 @@ public void set(String schema, String table, String column, ColumnValuePairs idC sqlBuilder.append(", ?".repeat(idColumns.getPairs().length)); sqlBuilder.append(")) AS source (\"").append(column).append("\""); for (ColumnValuePair columnValuePair : idColumns) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + String name = columnValuePair.column(); + for (ForeignKey.Link link : idColumnLinks) { + if (link.columnInReferringTable().equals(columnValuePair.column())) { + name = link.columnInReferencedTable(); + break; + } + } sqlBuilder.append(", \"").append(name).append("\""); } sqlBuilder.append(") ON "); for (ColumnValuePair columnValuePair : idColumns) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + String name = columnValuePair.column(); + for (ForeignKey.Link link : idColumnLinks) { + if (link.columnInReferringTable().equals(columnValuePair.column())) { + name = link.columnInReferencedTable(); + break; + } + } sqlBuilder.append("target.\"").append(name).append("\" = source.\"").append(name).append("\" AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); sqlBuilder.append(" WHEN MATCHED THEN UPDATE SET \"").append(column).append("\" = source.\"").append(column).append("\" WHEN NOT MATCHED THEN INSERT (\"").append(column).append("\""); for (ColumnValuePair columnValuePair : idColumns) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + String name = columnValuePair.column(); + for (ForeignKey.Link link : idColumnLinks) { + if (link.columnInReferringTable().equals(columnValuePair.column())) { + name = link.columnInReferencedTable(); + break; + } + } sqlBuilder.append(", \"").append(name).append("\""); } sqlBuilder.append(") VALUES (source.\"").append(column).append("\""); for (ColumnValuePair columnValuePair : idColumns) { - String name = idColumnLinks.getOrDefault(columnValuePair.column(), columnValuePair.column()); + String name = columnValuePair.column(); + for (ForeignKey.Link link : idColumnLinks) { + if (link.columnInReferringTable().equals(columnValuePair.column())) { + name = link.columnInReferencedTable(); + break; + } + } sqlBuilder.append(", source.\"").append(name).append("\""); } sqlBuilder.append(")"); @@ -633,7 +671,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC values.add(columnValuePair.value()); } try { - dataAccessor.executeUpdate(sql, values); + dataAccessor.executeUpdate(sql, values, delay); } catch (SQLException e) { throw new RuntimeException(e); } diff --git a/src/main/java/net/staticstudios/data/PersistentValue.java b/src/main/java/net/staticstudios/data/PersistentValue.java index 9e622a70..6a1f6f34 100644 --- a/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/src/main/java/net/staticstudios/data/PersistentValue.java @@ -1,6 +1,7 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; +import net.staticstudios.data.parse.ForeignKey; import net.staticstudios.data.util.*; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -8,7 +9,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; /** * A persistent value represents a single cell in a database table. @@ -29,7 +29,7 @@ static PersistentValue of(UniqueData holder, Class dataType) { Class getDataType(); @ApiStatus.Internal - Map getIdColumnLinks(); + List getIdColumnLinks(); PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler); @@ -41,7 +41,7 @@ class ProxyPersistentValue implements PersistentValue { protected final Class dataType; private final List> updateHandlers = new ArrayList<>(); private @Nullable PersistentValue delegate; - private Map idColumnLinks = Collections.emptyMap(); + private List idColumnLinks = Collections.emptyList(); private long updateIntervalMillis = -1; public ProxyPersistentValue(UniqueData holder, Class dataType) { @@ -53,13 +53,7 @@ public void setDelegate(ColumnMetadata columnMetadata, PersistentValue delega Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); this.delegate = delegate; - PersistentValueMetadata metadata = new PersistentValueMetadata( - holder.getClass(), - columnMetadata.schema(), - columnMetadata.table(), - columnMetadata.name(), - dataType - ); + PersistentValueMetadata metadata = new PersistentValueMetadata(delegate.getHolder().getClass(), columnMetadata); holder.getDataManager().registerUpdateHandler(metadata, updateHandlers); } @@ -75,7 +69,7 @@ public Class getDataType() { } @Override - public Map getIdColumnLinks() { + public List getIdColumnLinks() { return idColumnLinks; } // diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index 16f49d6a..1eb068b8 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -2,13 +2,12 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.*; +import net.staticstudios.data.parse.ForeignKey; import net.staticstudios.data.util.*; import org.jetbrains.annotations.Nullable; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.lang.reflect.Field; +import java.util.*; public class PersistentValueImpl implements PersistentValue { private final DataAccessor dataAccessor; @@ -17,9 +16,9 @@ public class PersistentValueImpl implements PersistentValue { private final String schema; private final String table; private final String column; - private final Map idColumnLinks; + private final List idColumnLinks; - private PersistentValueImpl(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, Map idColumnLinks) { + private PersistentValueImpl(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, List idColumnLinks) { this.dataAccessor = dataAccessor; this.holder = holder; this.dataType = dataType; @@ -43,70 +42,90 @@ public static void createAndDelegate(ProxyPersistentValue proxy, ColumnMe proxy.setDelegate(columnMetadata, delegate); } - public static PersistentValueImpl create(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, Map idColumnLinks) { + public static PersistentValueImpl create(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, List idColumnLinks) { return new PersistentValueImpl<>(dataAccessor, holder, dataType, schema, table, column, idColumnLinks); } - public static void delegate(String schema, String table, T instance) { //todo: cache this info. can we use the uniquedatametadata? + public static void delegate(T instance) { + UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); for (FieldInstancePair<@Nullable PersistentValue> pair : ReflectionUtils.getFieldInstancePairs(instance, PersistentValue.class)) { - IdColumn idColumn = pair.field().getAnnotation(IdColumn.class); - Column columnAnnotation = pair.field().getAnnotation(Column.class); - ForeignColumn foreignColumn = pair.field().getAnnotation(ForeignColumn.class); - ColumnMetadata columnMetadata = null; - Map idColumnLinks = Collections.emptyMap(); + PersistentValueMetadata pvMetadata = metadata.persistentValueMetadata().get(pair.field()); + ColumnMetadata columnMetadata = pvMetadata.getColumnMetadata(); + if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { + PersistentValueImpl.createAndDelegate(proxyPv, columnMetadata); + } else { + pair.field().setAccessible(true); + try { + List idColumnLinks = Collections.emptyList(); + if (pvMetadata instanceof ForeignPersistentValueMetadata foreignPvMetadata) { + idColumnLinks = foreignPvMetadata.getLinks(); + } + pair.field().set(instance, PersistentValueImpl.create(instance.getDataManager().getDataAccessor(), instance, ReflectionUtils.getGenericType(pair.field()), columnMetadata.schema(), columnMetadata.table(), columnMetadata.name(), idColumnLinks)); + } 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)) { + IdColumn idColumn = field.getAnnotation(IdColumn.class); + Column columnAnnotation = field.getAnnotation(Column.class); + ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); if (idColumn != null) { - Preconditions.checkArgument(columnAnnotation == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @Column", pair.field().getName()); - Preconditions.checkArgument(foreignColumn == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @ForeignColumn", pair.field().getName()); - columnMetadata = new ColumnMetadata( + Preconditions.checkArgument(columnAnnotation == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @Column", field.getName()); + Preconditions.checkArgument(foreignColumn == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @ForeignColumn", field.getName()); + ColumnMetadata columnMetadata = new ColumnMetadata( schema, table, ValueUtils.parseValue(idColumn.name()), - ReflectionUtils.getGenericType(pair.field()), + ReflectionUtils.getGenericType(field), false, false, "" ); - } else if (columnAnnotation != null) { - columnMetadata = new ColumnMetadata( + metadataMap.put(field, new PersistentValueMetadata(clazz, columnMetadata)); + continue; + } + if (columnAnnotation != null) { + ColumnMetadata columnMetadata = new ColumnMetadata( columnAnnotation.schema().isEmpty() ? schema : ValueUtils.parseValue(columnAnnotation.schema()), columnAnnotation.table().isEmpty() ? table : ValueUtils.parseValue(columnAnnotation.table()), ValueUtils.parseValue(columnAnnotation.name()), - ReflectionUtils.getGenericType(pair.field()), + ReflectionUtils.getGenericType(field), columnAnnotation.nullable(), columnAnnotation.index(), columnAnnotation.defaultValue() ); - } else if (foreignColumn != null) { - columnMetadata = new ColumnMetadata( + metadataMap.put(field, new PersistentValueMetadata(clazz, columnMetadata)); + continue; + } + if (foreignColumn != null) { + ColumnMetadata columnMetadata = new ColumnMetadata( foreignColumn.schema().isEmpty() ? schema : ValueUtils.parseValue(foreignColumn.schema()), foreignColumn.table().isEmpty() ? table : ValueUtils.parseValue(foreignColumn.table()), ValueUtils.parseValue(foreignColumn.name()), - ReflectionUtils.getGenericType(pair.field()), + ReflectionUtils.getGenericType(field), foreignColumn.nullable(), foreignColumn.index(), foreignColumn.defaultValue() ); - idColumnLinks = new HashMap<>(); + List idColumnLinks = new LinkedList<>(); List links = StringUtils.parseCommaSeperatedList(foreignColumn.link()); for (String link : links) { String[] parts = link.split("="); Preconditions.checkArgument(parts.length == 2, "ForeignColumn link must be in the format localColumn=foreignColumn, got: %s", link); - idColumnLinks.put(ValueUtils.parseValue(parts[0]), ValueUtils.parseValue(parts[1])); + idColumnLinks.add(new ForeignKey.Link(ValueUtils.parseValue(parts[1]), ValueUtils.parseValue(parts[0]))); } + metadataMap.put(field, new ForeignPersistentValueMetadata(clazz, columnMetadata, idColumnLinks)); + continue; } - Preconditions.checkNotNull(columnMetadata, "PersistentValue field %s is missing @Column annotation", pair.field().getName()); - if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { - PersistentValueImpl.createAndDelegate(proxyPv, columnMetadata); - } else { - pair.field().setAccessible(true); - try { - pair.field().set(instance, PersistentValueImpl.create(instance.getDataManager().getDataAccessor(), instance, ReflectionUtils.getGenericType(pair.field()), columnMetadata.schema(), columnMetadata.table(), columnMetadata.name(), idColumnLinks)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } + throw new IllegalStateException("PersistentValue field %s is missing @Column annotation".formatted(field.getName())); } + return metadataMap; } @Override @@ -125,7 +144,7 @@ public PersistentValue onUpdate(Class holderClass, } @Override - public Map getIdColumnLinks() { + public List getIdColumnLinks() { return idColumnLinks; } @@ -138,6 +157,6 @@ public T get() { @Override public void set(T value) { Preconditions.checkArgument(!holder.isDeleted(), "Cannot set value on a deleted UniqueData instance"); - holder.getDataManager().set(schema, table, column, holder.getIdColumns(), idColumnLinks, value); + holder.getDataManager().set(schema, table, column, holder.getIdColumns(), idColumnLinks, value, 0); //todo: this delay can be used to throttle frequent updates. allow it to be configured. this also needs to be tested } } diff --git a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java index eef1a917..60b42a19 100644 --- a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -5,29 +5,28 @@ import net.staticstudios.data.OneToOne; import net.staticstudios.data.Reference; import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.ForeignKey; import net.staticstudios.data.util.*; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; +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 Map link; + private final List link; - public ReferenceImpl(UniqueData holder, Class type, Map link) { + public ReferenceImpl(UniqueData holder, Class type, List link) { this.holder = holder; this.type = type; this.link = link; } - public static void createAndDelegate(Reference.ProxyReference proxy, Map link) { + public static void createAndDelegate(Reference.ProxyReference proxy, List link) { ReferenceImpl delegate = new ReferenceImpl<>( proxy.getHolder(), proxy.getReferenceType(), @@ -36,29 +35,21 @@ public static void createAndDelegate(Reference.ProxyRefer proxy.setDelegate(delegate); } - public static ReferenceImpl create(UniqueData holder, Class type, Map link) { + public static ReferenceImpl create(UniqueData holder, Class type, List link) { return new ReferenceImpl<>(holder, type, link); } - public static void delegate(T instance) { //todo: cache this info. can we use the uniquedatametadata? + public static void delegate(T instance) { + UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); for (FieldInstancePair<@Nullable Reference> pair : ReflectionUtils.getFieldInstancePairs(instance, Reference.class)) { - OneToOne oneToOneAnnotation = pair.field().getAnnotation(OneToOne.class); - Preconditions.checkNotNull(oneToOneAnnotation, "Field %s in class %s is missing @OneToOne annotation".formatted(pair.field().getName(), instance.getClass().getName())); - Class referencedClass = ReflectionUtils.getGenericType(pair.field()); - Preconditions.checkNotNull(referencedClass, "Field %s in class %s is not parameterized".formatted(pair.field().getName(), instance.getClass().getName())); - Map link = new HashMap<>(); - for (String l : StringUtils.parseCommaSeperatedList(oneToOneAnnotation.link())) { - String[] split = l.split("="); - Preconditions.checkArgument(split.length == 2, "Invalid link format in @OneToOne annotation on field %s in class %s".formatted(pair.field().getName(), instance.getClass().getName())); - link.put(ValueUtils.parseValue(split[0].trim()), ValueUtils.parseValue(split[1].trim())); - } + ReferenceMetadata refMetadata = metadata.referenceMetadata().get(pair.field()); if (pair.instance() instanceof Reference.ProxyReference proxyRef) { - createAndDelegate(proxyRef, link); + createAndDelegate(proxyRef, refMetadata.getLinks()); } else { pair.field().setAccessible(true); try { - pair.field().set(instance, create(instance, (Class) referencedClass, link)); + pair.field().set(instance, create(instance, refMetadata.getReferencedClass(), refMetadata.getLinks())); } catch (IllegalAccessException e) { throw new RuntimeException(e); } @@ -66,6 +57,26 @@ public static void delegate(T instance) { //todo: cache t } } + public static Map extractMetadata(Class clazz) { + Map metadataMap = new HashMap<>(); + for (Field field : ReflectionUtils.getFields(clazz, Reference.class)) { + 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); + Preconditions.checkNotNull(referencedClass, "Field %s in class %s is not parameterized".formatted(field.getName(), clazz.getName())); + List link = new LinkedList<>(); + for (String l : StringUtils.parseCommaSeperatedList(oneToOneAnnotation.link())) { + String[] split = l.split("="); + Preconditions.checkArgument(split.length == 2, "Invalid link format in @OneToOne annotation on field %s in class %s".formatted(field.getName(), clazz.getName())); + link.add(new ForeignKey.Link(ValueUtils.parseValue(split[1].trim()), ValueUtils.parseValue(split[0].trim()))); + } + + metadataMap.put(field, new ReferenceMetadata((Class) referencedClass, link)); + } + + return metadataMap; + } + @Override public UniqueData getHolder() { return holder; @@ -78,13 +89,14 @@ public Class getReferenceType() { @Override public @Nullable T get() { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot get reference on a deleted UniqueData instance"); ColumnValuePair[] idColumns = new ColumnValuePair[link.size()]; int i = 0; UniqueDataMetadata holderMetadata = holder.getMetadata(); DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); - for (Map.Entry entry : link.entrySet()) { - String myColumn = entry.getKey(); - String theirColumn = entry.getValue(); + for (ForeignKey.Link entry : link) { + String myColumn = entry.columnInReferringTable(); + String theirColumn = entry.columnInReferencedTable(); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT \"").append(myColumn).append("\" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); @@ -112,18 +124,19 @@ public Class getReferenceType() { @Override public void set(@Nullable T value) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set reference on a deleted UniqueData instance"); UniqueDataMetadata holderMetadata = holder.getMetadata(); List values = new ArrayList<>(); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("UPDATE \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" SET "); - for (Map.Entry entry : link.entrySet()) { - String myColumn = entry.getKey(); + for (ForeignKey.Link entry : link) { + String myColumn = entry.columnInReferringTable(); if (value == null) { sqlBuilder.append("\"").append(myColumn).append("\" = NULL, "); continue; } - String theirColumn = entry.getValue(); + String theirColumn = entry.columnInReferencedTable(); Object theirValue = null; for (ColumnValuePair columnValuePair : value.getIdColumns()) { if (columnValuePair.column().equals(theirColumn)) { @@ -148,7 +161,7 @@ public void set(@Nullable T value) { } try { - holder.getDataManager().getDataAccessor().executeUpdate(sqlBuilder.toString(), values); + holder.getDataManager().getDataAccessor().executeUpdate(sqlBuilder.toString(), values, 0); } catch (SQLException e) { throw new RuntimeException(e); } diff --git a/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java b/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java new file mode 100644 index 00000000..694b1402 --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.impl.h2; + +public record DelayedDatabaseTask(String sql, Object[] params) { +} diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index 283dba2f..3bdf132f 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -28,8 +28,8 @@ import java.nio.file.Paths; import java.sql.*; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; +import java.util.concurrent.*; +import java.util.function.Consumer; /** * This data accessor uses a write-behind caching strategy with an in-memory H2 database to optimize read and write operations. @@ -47,6 +47,12 @@ public class H2DataAccessor implements DataAccessor { private final Set knownTables = new HashSet<>(); private final DataManager dataManager; private final PostgresListener postgresListener; + private final Map> delayedTasks = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(thread -> { + Thread t = new Thread(thread); + t.setName(H2DataAccessor.class.getSimpleName() + "-ScheduledExecutor"); + return t; + }); public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener, TaskQueue taskQueue) { this.taskQueue = taskQueue; @@ -190,6 +196,16 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener logger.error("Failed to shutdown H2 database", e); } }); + + + ThreadUtils.onShutdownRunSync(ShutdownStage.EARLY, () -> { + List tasks = scheduledExecutorService.shutdownNow(); + + logger.info("Shutting down {}, running {} enqueued tasks", H2DataAccessor.class.getSimpleName(), tasks.size()); + for (Runnable task : tasks) { + task.run(); + } + }); } public synchronized void sync(List schemaTables) throws SQLException { @@ -368,7 +384,7 @@ public ResultSet executeQuery(@Language("SQL") String sql, List values) } @Override - public void executeUpdate(@Language("SQL") String sql, List values) throws SQLException { + public void executeUpdate(@Language("SQL") String sql, List values, int delay) throws SQLException { PreparedStatement cachePreparedStatement = prepareStatement(sql); for (int i = 0; i < values.size(); i++) { cachePreparedStatement.setObject(i + 1, values.get(i)); @@ -379,14 +395,7 @@ public void executeUpdate(@Language("SQL") String sql, List values) thro getConnection().commit(); } - taskQueue.submitTask(connection -> { - PreparedStatement realPreparedStatement = connection.prepareStatement(sql); - for (int i = 0; i < values.size(); i++) { - realPreparedStatement.setObject(i + 1, values.get(i)); - } - logger.debug("[DB] {}}", sql); - realPreparedStatement.executeUpdate(); - }); + runDatabaseTask(sql, values.toArray(), delay); } @Override @@ -456,6 +465,31 @@ private List getColumnsInTable(String schema, String table) throws SQLEx } return columns; } + + private void runDatabaseTask(String sql, Object[] params, int delay) { + Consumer consumer = task -> taskQueue.submitTask(connection -> { + PreparedStatement realPreparedStatement = connection.prepareStatement(task.sql()); + for (int i = 0; i < task.params().length; i++) { + realPreparedStatement.setObject(i + 1, task.params()[i]); + } + logger.debug("[DB] {}}", task.sql()); + realPreparedStatement.executeUpdate(); + }); + + DelayedDatabaseTask task = new DelayedDatabaseTask(sql, params); + if (delay <= 0) { + consumer.accept(task); + return; + } + if (delayedTasks.put(task, consumer) == null) { + scheduledExecutorService.schedule(() -> { + Consumer removed = delayedTasks.remove(task); + if (removed != null) { + removed.accept(task); + } + }, delay, TimeUnit.MILLISECONDS); + } + } } diff --git a/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java b/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java new file mode 100644 index 00000000..f07a0347 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java @@ -0,0 +1,40 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.ForeignKey; + +import java.util.List; +import java.util.Objects; + +public class ForeignPersistentValueMetadata extends PersistentValueMetadata { + private final List links; + + public ForeignPersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata, List links) { + super(holderClass, columnMetadata); + this.links = links; + } + + public List getLinks() { + return links; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + ForeignPersistentValueMetadata that = (ForeignPersistentValueMetadata) o; + return Objects.equals(links, that.links); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), links); + } + + @Override + public String toString() { + return "ForeignPersistentValueMetadata[" + + "columnMetadata=" + getColumnMetadata() + ", " + + "links=" + links + ']'; + } +} diff --git a/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java b/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java index e47bf656..95d2d253 100644 --- a/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java +++ b/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java @@ -6,42 +6,32 @@ public class PersistentValueMetadata { private final Class holderClass; - private final String schema; - private final String table; - private final String column; - private final Class dataType; + private final ColumnMetadata columnMetadata; - public PersistentValueMetadata(Class holderClass, String schema, String table, String column, Class dataType) { + public PersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata) { this.holderClass = holderClass; - this.schema = schema; - this.table = table; - this.column = column; - this.dataType = dataType; - } - - public Class getHolderClass() { - return holderClass; + this.columnMetadata = columnMetadata; } public String getSchema() { - return schema; + return columnMetadata.schema(); } public String getTable() { - return table; + return columnMetadata.table(); } public String getColumn() { - return column; + return columnMetadata.name(); } - public Class getDataType() { - return dataType; + public ColumnMetadata getColumnMetadata() { + return columnMetadata; } @Override public int hashCode() { - return Objects.hash(holderClass, schema, table, column, dataType); + return Objects.hash(holderClass, columnMetadata); } @Override @@ -49,10 +39,6 @@ public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; PersistentValueMetadata that = (PersistentValueMetadata) obj; - return holderClass.equals(that.holderClass) && - schema.equals(that.schema) && - table.equals(that.table) && - column.equals(that.column) && - dataType.equals(that.dataType); + return Objects.equals(holderClass, that.holderClass) && Objects.equals(columnMetadata, that.columnMetadata); } } diff --git a/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java b/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java new file mode 100644 index 00000000..2863ff0b --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java @@ -0,0 +1,43 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.ForeignKey; + +import java.util.List; +import java.util.Objects; + +public class ReferenceMetadata { + private final Class referencedClass; + private final List links; + + public ReferenceMetadata(Class referencedClass, List links) { + this.referencedClass = referencedClass; + this.links = links; + } + + public Class getReferencedClass() { + return referencedClass; + } + + public List getLinks() { + return links; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ReferenceMetadata that = (ReferenceMetadata) o; + return Objects.equals(referencedClass, that.referencedClass) && Objects.equals(links, that.links); + } + + @Override + public int hashCode() { + return Objects.hash(referencedClass, links); + } + + @Override + public String toString() { + return "ReferenceMetadata[" + + "links=" + links + ']'; + } +} diff --git a/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java b/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java index 51c38c77..f525e9c4 100644 --- a/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java +++ b/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java @@ -2,8 +2,12 @@ import net.staticstudios.data.UniqueData; +import java.lang.reflect.Field; import java.util.List; +import java.util.Map; public record UniqueDataMetadata(Class clazz, String schema, String table, - List idColumns) { + List idColumns, + Map persistentValueMetadata, + Map referenceMetadata) { } From d17366336378f172d6b98147110daba43396db6f Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 23 Sep 2025 17:51:25 -0400 Subject: [PATCH 19/75] support update intervals --- .../java/net/staticstudios/data/Column.java | 3 ++ .../java/net/staticstudios/data/Data.java | 3 ++ .../net/staticstudios/data/ForeignColumn.java | 3 ++ .../java/net/staticstudios/data/IdColumn.java | 3 ++ .../java/net/staticstudios/data/OneToOne.java | 3 ++ .../staticstudios/data/UpdateInterval.java | 22 +++++++++ .../staticstudios/data/PersistentValue.java | 49 ++----------------- .../data/impl/data/PersistentValueImpl.java | 38 +++++++------- .../data/impl/h2/H2DataAccessor.java | 25 ++++------ .../util/ForeignPersistentValueMetadata.java | 4 +- .../data/util/PersistentValueMetadata.java | 12 +++-- .../data/PersistentValueTest.java | 39 +++++++++++++++ .../data/mock/user/MockUser.java | 4 +- 13 files changed, 123 insertions(+), 85 deletions(-) create mode 100644 annotations/src/main/java/net/staticstudios/data/UpdateInterval.java diff --git a/annotations/src/main/java/net/staticstudios/data/Column.java b/annotations/src/main/java/net/staticstudios/data/Column.java index b38573fb..c9b1b71a 100644 --- a/annotations/src/main/java/net/staticstudios/data/Column.java +++ b/annotations/src/main/java/net/staticstudios/data/Column.java @@ -1,9 +1,12 @@ package net.staticstudios.data; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) public @interface Column { String name(); diff --git a/annotations/src/main/java/net/staticstudios/data/Data.java b/annotations/src/main/java/net/staticstudios/data/Data.java index 7dbe9b24..834cb1e2 100644 --- a/annotations/src/main/java/net/staticstudios/data/Data.java +++ b/annotations/src/main/java/net/staticstudios/data/Data.java @@ -1,9 +1,12 @@ package net.staticstudios.data; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) public @interface Data { String schema(); diff --git a/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java index 7d788819..14cfb4ab 100644 --- a/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java +++ b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java @@ -1,9 +1,12 @@ package net.staticstudios.data; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) public @interface ForeignColumn { String name(); diff --git a/annotations/src/main/java/net/staticstudios/data/IdColumn.java b/annotations/src/main/java/net/staticstudios/data/IdColumn.java index 5572b661..5c0db8ae 100644 --- a/annotations/src/main/java/net/staticstudios/data/IdColumn.java +++ b/annotations/src/main/java/net/staticstudios/data/IdColumn.java @@ -1,9 +1,12 @@ package net.staticstudios.data; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) public @interface IdColumn { String name(); } diff --git a/annotations/src/main/java/net/staticstudios/data/OneToOne.java b/annotations/src/main/java/net/staticstudios/data/OneToOne.java index fddd7d41..f4412ce4 100644 --- a/annotations/src/main/java/net/staticstudios/data/OneToOne.java +++ b/annotations/src/main/java/net/staticstudios/data/OneToOne.java @@ -1,10 +1,13 @@ package net.staticstudios.data; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) public @interface OneToOne { /** * How should this relation be linked? diff --git a/annotations/src/main/java/net/staticstudios/data/UpdateInterval.java b/annotations/src/main/java/net/staticstudios/data/UpdateInterval.java new file mode 100644 index 00000000..ff10c23c --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/UpdateInterval.java @@ -0,0 +1,22 @@ +package net.staticstudios.data; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is only applicable to {@link net.staticstudios.data.PersistentValue}s. + * Instead of propagating updates to the real database instantly, only the latest update will be made every Nms. + * This is useful for values that update very frequently, since this could otherwise overwhelm the task queue. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface UpdateInterval { + /** + * How long should we wait between updates, in milliseconds? + * + * @return The interval in milliseconds + */ + int value(); +} diff --git a/src/main/java/net/staticstudios/data/PersistentValue.java b/src/main/java/net/staticstudios/data/PersistentValue.java index 6a1f6f34..e8d27910 100644 --- a/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/src/main/java/net/staticstudios/data/PersistentValue.java @@ -1,13 +1,13 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.parse.ForeignKey; -import net.staticstudios.data.util.*; -import org.jetbrains.annotations.ApiStatus; +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 org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -28,33 +28,23 @@ static PersistentValue of(UniqueData holder, Class dataType) { Class getDataType(); - @ApiStatus.Internal - List getIdColumnLinks(); - PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler); - -// PersistentValue updateInterval(long intervalMillis); - class ProxyPersistentValue implements PersistentValue { protected final UniqueData holder; protected final Class dataType; private final List> updateHandlers = new ArrayList<>(); private @Nullable PersistentValue delegate; - private List idColumnLinks = Collections.emptyList(); - private long updateIntervalMillis = -1; public ProxyPersistentValue(UniqueData holder, Class dataType) { this.holder = holder; this.dataType = dataType; } - public void setDelegate(ColumnMetadata columnMetadata, PersistentValue delegate) { + public void setDelegate(PersistentValueMetadata metadata, PersistentValue delegate) { Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); this.delegate = delegate; - PersistentValueMetadata metadata = new PersistentValueMetadata(delegate.getHolder().getClass(), columnMetadata); - holder.getDataManager().registerUpdateHandler(metadata, updateHandlers); } @@ -68,25 +58,6 @@ public Class getDataType() { return dataType; } - @Override - public List getIdColumnLinks() { - return idColumnLinks; - } -// -// @Override -// public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { -// Preconditions.checkNotNull(updateHandler, "Update handler cannot be null"); -// -// if (delegate != null) { -// delegate.onUpdate(updateHandler); -// } else { -// this.updateHandlers.add(updateHandler); -// } -// -// return this; -// } - - @Override public PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler) { Preconditions.checkArgument(delegate == null, "Cannot dynamically add an update handler after the holder has been initialized!"); @@ -95,16 +66,6 @@ public PersistentValue onUpdate(Class holderClass, return this; } -// @Override -// public PersistentValue updateInterval(long intervalMillis) { -// if (delegate != null) { -// delegate.updateInterval(intervalMillis); -// return this; -// } -// this.updateIntervalMillis = intervalMillis; -// return this; -// } - @Override public T get() { if (delegate != null) { diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index 1eb068b8..7c7b33b6 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -10,40 +10,41 @@ import java.util.*; public class PersistentValueImpl implements PersistentValue { - private final DataAccessor dataAccessor; private final UniqueData holder; private final Class dataType; private final String schema; private final String table; private final String column; + private final int updateInterval; private final List idColumnLinks; - private PersistentValueImpl(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, List idColumnLinks) { - this.dataAccessor = dataAccessor; + private PersistentValueImpl(UniqueData holder, Class dataType, String schema, String table, String column, int updateInterval, List idColumnLinks) { this.holder = holder; this.dataType = dataType; this.schema = schema; this.table = table; this.column = column; + this.updateInterval = updateInterval; this.idColumnLinks = idColumnLinks; } - public static void createAndDelegate(ProxyPersistentValue proxy, ColumnMetadata columnMetadata) { + public static void createAndDelegate(ProxyPersistentValue proxy, PersistentValueMetadata metadata) { + ColumnMetadata columnMetadata = metadata.getColumnMetadata(); PersistentValueImpl delegate = new PersistentValueImpl<>( - proxy.getHolder().getDataManager().getDataAccessor(), proxy.getHolder(), proxy.getDataType(), columnMetadata.schema(), columnMetadata.table(), columnMetadata.name(), - proxy.getIdColumnLinks() + metadata.getUpdateInterval(), + Collections.emptyList() ); - proxy.setDelegate(columnMetadata, delegate); + proxy.setDelegate(metadata, delegate); } - public static PersistentValueImpl create(DataAccessor dataAccessor, UniqueData holder, Class dataType, String schema, String table, String column, List idColumnLinks) { - return new PersistentValueImpl<>(dataAccessor, holder, dataType, schema, table, column, idColumnLinks); + public static PersistentValueImpl create(UniqueData holder, Class dataType, String schema, String table, String column, int updateInterval, List idColumnLinks) { + return new PersistentValueImpl<>(holder, dataType, schema, table, column, updateInterval, idColumnLinks); } public static void delegate(T instance) { @@ -52,7 +53,7 @@ public static void delegate(T instance) { PersistentValueMetadata pvMetadata = metadata.persistentValueMetadata().get(pair.field()); ColumnMetadata columnMetadata = pvMetadata.getColumnMetadata(); if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { - PersistentValueImpl.createAndDelegate(proxyPv, columnMetadata); + PersistentValueImpl.createAndDelegate(proxyPv, pvMetadata); } else { pair.field().setAccessible(true); try { @@ -60,7 +61,7 @@ public static void delegate(T instance) { if (pvMetadata instanceof ForeignPersistentValueMetadata foreignPvMetadata) { idColumnLinks = foreignPvMetadata.getLinks(); } - pair.field().set(instance, PersistentValueImpl.create(instance.getDataManager().getDataAccessor(), instance, ReflectionUtils.getGenericType(pair.field()), columnMetadata.schema(), columnMetadata.table(), columnMetadata.name(), idColumnLinks)); + pair.field().set(instance, PersistentValueImpl.create(instance, ReflectionUtils.getGenericType(pair.field()), columnMetadata.schema(), columnMetadata.table(), columnMetadata.name(), pvMetadata.getUpdateInterval(), idColumnLinks)); } catch (IllegalAccessException e) { throw new RuntimeException(e); } @@ -74,6 +75,8 @@ public static Map extract IdColumn idColumn = field.getAnnotation(IdColumn.class); Column columnAnnotation = field.getAnnotation(Column.class); ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); + UpdateInterval updateIntervalAnnotation = field.getAnnotation(UpdateInterval.class); + int updateInterval = updateIntervalAnnotation != null ? updateIntervalAnnotation.value() : 0; if (idColumn != null) { Preconditions.checkArgument(columnAnnotation == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @Column", field.getName()); Preconditions.checkArgument(foreignColumn == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @ForeignColumn", field.getName()); @@ -86,7 +89,7 @@ public static Map extract false, "" ); - metadataMap.put(field, new PersistentValueMetadata(clazz, columnMetadata)); + metadataMap.put(field, new PersistentValueMetadata(clazz, columnMetadata, updateInterval)); continue; } if (columnAnnotation != null) { @@ -99,7 +102,7 @@ public static Map extract columnAnnotation.index(), columnAnnotation.defaultValue() ); - metadataMap.put(field, new PersistentValueMetadata(clazz, columnMetadata)); + metadataMap.put(field, new PersistentValueMetadata(clazz, columnMetadata, updateInterval)); continue; } if (foreignColumn != null) { @@ -119,7 +122,7 @@ public static Map extract Preconditions.checkArgument(parts.length == 2, "ForeignColumn link must be in the format localColumn=foreignColumn, got: %s", link); idColumnLinks.add(new ForeignKey.Link(ValueUtils.parseValue(parts[1]), ValueUtils.parseValue(parts[0]))); } - metadataMap.put(field, new ForeignPersistentValueMetadata(clazz, columnMetadata, idColumnLinks)); + metadataMap.put(field, new ForeignPersistentValueMetadata(clazz, columnMetadata, updateInterval, idColumnLinks)); continue; } @@ -143,11 +146,6 @@ public PersistentValue onUpdate(Class holderClass, throw new UnsupportedOperationException("Dynamically adding update handlers is not supported"); } - @Override - public List getIdColumnLinks() { - return idColumnLinks; - } - @Override public T get() { Preconditions.checkArgument(!holder.isDeleted(), "Cannot get value from a deleted UniqueData instance"); @@ -157,6 +155,6 @@ public T get() { @Override public void set(T value) { Preconditions.checkArgument(!holder.isDeleted(), "Cannot set value on a deleted UniqueData instance"); - holder.getDataManager().set(schema, table, column, holder.getIdColumns(), idColumnLinks, value, 0); //todo: this delay can be used to throttle frequent updates. allow it to be configured. this also needs to be tested + holder.getDataManager().set(schema, table, column, holder.getIdColumns(), idColumnLinks, value, updateInterval); } } diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index 3bdf132f..03b4358e 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -29,7 +29,6 @@ import java.sql.*; import java.util.*; import java.util.concurrent.*; -import java.util.function.Consumer; /** * This data accessor uses a write-behind caching strategy with an in-memory H2 database to optimize read and write operations. @@ -47,7 +46,7 @@ public class H2DataAccessor implements DataAccessor { private final Set knownTables = new HashSet<>(); private final DataManager dataManager; private final PostgresListener postgresListener; - private final Map> delayedTasks = new ConcurrentHashMap<>(); + private final Map delayedTasks = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(thread -> { Thread t = new Thread(thread); t.setName(H2DataAccessor.class.getSimpleName() + "-ScheduledExecutor"); @@ -467,30 +466,26 @@ private List getColumnsInTable(String schema, String table) throws SQLEx } private void runDatabaseTask(String sql, Object[] params, int delay) { - Consumer consumer = task -> taskQueue.submitTask(connection -> { - PreparedStatement realPreparedStatement = connection.prepareStatement(task.sql()); - for (int i = 0; i < task.params().length; i++) { - realPreparedStatement.setObject(i + 1, task.params()[i]); + Runnable runnable = () -> taskQueue.submitTask(connection -> { + PreparedStatement realPreparedStatement = connection.prepareStatement(sql); + for (int i = 0; i < params.length; i++) { + realPreparedStatement.setObject(i + 1, params[i]); } - logger.debug("[DB] {}}", task.sql()); + logger.debug("[DB] {}}", sql); realPreparedStatement.executeUpdate(); }); - DelayedDatabaseTask task = new DelayedDatabaseTask(sql, params); if (delay <= 0) { - consumer.accept(task); + runnable.run(); return; } - if (delayedTasks.put(task, consumer) == null) { + if (delayedTasks.put(sql, runnable) == null) { scheduledExecutorService.schedule(() -> { - Consumer removed = delayedTasks.remove(task); + Runnable removed = delayedTasks.remove(sql); if (removed != null) { - removed.accept(task); + removed.run(); } }, delay, TimeUnit.MILLISECONDS); } } } - - -//todo: maintain a buffer of what to send the the real db, and then collapse similar prepared statements into one so we can batch them. have a configurable interval to flush, but by default this will be 0ms diff --git a/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java b/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java index f07a0347..c66d7d19 100644 --- a/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java +++ b/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java @@ -9,8 +9,8 @@ public class ForeignPersistentValueMetadata extends PersistentValueMetadata { private final List links; - public ForeignPersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata, List links) { - super(holderClass, columnMetadata); + public ForeignPersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata, int updateInterval, List links) { + super(holderClass, columnMetadata, updateInterval); this.links = links; } diff --git a/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java b/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java index 95d2d253..b78b15fb 100644 --- a/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java +++ b/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java @@ -7,10 +7,12 @@ public class PersistentValueMetadata { private final Class holderClass; private final ColumnMetadata columnMetadata; + private final int updateInterval; - public PersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata) { + public PersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata, int updateInterval) { this.holderClass = holderClass; this.columnMetadata = columnMetadata; + this.updateInterval = updateInterval; } public String getSchema() { @@ -29,9 +31,13 @@ public ColumnMetadata getColumnMetadata() { return columnMetadata; } + public int getUpdateInterval() { + return updateInterval; + } + @Override public int hashCode() { - return Objects.hash(holderClass, columnMetadata); + return Objects.hash(holderClass, columnMetadata, updateInterval); } @Override @@ -39,6 +45,6 @@ public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; PersistentValueMetadata that = (PersistentValueMetadata) obj; - return Objects.equals(holderClass, that.holderClass) && Objects.equals(columnMetadata, that.columnMetadata); + return Objects.equals(holderClass, that.holderClass) && Objects.equals(columnMetadata, that.columnMetadata) && updateInterval == that.updateInterval; } } diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 77479ca4..5a5d070d 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -7,7 +7,9 @@ import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Test; +import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -296,4 +298,41 @@ public void testChangeIdColumnInPostgres() { waitForUpdateHandlers(); assertEquals(2, mockUser.nameUpdates.get()); } + + @Test + public void testUpdateInterval() throws Exception { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .favoriteColor("orange") + .nameUpdates(0) + .insert(InsertMode.SYNC); + + assertNull(mockUser.views.get()); + + for (int i = 0; i < 5; i++) { + mockUser.views.set(i); + } + + assertEquals(4, mockUser.views.get()); + waitForDataPropagation(); + Connection connection = getConnection(); + try (PreparedStatement preparedStatement = connection.prepareStatement("SELECT views FROM users WHERE id = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertNull(rs.getObject("views")); + } + + Thread.sleep(6000); + try (PreparedStatement preparedStatement = connection.prepareStatement("SELECT views FROM users WHERE id = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(4, rs.getInt("views")); + } + } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/src/test/java/net/staticstudios/data/mock/user/MockUser.java index ad519250..d0406376 100644 --- a/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -29,9 +29,11 @@ public class MockUser extends UniqueData { .onUpdate(MockUser.class, (user, update) -> { user.nameUpdates.set(user.getNameUpdates() + 1); }); + @UpdateInterval(5000) + @Column(name = "views", nullable = true) + public PersistentValue views; public int getNameUpdates() { return nameUpdates.get(); } - } From 87de2ef8b99a3dc20d51eaea96ee7615426c11c8 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 23 Sep 2025 19:19:59 -0400 Subject: [PATCH 20/75] more tests and bug fixes --- .../net/staticstudios/data/DataManager.java | 16 +- .../staticstudios/data/PersistentValue.java | 2 +- .../staticstudios/data/ValueSerializer.java | 6 +- .../data/insert/InsertContext.java | 4 +- .../staticstudios/data/parse/SQLBuilder.java | 49 ++- .../data/primative/Primitive.java | 30 +- .../data/primative/PrimitiveBuilder.java | 22 +- .../data/primative/Primitives.java | 83 ++--- .../net/staticstudios/data/util/SQLUtils.java | 45 +-- .../staticstudios/data/CustomTypeTest.java | 345 ++++++++++++++++++ .../staticstudios/data/PrimitivesTest.java | 122 +++++++ .../data/{ => misc}/MultiEnvironmentTest.java | 4 +- .../AccountDetailsValueSerializer.java | 5 +- .../AccountSettingsValueSerializer.java | 5 +- .../data/mock/user/MockUser.java | 2 +- .../booleanprimitive/BooleanWrapper.java | 5 + .../BooleanWrapperDataClass.java | 12 + .../BooleanWrapperValueSerializer.java | 27 ++ .../bytearrayprimitive/ByteArrayWrapper.java | 5 + .../ByteArrayWrapperDataClass.java | 12 + .../ByteArrayWrapperValueSerializer.java | 27 ++ .../doubleprimitive/DoubleWrapper.java | 5 + .../DoubleWrapperDataClass.java | 12 + .../DoubleWrapperValueSerializer.java | 27 ++ .../wrapper/floatprimitive/FloatWrapper.java | 5 + .../floatprimitive/FloatWrapperDataClass.java | 12 + .../FloatWrapperValueSerializer.java | 27 ++ .../integerprimitive/IntegerWrapper.java | 5 + .../IntegerWrapperDataClass.java | 12 + .../IntegerWrapperValueSerializer.java | 27 ++ .../wrapper/longprimitive/LongWrapper.java | 5 + .../longprimitive/LongWrapperDataClass.java | 12 + .../LongWrapperValueSerializer.java | 27 ++ .../stringprimitive/StringWrapper.java | 4 + .../StringWrapperDataClass.java | 11 + .../StringWrapperValueSerializer.java | 26 ++ .../timestampprimitive/TimestampWrapper.java | 7 + .../TimestampWrapperDataClass.java | 12 + .../TimestampWrapperValueSerializer.java | 29 ++ .../wrapper/uuidprimitive/UUIDWrapper.java | 7 + .../uuidprimitive/UUIDWrapperDataClass.java | 12 + .../UUIDWrapperValueSerializer.java | 29 ++ 42 files changed, 1016 insertions(+), 125 deletions(-) create mode 100644 src/test/java/net/staticstudios/data/PrimitivesTest.java rename src/test/java/net/staticstudios/data/{ => misc}/MultiEnvironmentTest.java (79%) create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java create mode 100644 src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index a678432d..49d2e594 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -557,7 +557,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { List values = new ArrayList<>(); for (SimpleColumnMetadata column : columnsInTable) { Object deserializedValue = insertContext.getEntries().get(column); - Object serializedValue = serialize(column.type(), deserializedValue); + Object serializedValue = serialize(deserializedValue); values.add(serializedValue); } sqlStatements.add(new SQlStatement(sql, values)); @@ -589,7 +589,7 @@ public T get(String schema, String table, String column, ColumnValuePairs id try (ResultSet rs = dataAccessor.executeQuery(sql, idColumns.stream().map(ColumnValuePair::value).toList())) { Object serializedValue = null; if (rs.next()) { - serializedValue = rs.getObject(column); + 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); @@ -666,7 +666,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC } @Language("SQL") String sql = sqlBuilder.toString(); List values = new ArrayList<>(1 + idColumns.getPairs().length); - values.add(serialize(value.getClass(), value)); + values.add(serialize(value)); for (ColumnValuePair columnValuePair : idColumns) { values.add(columnValuePair.value()); } @@ -734,18 +734,18 @@ public void registerValueSerializer(ValueSerializer serializer) { throw new IllegalStateException("No ValueSerializer registered for type " + deserializedType.getName()); } - private T deserialize(Class clazz, Object serialized) { - if (Primitives.isPrimitive(clazz) || serialized == null) { + public T deserialize(Class clazz, Object serialized) { + if (serialized == null || Primitives.isPrimitive(clazz)) { return (T) serialized; } return (T) deserialize(getValueSerializer(clazz), serialized); } - private T serialize(Class clazz, Object deserialized) { - if (Primitives.isPrimitive(clazz) || deserialized == null) { + public T serialize(Object deserialized) { + if (deserialized == null || Primitives.isPrimitive(deserialized.getClass())) { return (T) deserialized; } - return (T) serialize(getValueSerializer(clazz), deserialized); + return (T) serialize(getValueSerializer(deserialized.getClass()), deserialized); } private D deserialize(ValueSerializer serializer, Object serialized) { diff --git a/src/main/java/net/staticstudios/data/PersistentValue.java b/src/main/java/net/staticstudios/data/PersistentValue.java index e8d27910..5afd9cd0 100644 --- a/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/src/main/java/net/staticstudios/data/PersistentValue.java @@ -18,7 +18,7 @@ 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 - //todo: insert strategy, deletion strategy, update interval, update handling + //todo: insert strategy, deletion strategy static PersistentValue of(UniqueData holder, Class dataType) { return new ProxyPersistentValue<>(holder, dataType); diff --git a/src/main/java/net/staticstudios/data/ValueSerializer.java b/src/main/java/net/staticstudios/data/ValueSerializer.java index df52b82c..472c14b9 100644 --- a/src/main/java/net/staticstudios/data/ValueSerializer.java +++ b/src/main/java/net/staticstudios/data/ValueSerializer.java @@ -1,5 +1,7 @@ package net.staticstudios.data; +import org.jetbrains.annotations.NotNull; + /** * A serializer for non-primitive types. * See {@link net.staticstudios.data.primative.Primitives} for primitive types. @@ -15,7 +17,7 @@ public interface ValueSerializer { * @param serialized The serialized object * @return The deserialized object */ - D deserialize(S serialized); + D deserialize(@NotNull S serialized); /** * Serialize the deserialized object @@ -23,7 +25,7 @@ public interface ValueSerializer { * @param deserialized The deserialized object * @return The serialized object */ - S serialize(D deserialized); + S serialize(@NotNull D deserialized); /** * Get the deserialized type diff --git a/src/main/java/net/staticstudios/data/insert/InsertContext.java b/src/main/java/net/staticstudios/data/insert/InsertContext.java index 154e4bae..170fe2a7 100644 --- a/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -53,9 +53,9 @@ public InsertContext set(String schema, String table, String column, @Nullable O return this; } - Preconditions.checkArgument(sqlColumn.getType().isInstance(value), "Value type mismatch for name " + column + " in table " + table + " schema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); + Preconditions.checkArgument(sqlColumn.getType().isAssignableFrom(dataManager.getSerializedType(value.getClass())), "Value type mismatch for name " + column + " in table " + table + " schema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); - entries.put(columnMetadata, value); + entries.put(columnMetadata, dataManager.serialize(value)); return this; } diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index c47b6471..da397f58 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -69,38 +69,53 @@ private List getDefs(Collection schemas) { List statements = new ArrayList<>(); for (SQLSchema schema : schemas) { statements.add(DDLStatement.both("CREATE SCHEMA IF NOT EXISTS \"" + schema.getName() + "\";")); - StringBuilder sb; + StringBuilder h2Sb; + StringBuilder pgSb; for (SQLTable table : schema.getTables()) { - sb = new StringBuilder(); - sb.append("CREATE TABLE IF NOT EXISTS \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" (\n"); + h2Sb = new StringBuilder(); + pgSb = new StringBuilder(); + h2Sb.append("CREATE TABLE IF NOT EXISTS \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" (\n"); + pgSb.append("CREATE TABLE IF NOT EXISTS \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" (\n"); for (ColumnMetadata idColumn : table.getIdColumns()) { - sb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append(SQLUtils.getSqlType(idColumn.type())).append(",\n"); + h2Sb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append(SQLUtils.getH2SqlType(idColumn.type())).append(",\n"); + pgSb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append(SQLUtils.getPgSqlType(idColumn.type())).append(" NOT NULL,\n"); } - sb.append(INDENT).append("PRIMARY KEY ("); + h2Sb.append(INDENT).append("PRIMARY KEY ("); + pgSb.append(INDENT).append("PRIMARY KEY ("); for (ColumnMetadata idColumn : table.getIdColumns()) { - sb.append("\"").append(idColumn.name()).append("\", "); + h2Sb.append("\"").append(idColumn.name()).append("\", "); + pgSb.append("\"").append(idColumn.name()).append("\", "); } - sb.setLength(sb.length() - 2); - sb.append(")\n"); - sb.append(");"); - statements.add(DDLStatement.both(sb.toString())); + h2Sb.setLength(h2Sb.length() - 2); + pgSb.setLength(pgSb.length() - 2); + h2Sb.append(")\n"); + pgSb.append(")\n"); + h2Sb.append(");"); + pgSb.append(");"); + statements.add(DDLStatement.of(h2Sb.toString(), pgSb.toString())); if (!table.getColumns().isEmpty()) { for (SQLColumn column : table.getColumns()) { - sb = new StringBuilder(); - sb.append("ALTER TABLE \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ").append("ADD COLUMN IF NOT EXISTS ").append("\"").append(column.getName()).append("\" ").append(SQLUtils.getSqlType(column.getType())); + h2Sb = new StringBuilder(); + pgSb = new StringBuilder(); + h2Sb.append("ALTER TABLE \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ").append("ADD COLUMN IF NOT EXISTS ").append("\"").append(column.getName()).append("\" ").append(SQLUtils.getH2SqlType(column.getType())); + pgSb.append("ALTER TABLE \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ").append("ADD COLUMN IF NOT EXISTS ").append("\"").append(column.getName()).append("\" ").append(SQLUtils.getPgSqlType(column.getType())); if (!column.isNullable()) { - sb.append(" NOT NULL"); + h2Sb.append(" NOT NULL"); + pgSb.append(" NOT NULL"); } if (column.getDefaultValue() != null) { - sb.append(" DEFAULT ").append(column.getDefaultValue()); + h2Sb.append(" DEFAULT ").append(column.getDefaultValue()); + pgSb.append(" DEFAULT ").append(column.getDefaultValue()); } if (column.isUnique()) { - sb.append(" UNIQUE"); + h2Sb.append(" UNIQUE"); + pgSb.append(" UNIQUE"); } - sb.append(";"); - statements.add(DDLStatement.both(sb.toString())); + h2Sb.append(";"); + pgSb.append(";"); + statements.add(DDLStatement.of(h2Sb.toString(), pgSb.toString())); } } } diff --git a/src/main/java/net/staticstudios/data/primative/Primitive.java b/src/main/java/net/staticstudios/data/primative/Primitive.java index 96db9d99..815778a1 100644 --- a/src/main/java/net/staticstudios/data/primative/Primitive.java +++ b/src/main/java/net/staticstudios/data/primative/Primitive.java @@ -1,34 +1,50 @@ package net.staticstudios.data.primative; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.util.function.Function; public class Primitive { private final Class runtimeType; - private final Function decoder; - private final Function encoder; + private final Function<@NotNull String, @NotNull T> decoder; + private final Function<@NotNull T, @NotNull String> encoder; + private final String h2SQLType; + private final String pgSQLType; - public Primitive(Class runtimeType, Function decoder, Function encoder) { + public Primitive(Class runtimeType, Function<@NotNull String, @NotNull T> decoder, Function<@NotNull T, @NotNull String> encoder, String h2SQLType, String pgSQLType) { this.runtimeType = runtimeType; this.decoder = decoder; this.encoder = encoder; + this.h2SQLType = h2SQLType; + this.pgSQLType = pgSQLType; } public static PrimitiveBuilder builder(Class runtimeType) { return new PrimitiveBuilder<>(runtimeType); } - public T decode(String value) { + public @Nullable T decode(@Nullable String value) { + if (value == null) { + return null; + } return decoder.apply(value); } - public String encode(T value) { + public @Nullable String encode(@Nullable T value) { + if (value == null) { + return null; + } return encoder.apply(value); } - public String unsafeEncode(Object value) { - return encoder.apply(runtimeType.cast(value)); + public String getH2SQLType() { + return h2SQLType; } + public String getPgSQLType() { + return pgSQLType; + } public Class getRuntimeType() { return runtimeType; diff --git a/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java b/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java index 9574564a..cec09e5d 100644 --- a/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java +++ b/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.jetbrains.annotations.NotNull; import java.util.function.Consumer; import java.util.function.Function; @@ -9,12 +10,14 @@ public class PrimitiveBuilder { private final Class runtimeType; private Function decoder; private Function encoder; + private String h2SQLType; + private String pgSQLType; public PrimitiveBuilder(Class runtimeType) { this.runtimeType = runtimeType; } - public PrimitiveBuilder decoder(Function decoder) { + public PrimitiveBuilder decoder(Function<@NotNull String, @NotNull T> decoder) { this.decoder = decoder; return this; } @@ -25,17 +28,30 @@ public PrimitiveBuilder decoder(Function decoder) { * @param encoder The encoder function * @return The builder */ - public PrimitiveBuilder encoder(Function encoder) { + public PrimitiveBuilder encoder(Function<@NotNull T, @NotNull String> encoder) { this.encoder = encoder; return this; } + public PrimitiveBuilder h2SQLType(String h2SQLType) { + this.h2SQLType = h2SQLType; + return this; + } + + public PrimitiveBuilder pgSQLType(String pgSQLType) { + this.pgSQLType = pgSQLType; + return this; + } + + public Primitive build(Consumer> consumer) { Preconditions.checkNotNull(decoder, "Decoder is null"); Preconditions.checkNotNull(encoder, "Encoder 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); + Primitive primitive = new Primitive<>(runtimeType, decoder, encoder, h2SQLType, pgSQLType); consumer.accept(primitive); return primitive; diff --git a/src/main/java/net/staticstudios/data/primative/Primitives.java b/src/main/java/net/staticstudios/data/primative/Primitives.java index 1e35c250..d5c8cb8e 100644 --- a/src/main/java/net/staticstudios/data/primative/Primitives.java +++ b/src/main/java/net/staticstudios/data/primative/Primitives.java @@ -12,7 +12,7 @@ import java.util.Map; @SuppressWarnings("unused") -public class Primitives { //todo: test each of these in h2 and pg. +public class Primitives { private static final DateTimeFormatter TIMESTAMP_FORMATTER = new DateTimeFormatterBuilder() .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) .appendPattern("xxx") @@ -21,81 +21,63 @@ public class Primitives { //todo: test each of these in h2 and pg. private static Map, Primitive> primitives; public static final Primitive STRING = Primitive.builder(String.class) + .h2SQLType("TEXT") + .pgSQLType("TEXT") .encoder(s -> s) .decoder(s -> s) .build(Primitives::register); - public static final Primitive CHARACTER = Primitive.builder(Character.class) - .encoder(c -> Character.toString(c)) - .decoder(s -> s.charAt(0)) - .build(Primitives::register); - public static final Primitive BYTE = Primitive.builder(Byte.class) - .encoder(b -> Byte.toString(b)) - .decoder(Byte::parseByte) - .build(Primitives::register); - public static final Primitive SHORT = Primitive.builder(Short.class) - .encoder(s -> Short.toString(s)) - .decoder(Short::parseShort) - .build(Primitives::register); public static final Primitive INTEGER = Primitive.builder(Integer.class) + .h2SQLType("INTEGER") + .pgSQLType("INTEGER") .encoder(i -> Integer.toString(i)) .decoder(Integer::parseInt) .build(Primitives::register); public static final Primitive LONG = Primitive.builder(Long.class) + .h2SQLType("BIGINT") + .pgSQLType("BIGINT") .encoder(l -> Long.toString(l)) .decoder(Long::parseLong) .build(Primitives::register); public static final Primitive FLOAT = Primitive.builder(Float.class) + .h2SQLType("REAL") + .pgSQLType("REAL") .encoder(f -> Float.toString(f)) .decoder(Float::parseFloat) .build(Primitives::register); public static final Primitive DOUBLE = Primitive.builder(Double.class) + .h2SQLType("DOUBLE PRECISION") + .pgSQLType("DOUBLE PRECISION") .encoder(d -> Double.toString(d)) .decoder(Double::parseDouble) .build(Primitives::register); public static final Primitive BOOLEAN = Primitive.builder(Boolean.class) + .h2SQLType("BOOLEAN") + .pgSQLType("BOOLEAN") .encoder(b -> Boolean.toString(b)) .decoder(Boolean::parseBoolean) .build(Primitives::register); public static final Primitive UUID = Primitive.builder(java.util.UUID.class) - .encoder(uuid -> uuid == null ? null : uuid.toString()) - .decoder(s -> s == null ? null : java.util.UUID.fromString(s)) + .h2SQLType("UUID") + .pgSQLType("UUID") + .encoder(java.util.UUID::toString) + .decoder(java.util.UUID::fromString) .build(Primitives::register); public static final Primitive TIMESTAMP = Primitive.builder(Timestamp.class) - .encoder(timestamp -> { - if (timestamp == null) { - return null; - } - - return TIMESTAMP_FORMATTER.format(timestamp.toInstant()); - }) - .decoder(s -> { - if (s == null) { - return null; - } - - OffsetDateTime parsedTimestamp = OffsetDateTime.parse(s, TIMESTAMP_FORMATTER); - return Timestamp.from(parsedTimestamp.toInstant()); - }) + .h2SQLType("TIMESTAMP WITH TIME ZONE") + .pgSQLType("TIMESTAMPTZ") + .encoder(timestamp -> TIMESTAMP_FORMATTER.format(timestamp.toInstant())) + .decoder(s -> Timestamp.from(OffsetDateTime.parse(s, TIMESTAMP_FORMATTER).toInstant())) .build(Primitives::register); public static final Primitive BYTE_ARRAY = Primitive.builder(byte[].class) - .encoder(b -> { - if (b == null) { - return null; - } - - return PostgresUtils.toHex(b); - }) - .decoder(s -> { - if (s == null) { - return null; - } - - return PostgresUtils.toBytes(s); - }) + .h2SQLType("BINARY LARGE OBJECT") + .pgSQLType("BYTEA") + .encoder(PostgresUtils::toHex) + .decoder(PostgresUtils::toBytes) .build(Primitives::register); - public static Primitive getPrimitive(Class type) { - return primitives.get(type); + @SuppressWarnings("unchecked") + public static Primitive getPrimitive(Class type) { + return (Primitive) primitives.get(type); } public static Object decodePrimitive(Class type, String value) { @@ -108,16 +90,19 @@ public static boolean isPrimitive(Class type) { return primitives.containsKey(type); } - @SuppressWarnings("unchecked") public static T decode(Class type, String value) { - return (T) getPrimitive(type).decode(value); + return getPrimitive(type).decode(value); } public static String encode(Object value) { if (value == null) { return null; } - return getPrimitive(value.getClass()).unsafeEncode(value); + return encode(value, value.getClass()); + } + + private static String encode(Object value, Class type) { + return getPrimitive(type).encode(type.cast(value)); } private static void register(Primitive primitive) { diff --git a/src/main/java/net/staticstudios/data/util/SQLUtils.java b/src/main/java/net/staticstudios/data/util/SQLUtils.java index 4246fec4..d6f4ab21 100644 --- a/src/main/java/net/staticstudios/data/util/SQLUtils.java +++ b/src/main/java/net/staticstudios/data/util/SQLUtils.java @@ -1,40 +1,29 @@ package net.staticstudios.data.util; +import net.staticstudios.data.primative.Primitives; + public class SQLUtils { - public static String getSqlType(Class clazz) { - if (clazz.equals(String.class)) { - return "TEXT"; - } - if (clazz.equals(Integer.class) || clazz.equals(int.class)) { - return "INTEGER"; - } - if (clazz.equals(Long.class) || clazz.equals(long.class)) { - return "BIGINT"; - } - if (clazz.equals(Boolean.class) || clazz.equals(boolean.class)) { - return "BOOLEAN"; - } - if (clazz.equals(Double.class) || clazz.equals(double.class)) { - return "DOUBLE PRECISION"; + public static String getH2SqlType(Class clazz) { + if (Primitives.isPrimitive(clazz)) { + return Primitives.getPrimitive(clazz).getH2SQLType(); } - if (clazz.equals(Float.class) || clazz.equals(float.class)) { - return "REAL"; - } - if (clazz.equals(java.util.UUID.class)) { - return "UUID"; + throw new IllegalArgumentException("Unsupported class type: " + clazz.getName()); + } + + public static String getPgSqlType(Class clazz) { + if (Primitives.isPrimitive(clazz)) { + return Primitives.getPrimitive(clazz).getPgSQLType(); } throw new IllegalArgumentException("Unsupported class type: " + clazz.getName()); } public static String parseDefaultValue(Class clazz, String defaultValue) { - try { - return switch (getSqlType(clazz)) { - case "TEXT" -> "'" + defaultValue.replace("'", "''") + "'"; - case "BOOLEAN" -> Boolean.parseBoolean(defaultValue) ? "TRUE" : "FALSE"; - default -> defaultValue; - }; - } catch (IllegalArgumentException e) { - return defaultValue; + if (clazz == String.class) { + return "'" + defaultValue.replace("'", "''") + "'"; + } + if (clazz == Boolean.class) { + return Boolean.parseBoolean(defaultValue) ? "TRUE" : "FALSE"; } + return defaultValue; } } diff --git a/src/test/java/net/staticstudios/data/CustomTypeTest.java b/src/test/java/net/staticstudios/data/CustomTypeTest.java index 2f07dccb..9fa705ca 100644 --- a/src/test/java/net/staticstudios/data/CustomTypeTest.java +++ b/src/test/java/net/staticstudios/data/CustomTypeTest.java @@ -2,9 +2,46 @@ import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.mock.account.*; +import net.staticstudios.data.mock.wrapper.booleanprimitive.BooleanWrapper; +import net.staticstudios.data.mock.wrapper.booleanprimitive.BooleanWrapperDataClass; +import net.staticstudios.data.mock.wrapper.booleanprimitive.BooleanWrapperDataClassFactory; +import net.staticstudios.data.mock.wrapper.booleanprimitive.BooleanWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapper; +import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapperDataClass; +import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapperDataClassFactory; +import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.doubleprimitive.DoubleWrapper; +import net.staticstudios.data.mock.wrapper.doubleprimitive.DoubleWrapperDataClass; +import net.staticstudios.data.mock.wrapper.doubleprimitive.DoubleWrapperDataClassFactory; +import net.staticstudios.data.mock.wrapper.doubleprimitive.DoubleWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapper; +import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapperDataClass; +import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapperDataClassFactory; +import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.integerprimitive.IntegerWrapper; +import net.staticstudios.data.mock.wrapper.integerprimitive.IntegerWrapperDataClass; +import net.staticstudios.data.mock.wrapper.integerprimitive.IntegerWrapperDataClassFactory; +import net.staticstudios.data.mock.wrapper.integerprimitive.IntegerWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapper; +import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapperDataClass; +import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapperDataClassFactory; +import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.stringprimitive.StringWrapper; +import net.staticstudios.data.mock.wrapper.stringprimitive.StringWrapperDataClass; +import net.staticstudios.data.mock.wrapper.stringprimitive.StringWrapperDataClassFactory; +import net.staticstudios.data.mock.wrapper.stringprimitive.StringWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapper; +import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapperDataClass; +import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapperDataClassFactory; +import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.uuidprimitive.UUIDWrapper; +import net.staticstudios.data.mock.wrapper.uuidprimitive.UUIDWrapperDataClass; +import net.staticstudios.data.mock.wrapper.uuidprimitive.UUIDWrapperDataClassFactory; +import net.staticstudios.data.mock.wrapper.uuidprimitive.UUIDWrapperValueSerializer; import org.junit.jupiter.api.Test; import java.sql.Connection; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -64,5 +101,313 @@ public void testCustomTypesLoad() throws SQLException { assertEquals(details, account.details.get()); } + @Test + public void testCustomTypeWithStringPrimitive() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new StringWrapperValueSerializer()); + dataManager.load(StringWrapperDataClass.class); + StringWrapperDataClass data = StringWrapperDataClassFactory.builder(dataManager) + .id(1) + .value(new StringWrapper("Hello, World!")) + .insert(InsertMode.SYNC); + + assertNotNull(data.value.get()); + assertEquals("Hello, World!", data.value.get().value()); + waitForDataPropagation(); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertEquals("Hello, World!", rs.getObject("val")); + } + } + + data.value.set(null); + assertNull(data.value.get()); + waitForDataPropagation(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertNull(rs.getObject("val")); + } + } + } + + @Test + public void testCustomTypeWithIntegerPrimitive() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new IntegerWrapperValueSerializer()); + dataManager.load(IntegerWrapperDataClass.class); + IntegerWrapperDataClass data = IntegerWrapperDataClassFactory.builder(dataManager) + .id(1) + .value(new IntegerWrapper(42)) + .insert(InsertMode.SYNC); + + assertNotNull(data.value.get()); + assertEquals(42, data.value.get().value()); + + waitForDataPropagation(); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_integer\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertEquals(42, rs.getObject("val")); + } + } + + data.value.set(null); + assertNull(data.value.get()); + waitForDataPropagation(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_integer\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertNull(rs.getObject("val")); + } + } + } + + @Test + public void testCustomTypeWithLongPrimitive() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new LongWrapperValueSerializer()); + dataManager.load(LongWrapperDataClass.class); + LongWrapperDataClass data = LongWrapperDataClassFactory.builder(dataManager) + .id(1) + .value(new LongWrapper(1234567890123L)) + .insert(InsertMode.SYNC); + + assertNotNull(data.value.get()); + assertEquals(1234567890123L, data.value.get().value()); + + waitForDataPropagation(); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_long\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertEquals(1234567890123L, rs.getObject("val")); + } + } + + data.value.set(null); + assertNull(data.value.get()); + waitForDataPropagation(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_long\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertNull(rs.getObject("val")); + } + } + } + + @Test + public void testCustomTypeWithFloatPrimitive() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new FloatWrapperValueSerializer()); + dataManager.load(FloatWrapperDataClass.class); + FloatWrapperDataClass data = FloatWrapperDataClassFactory.builder(dataManager) + .id(1) + .value(new FloatWrapper(3.14f)) + .insert(InsertMode.SYNC); + + assertNotNull(data.value.get()); + assertEquals(3.14f, data.value.get().value()); + + waitForDataPropagation(); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_float\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertEquals(3.14f, rs.getObject("val")); + } + } + + data.value.set(null); + assertNull(data.value.get()); + waitForDataPropagation(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_float\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertNull(rs.getObject("val")); + } + } + } + + @Test + public void testCustomTypeWithDoublePrimitive() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new DoubleWrapperValueSerializer()); + dataManager.load(DoubleWrapperDataClass.class); + DoubleWrapperDataClass data = DoubleWrapperDataClassFactory.builder(dataManager) + .id(1) + .value(new DoubleWrapper(2.71828)) + .insert(InsertMode.SYNC); + + assertNotNull(data.value.get()); + assertEquals(2.71828, data.value.get().value()); + + waitForDataPropagation(); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_double\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertEquals(2.71828, rs.getObject("val")); + } + } + + data.value.set(null); + assertNull(data.value.get()); + waitForDataPropagation(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_double\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertNull(rs.getObject("val")); + } + } + } + + @Test + public void testCustomTypeWithBooleanPrimitive() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new BooleanWrapperValueSerializer()); + dataManager.load(BooleanWrapperDataClass.class); + BooleanWrapperDataClass data = BooleanWrapperDataClassFactory.builder(dataManager) + .id(1) + .value(new BooleanWrapper(true)) + .insert(InsertMode.SYNC); + + assertNotNull(data.value.get()); + assertTrue(data.value.get().value()); + + waitForDataPropagation(); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_boolean\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertEquals(true, rs.getObject("val")); + } + } + + data.value.set(null); + assertNull(data.value.get()); + waitForDataPropagation(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_boolean\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertNull(rs.getObject("val")); + } + } + } + + @Test + public void testCustomTypeWithUUIDPrimitive() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new UUIDWrapperValueSerializer()); + dataManager.load(UUIDWrapperDataClass.class); + java.util.UUID uuid = java.util.UUID.randomUUID(); + UUIDWrapperDataClass data = UUIDWrapperDataClassFactory.builder(dataManager) + .id(1) + .value(new UUIDWrapper(uuid)) + .insert(InsertMode.SYNC); + + assertNotNull(data.value.get()); + assertEquals(uuid, data.value.get().value()); + + waitForDataPropagation(); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_uuid\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertEquals(uuid, rs.getObject("val")); + } + } + + data.value.set(null); + assertNull(data.value.get()); + waitForDataPropagation(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_uuid\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertNull(rs.getObject("val")); + } + } + } + + @Test + public void testCustomTypeWithTimestampPrimitive() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new TimestampWrapperValueSerializer()); + dataManager.load(TimestampWrapperDataClass.class); + java.sql.Timestamp timestamp = new java.sql.Timestamp(System.currentTimeMillis()); + TimestampWrapperDataClass data = TimestampWrapperDataClassFactory.builder(dataManager) + .id(1) + .value(new TimestampWrapper(timestamp)) + .insert(InsertMode.SYNC); + + assertNotNull(data.value.get()); + assertEquals(timestamp, data.value.get().value()); + + waitForDataPropagation(); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_timestamp\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertEquals(timestamp, rs.getObject("val")); + } + } + + data.value.set(null); + assertNull(data.value.get()); + waitForDataPropagation(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_timestamp\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertNull(rs.getObject("val")); + } + } + } + + @Test + public void testCustomTypeWithByteArrayPrimitive() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.registerValueSerializer(new ByteArrayWrapperValueSerializer()); + dataManager.load(ByteArrayWrapperDataClass.class); + byte[] bytes = new byte[]{1, 2, 3, 4, 5}; + ByteArrayWrapperDataClass data = ByteArrayWrapperDataClassFactory.builder(dataManager) + .id(1) + .value(new ByteArrayWrapper(bytes)) + .insert(InsertMode.SYNC); + + assertNotNull(data.value.get()); + assertArrayEquals(bytes, data.value.get().value()); + + waitForDataPropagation(); + + Connection connection = getConnection(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_bytea\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertArrayEquals(bytes, (byte[]) rs.getObject("val")); + } + } + + data.value.set(null); + assertNull(data.value.get()); + waitForDataPropagation(); + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT * FROM \"public\".\"custom_type_test_bytea\" WHERE \"id\" = 1;")) { + assertTrue(rs.next()); + assertNull(rs.getObject("val")); + } + } + } + //todo: test postgres listen/notify with custom types } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/PrimitivesTest.java b/src/test/java/net/staticstudios/data/PrimitivesTest.java new file mode 100644 index 00000000..c21001c8 --- /dev/null +++ b/src/test/java/net/staticstudios/data/PrimitivesTest.java @@ -0,0 +1,122 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.primative.Primitive; +import net.staticstudios.data.primative.Primitives; +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.*; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertNull; + +public class PrimitivesTest extends DataTest { + private Connection postgresConnection; + private Connection h2Connection; + + @BeforeEach + public void setup() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + postgresConnection = getConnection(); + h2Connection = getH2Connection(dataManager); + } + + @Test + public void testString() throws Exception { + test(Primitives.STRING, "Hello, World!"); + } + + @Test + public void testInteger() throws Exception { + test(Primitives.INTEGER, 12345); + } + + @Test + public void testLong() throws Exception { + test(Primitives.LONG, 123456789L); + } + + @Test + public void testFloat() throws Exception { + test(Primitives.FLOAT, 123.45f); + } + + @Test + public void testDouble() throws Exception { + test(Primitives.DOUBLE, 123456.789); + } + + @Test + public void testBoolean() throws Exception { + test(Primitives.BOOLEAN, true); + test(Primitives.BOOLEAN, false); + } + + @Test + public void testUUID() throws Exception { + test(Primitives.UUID, UUID.randomUUID()); + } + + @Test + public void testTimestamp() throws Exception { + test(Primitives.TIMESTAMP, new Timestamp(System.currentTimeMillis())); + } + + @Test + public void testByteArray() throws Exception { + test(Primitives.BYTE_ARRAY, new byte[]{1, 2, 3, 4, 5}); + } + + private void test(Primitive primitive, T value) throws Exception { + // note that encoding and decoding is only in the context of PG. H2 byte[] is in a different format, but we don't every encode/decode when working with H2. + @Language("SQL") String sql = "CREATE TABLE test (id INT PRIMARY KEY, val %s)"; + T decoded = primitive.decode(primitive.encode(value)); + assertEquals(value, decoded); + assertNull(primitive.decode(null)); + assertNull(primitive.encode(null)); + + try (Statement h2Statement = h2Connection.createStatement()) { + h2Statement.execute("DROP TABLE IF EXISTS test"); + h2Statement.execute(String.format(sql, primitive.getH2SQLType())); + } + + try (PreparedStatement h2Statement = h2Connection.prepareStatement("INSERT INTO test (id, val) VALUES (?, ?)")) { + h2Statement.setInt(1, 1); + h2Statement.setObject(2, value); + h2Statement.executeUpdate(); + } + + try (Statement h2Statement = h2Connection.createStatement()) { + try (ResultSet rs = h2Statement.executeQuery("SELECT val FROM test WHERE id = 1")) { + if (rs.next()) { + Object fromDb = rs.getObject("val", primitive.getRuntimeType()); + assertEquals(value, fromDb); + } + } + } + + try (Statement postgresStatement = postgresConnection.createStatement()) { + postgresStatement.execute("DROP TABLE IF EXISTS test"); + postgresStatement.execute(String.format(sql, primitive.getPgSQLType())); + postgresStatement.execute(String.format("INSERT INTO test (id, val) VALUES (1, '%s'::%s)", primitive.encode(value), primitive.getPgSQLType())); + try (ResultSet rs = postgresStatement.executeQuery("SELECT val FROM test WHERE id = 1")) { + if (rs.next()) { + Object fromDb = rs.getObject("val"); + assertEquals(value, fromDb); + } + } + } + } + + public void assertEquals(Object expected, Object actual) { + if (expected instanceof byte[] expectedBytes && actual instanceof byte[] actualBytes) { + Assertions.assertArrayEquals(expectedBytes, actualBytes); + } else { + Assertions.assertEquals(expected, actual); + } + } + +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/MultiEnvironmentTest.java b/src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java similarity index 79% rename from src/test/java/net/staticstudios/data/MultiEnvironmentTest.java rename to src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java index 366e8468..50467070 100644 --- a/src/test/java/net/staticstudios/data/MultiEnvironmentTest.java +++ b/src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java @@ -1,7 +1,5 @@ -package net.staticstudios.data; +package net.staticstudios.data.misc; -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java b/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java index e88ef49f..35f8ba5e 100644 --- a/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java +++ b/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java @@ -2,12 +2,13 @@ import com.google.gson.Gson; import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; public class AccountDetailsValueSerializer implements ValueSerializer { private static final Gson GSON = new Gson(); @Override - public AccountDetails deserialize(String serialized) { + public AccountDetails deserialize(@NotNull String serialized) { if (serialized == null || serialized.isEmpty()) { return null; } @@ -15,7 +16,7 @@ public AccountDetails deserialize(String serialized) { } @Override - public String serialize(AccountDetails deserialized) { + public String serialize(@NotNull AccountDetails deserialized) { if (deserialized == null) { return null; } diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java b/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java index 8aaffbc5..eeb1f356 100644 --- a/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java +++ b/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java @@ -2,12 +2,13 @@ import com.google.gson.Gson; import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; public class AccountSettingsValueSerializer implements ValueSerializer { private static final Gson GSON = new Gson(); @Override - public AccountSettings deserialize(String serialized) { + public AccountSettings deserialize(@NotNull String serialized) { if (serialized == null || serialized.isEmpty()) { return null; } @@ -15,7 +16,7 @@ public AccountSettings deserialize(String serialized) { } @Override - public String serialize(AccountSettings deserialized) { + public String serialize(@NotNull AccountSettings deserialized) { if (deserialized == null) { return null; } diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/src/test/java/net/staticstudios/data/mock/user/MockUser.java index d0406376..f335f0f2 100644 --- a/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -6,7 +6,7 @@ //todo: heres how inheritance should look: // if the super class provides a data annotation, ignore it and use the child's annotation. it would be cool tho to allow the super class to use a @data annotation. the former is whats implemented now. if changed, update the processor. - +//todo: make default values their own annotation @Data(schema = "public", table = "users") public class MockUser extends UniqueData { //todo: cached values diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java b/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java new file mode 100644 index 00000000..cbde13c7 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.mock.wrapper.booleanprimitive; + +public record BooleanWrapper(Boolean value) { +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java b/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java new file mode 100644 index 00000000..0045d128 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.mock.wrapper.booleanprimitive; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "custom_type_test_boolean") +public class BooleanWrapperDataClass extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "val", nullable = true) + public PersistentValue value; +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java b/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java new file mode 100644 index 00000000..d3f01ab4 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.mock.wrapper.booleanprimitive; + +import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; + +public class BooleanWrapperValueSerializer implements ValueSerializer { + @Override + public BooleanWrapper deserialize(@NotNull Boolean serialized) { + return new BooleanWrapper(serialized); + } + + @Override + public Boolean serialize(@NotNull BooleanWrapper deserialized) { + return deserialized.value(); + } + + @Override + public Class getDeserializedType() { + return BooleanWrapper.class; + } + + @Override + public Class getSerializedType() { + return Boolean.class; + } +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java b/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java new file mode 100644 index 00000000..85865ed0 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.mock.wrapper.bytearrayprimitive; + +public record ByteArrayWrapper(byte[] value) { +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java b/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java new file mode 100644 index 00000000..e7a9afcd --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.mock.wrapper.bytearrayprimitive; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "custom_type_test_bytea") +public class ByteArrayWrapperDataClass extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "val", nullable = true) + public PersistentValue value; +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java b/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java new file mode 100644 index 00000000..a030a344 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.mock.wrapper.bytearrayprimitive; + +import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; + +public class ByteArrayWrapperValueSerializer implements ValueSerializer { + @Override + public ByteArrayWrapper deserialize(@NotNull byte[] serialized) { + return new ByteArrayWrapper(serialized); + } + + @Override + public byte[] serialize(@NotNull ByteArrayWrapper deserialized) { + return deserialized.value(); + } + + @Override + public Class getDeserializedType() { + return ByteArrayWrapper.class; + } + + @Override + public Class getSerializedType() { + return byte[].class; + } +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java b/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java new file mode 100644 index 00000000..bf6f7853 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.mock.wrapper.doubleprimitive; + +public record DoubleWrapper(Double value) { +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java b/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java new file mode 100644 index 00000000..30d68cdf --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.mock.wrapper.doubleprimitive; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "custom_type_test_double") +public class DoubleWrapperDataClass extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "val", nullable = true) + public PersistentValue value; +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java b/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java new file mode 100644 index 00000000..372110e3 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.mock.wrapper.doubleprimitive; + +import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; + +public class DoubleWrapperValueSerializer implements ValueSerializer { + @Override + public DoubleWrapper deserialize(@NotNull Double serialized) { + return new DoubleWrapper(serialized); + } + + @Override + public Double serialize(@NotNull DoubleWrapper deserialized) { + return deserialized.value(); + } + + @Override + public Class getDeserializedType() { + return DoubleWrapper.class; + } + + @Override + public Class getSerializedType() { + return Double.class; + } +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java b/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java new file mode 100644 index 00000000..aff0bae8 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.mock.wrapper.floatprimitive; + +public record FloatWrapper(Float value) { +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java b/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java new file mode 100644 index 00000000..2d2dad73 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.mock.wrapper.floatprimitive; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "custom_type_test_float") +public class FloatWrapperDataClass extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "val", nullable = true) + public PersistentValue value; +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java b/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java new file mode 100644 index 00000000..8c0775c9 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.mock.wrapper.floatprimitive; + +import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; + +public class FloatWrapperValueSerializer implements ValueSerializer { + @Override + public FloatWrapper deserialize(@NotNull Float serialized) { + return new FloatWrapper(serialized); + } + + @Override + public Float serialize(@NotNull FloatWrapper deserialized) { + return deserialized.value(); + } + + @Override + public Class getDeserializedType() { + return FloatWrapper.class; + } + + @Override + public Class getSerializedType() { + return Float.class; + } +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java b/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java new file mode 100644 index 00000000..113ac306 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.mock.wrapper.integerprimitive; + +public record IntegerWrapper(Integer value) { +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java b/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java new file mode 100644 index 00000000..dad66f23 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.mock.wrapper.integerprimitive; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "custom_type_test_integer") +public class IntegerWrapperDataClass extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "val", nullable = true) + public PersistentValue value; +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java b/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java new file mode 100644 index 00000000..52235850 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.mock.wrapper.integerprimitive; + +import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; + +public class IntegerWrapperValueSerializer implements ValueSerializer { + @Override + public IntegerWrapper deserialize(@NotNull Integer serialized) { + return new IntegerWrapper(serialized); + } + + @Override + public Integer serialize(@NotNull IntegerWrapper deserialized) { + return deserialized.value(); + } + + @Override + public Class getDeserializedType() { + return IntegerWrapper.class; + } + + @Override + public Class getSerializedType() { + return Integer.class; + } +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java b/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java new file mode 100644 index 00000000..717843ff --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.mock.wrapper.longprimitive; + +public record LongWrapper(Long value) { +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java b/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java new file mode 100644 index 00000000..507ef959 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.mock.wrapper.longprimitive; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "custom_type_test_long") +public class LongWrapperDataClass extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "val", nullable = true) + public PersistentValue value; +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java b/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java new file mode 100644 index 00000000..fd31f57d --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.mock.wrapper.longprimitive; + +import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; + +public class LongWrapperValueSerializer implements ValueSerializer { + @Override + public LongWrapper deserialize(@NotNull Long serialized) { + return new LongWrapper(serialized); + } + + @Override + public Long serialize(@NotNull LongWrapper deserialized) { + return deserialized.value(); + } + + @Override + public Class getDeserializedType() { + return LongWrapper.class; + } + + @Override + public Class getSerializedType() { + return Long.class; + } +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java b/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java new file mode 100644 index 00000000..f6c3e403 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.mock.wrapper.stringprimitive; + +public record StringWrapper(String value) { +} diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java b/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java new file mode 100644 index 00000000..58a94446 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java @@ -0,0 +1,11 @@ +package net.staticstudios.data.mock.wrapper.stringprimitive; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "custom_type_test") +public class StringWrapperDataClass extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "val", nullable = true) + public PersistentValue value; +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java b/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java new file mode 100644 index 00000000..68a7a858 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.mock.wrapper.stringprimitive; + +import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; + +public class StringWrapperValueSerializer implements ValueSerializer { + @Override + public StringWrapper deserialize(@NotNull String serialized) { + return new StringWrapper(serialized); + } + + @Override + public String serialize(@NotNull StringWrapper deserialized) { + return deserialized.value(); + } + + @Override + public Class getDeserializedType() { + return StringWrapper.class; + } + + @Override + public Class getSerializedType() { + return String.class; + } +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java b/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java new file mode 100644 index 00000000..245d5ca1 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.mock.wrapper.timestampprimitive; + +import java.sql.Timestamp; + +public record TimestampWrapper(Timestamp value) { +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java b/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java new file mode 100644 index 00000000..846204fb --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.mock.wrapper.timestampprimitive; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "custom_type_test_timestamp") +public class TimestampWrapperDataClass extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "val", nullable = true) + public PersistentValue value; +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java b/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java new file mode 100644 index 00000000..a93a113c --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java @@ -0,0 +1,29 @@ +package net.staticstudios.data.mock.wrapper.timestampprimitive; + +import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; + +import java.sql.Timestamp; + +public class TimestampWrapperValueSerializer implements ValueSerializer { + @Override + public TimestampWrapper deserialize(@NotNull Timestamp serialized) { + return new TimestampWrapper(serialized); + } + + @Override + public Timestamp serialize(@NotNull TimestampWrapper deserialized) { + return deserialized.value(); + } + + @Override + public Class getDeserializedType() { + return TimestampWrapper.class; + } + + @Override + public Class getSerializedType() { + return Timestamp.class; + } +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java b/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java new file mode 100644 index 00000000..e4e56c40 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.mock.wrapper.uuidprimitive; + +import java.util.UUID; + +public record UUIDWrapper(UUID value) { +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java b/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java new file mode 100644 index 00000000..c9d33232 --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.mock.wrapper.uuidprimitive; + +import net.staticstudios.data.*; + +@Data(schema = "public", table = "custom_type_test_uuid") +public class UUIDWrapperDataClass extends UniqueData { + @IdColumn(name = "id") + public PersistentValue id; + @Column(name = "val", nullable = true) + public PersistentValue value; +} + diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java b/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java new file mode 100644 index 00000000..bc81bcaf --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java @@ -0,0 +1,29 @@ +package net.staticstudios.data.mock.wrapper.uuidprimitive; + +import net.staticstudios.data.ValueSerializer; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class UUIDWrapperValueSerializer implements ValueSerializer { + @Override + public UUIDWrapper deserialize(@NotNull UUID serialized) { + return new UUIDWrapper(serialized); + } + + @Override + public UUID serialize(@NotNull UUIDWrapper deserialized) { + return deserialized.value(); + } + + @Override + public Class getDeserializedType() { + return UUIDWrapper.class; + } + + @Override + public Class getSerializedType() { + return UUID.class; + } +} + From e1fb28bda472a203285946157959ca345a69b39d Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 23 Sep 2025 19:39:04 -0400 Subject: [PATCH 21/75] support numeric queries on timestamps --- .../java/net/staticstudios/data/processor/QueryFactory.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java index f3495255..37652f32 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java +++ b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java @@ -8,6 +8,7 @@ import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import java.io.IOException; +import java.sql.Timestamp; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -97,12 +98,13 @@ public void generateQueryBuilder() throws IOException { makeNotNullClause(builderType, persistentValueMetadata); } - if (TypeName.FLOAT.box().equals(persistentValueMetadata.genericType()) //todo: support timestampts and dates + if (TypeName.FLOAT.box().equals(persistentValueMetadata.genericType()) || TypeName.DOUBLE.box().equals(persistentValueMetadata.genericType()) || TypeName.LONG.box().equals(persistentValueMetadata.genericType()) || TypeName.SHORT.box().equals(persistentValueMetadata.genericType()) || TypeName.BYTE.box().equals(persistentValueMetadata.genericType()) || TypeName.INT.box().equals(persistentValueMetadata.genericType()) + || TypeName.get(Timestamp.class).equals(persistentValueMetadata.genericType()) ) { makeLessThanClause(builderType, persistentValueMetadata); makeLessThanOrEqualToClause(builderType, persistentValueMetadata); From 18323c472fbd66ae8a9c428e4b5c759ce28cd33d Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 30 Sep 2025 23:49:58 -0400 Subject: [PATCH 22/75] insert strategy, delete strategy, change fkeys again, and break changing id columns --- .../java/net/staticstudios/data/Column.java | 2 - .../net/staticstudios/data/DefaultValue.java | 12 ++ .../java/net/staticstudios/data/Delete.java | 12 ++ .../staticstudios/data/DeleteStrategy.java | 10 +- .../net/staticstudios/data/ForeignColumn.java | 6 - .../java/net/staticstudios/data/Insert.java | 12 ++ .../java/net/staticstudios/data/OneToOne.java | 4 - .../data/processor/DataProcessor.java | 23 ++- .../ForeignPersistentValueMetadata.java | 9 +- .../data/processor/MetadataUtils.java | 12 +- .../net/staticstudios/data/DataManager.java | 123 ++++++++---- .../staticstudios/data/PersistentValue.java | 2 - .../net/staticstudios/data/UniqueData.java | 24 +++ .../data/impl/data/PersistentValueImpl.java | 156 ++++++++-------- .../data/impl/h2/H2DataAccessor.java | 26 ++- .../H2DeleteStrategyCascadeTrigger.java | 106 +++++++++++ .../H2UpdateHandlerTrigger.java} | 8 +- .../data/insert/InsertContext.java | 23 ++- .../staticstudios/data/parse/ForeignKey.java | 38 ++-- .../staticstudios/data/parse/SQLBuilder.java | 78 ++++---- .../data/parse/SQLDeleteStrategyTrigger.java | 78 ++++++++ .../staticstudios/data/parse/SQLTable.java | 34 ++-- .../staticstudios/data/parse/SQLTrigger.java | 8 + .../util/ForeignPersistentValueMetadata.java | 4 +- .../staticstudios/data/util/SQlStatement.java | 16 +- .../staticstudios/data/CustomTypeTest.java | 2 +- .../data/PersistentValueTest.java | 176 +++++++++++++++++- .../net/staticstudios/data/ReferenceTest.java | 56 ++++++ .../net/staticstudios/data/SQLParseTest.java | 8 +- .../data/mock/post/MockPost.java | 6 +- .../data/mock/user/MockUser.java | 21 ++- .../data/mock/user/MockUserSettings.java | 3 +- 32 files changed, 846 insertions(+), 252 deletions(-) create mode 100644 annotations/src/main/java/net/staticstudios/data/DefaultValue.java create mode 100644 annotations/src/main/java/net/staticstudios/data/Delete.java create mode 100644 annotations/src/main/java/net/staticstudios/data/Insert.java create mode 100644 src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java rename src/main/java/net/staticstudios/data/impl/h2/{H2Trigger.java => trigger/H2UpdateHandlerTrigger.java} (93%) create mode 100644 src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java create mode 100644 src/main/java/net/staticstudios/data/parse/SQLTrigger.java diff --git a/annotations/src/main/java/net/staticstudios/data/Column.java b/annotations/src/main/java/net/staticstudios/data/Column.java index c9b1b71a..07c90c20 100644 --- a/annotations/src/main/java/net/staticstudios/data/Column.java +++ b/annotations/src/main/java/net/staticstudios/data/Column.java @@ -19,6 +19,4 @@ boolean nullable() default false; boolean unique() default false; - - String defaultValue() default ""; } diff --git a/annotations/src/main/java/net/staticstudios/data/DefaultValue.java b/annotations/src/main/java/net/staticstudios/data/DefaultValue.java new file mode 100644 index 00000000..41bd1693 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/DefaultValue.java @@ -0,0 +1,12 @@ +package net.staticstudios.data; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DefaultValue { + String value(); +} diff --git a/annotations/src/main/java/net/staticstudios/data/Delete.java b/annotations/src/main/java/net/staticstudios/data/Delete.java new file mode 100644 index 00000000..b7094618 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/Delete.java @@ -0,0 +1,12 @@ +package net.staticstudios.data; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Delete { + DeleteStrategy value(); +} diff --git a/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java index 50ece10e..9364bcd1 100644 --- a/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java +++ b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java @@ -2,15 +2,11 @@ public enum DeleteStrategy { /** - * When the parent holder is deleted, delete this data as well. + * When the parent data is deleted, delete this data as well. */ CASCADE, /** - * Do nothing when the parent holder is deleted. + * Do nothing when the parent data is deleted. */ - NO_ACTION, - /** - * This is only for use in PersistentCollections created via - */ - UNLINK //todo: this + NO_ACTION } diff --git a/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java index 14cfb4ab..0efb1acc 100644 --- a/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java +++ b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java @@ -18,11 +18,5 @@ boolean index() default false; - String defaultValue() default ""; - String link(); - - InsertStrategy insertStrategy() default InsertStrategy.OVERWRITE_EXISTING; - - DeleteStrategy deleteStrategy() default DeleteStrategy.NO_ACTION; } diff --git a/annotations/src/main/java/net/staticstudios/data/Insert.java b/annotations/src/main/java/net/staticstudios/data/Insert.java new file mode 100644 index 00000000..e8ae85cb --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/Insert.java @@ -0,0 +1,12 @@ +package net.staticstudios.data; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Insert { + InsertStrategy value(); +} diff --git a/annotations/src/main/java/net/staticstudios/data/OneToOne.java b/annotations/src/main/java/net/staticstudios/data/OneToOne.java index f4412ce4..4526df50 100644 --- a/annotations/src/main/java/net/staticstudios/data/OneToOne.java +++ b/annotations/src/main/java/net/staticstudios/data/OneToOne.java @@ -16,8 +16,4 @@ * @return The link format */ String link(); - - DeleteStrategy deleteStrategy() default DeleteStrategy.NO_ACTION; - - UpdateStrategy updateStrategy() default UpdateStrategy.CASCADE; } diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index 49ef76b3..dfbdc99b 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -2,6 +2,7 @@ import com.palantir.javapoet.*; import net.staticstudios.data.Data; +import net.staticstudios.data.InsertStrategy; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; @@ -91,18 +92,24 @@ private void generateFactory(TypeElement entityType, Data dataAnnotation, List links; + private final InsertStrategy insertStrategy; - public ForeignPersistentValueMetadata(String schema, String table, String column, String fieldName, TypeName genericType, boolean nullable, Map links) { + public ForeignPersistentValueMetadata(String schema, String table, String column, String fieldName, TypeName genericType, boolean nullable, Map links, InsertStrategy insertStrategy) { super(schema, table, column, fieldName, genericType, nullable); this.links = links; + this.insertStrategy = insertStrategy; } public Map links() { return links; } + + public InsertStrategy insertStrategy() { + return insertStrategy; + } } diff --git a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java index fc4b813b..08e8bf20 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java +++ b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java @@ -4,10 +4,7 @@ import com.palantir.javapoet.FieldSpec; import com.palantir.javapoet.TypeName; import com.palantir.javapoet.TypeSpec; -import net.staticstudios.data.Column; -import net.staticstudios.data.Data; -import net.staticstudios.data.ForeignColumn; -import net.staticstudios.data.IdColumn; +import net.staticstudios.data.*; import net.staticstudios.data.utils.StringUtils; import javax.lang.model.element.Modifier; @@ -71,6 +68,7 @@ private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnot IdColumn idColumn = field.getAnnotation(IdColumn.class); Column column = field.getAnnotation(Column.class); ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); + Insert insert = field.getAnnotation(Insert.class); if (idColumn != null) { tableName = dataAnnotation.table(); @@ -107,6 +105,9 @@ private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnot } links.put(parts[0].trim(), parts[1].trim()); } + + InsertStrategy insertStrategy = insert != null ? insert.value() : InsertStrategy.PREFER_EXISTING; + return new ForeignPersistentValueMetadata( schemaName, tableName, @@ -114,7 +115,8 @@ private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnot field.getSimpleName().toString(), typeName, nullable, - links + links, + insertStrategy ); } throw new IllegalStateException("Field " + field.getSimpleName() + " is not annotated with @IdColumn, @Column, or @ForeignColumn"); diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 49d2e594..4e9efb58 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -219,7 +219,7 @@ public UniqueDataMetadata getMetadata(Class clazz) { } @ApiStatus.Internal - public void delete(List columnNames, String schema, String table, Object[] values) { + public void handleDelete(List columnNames, String schema, String table, Object[] values) { uniqueDataMetadataMap.values().forEach(uniqueDataMetadata -> { if (!uniqueDataMetadata.schema().equals(schema) || !uniqueDataMetadata.table().equals(table)) { return; @@ -448,20 +448,20 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { // handle foreign keys. for each foreign key, if we have a value for the local column, we need to set the value for the foreign column. this consists of linking ids mostly. for (SQLTable table : tables) { - for (ForeignKey fKey : table.getForeignKeysThatReferenceThisTable()) { - SQLSchema otherSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getSchema())); - SQLTable otherTable = Objects.requireNonNull(otherSchema.getTable(fKey.getTable())); + for (ForeignKey fKey : table.getForeignKeys()) { + SQLSchema referencedSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getReferencedSchema())); + SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(fKey.getReferencedTable())); for (ForeignKey.Link link : fKey.getLinkingColumns()) { - String myColumnName = link.columnInReferencedTable(); - String otherColumnName = link.columnInReferringTable(); - SQLColumn otherColumn = Objects.requireNonNull(otherTable.getColumn(otherColumnName)); + String myColumnName = link.columnInReferringTable(); + String otherColumnName = link.columnInReferencedTable(); + SQLColumn otherColumn = Objects.requireNonNull(referencedTable.getColumn(otherColumnName)); // if its nullable and we don't have a value, skip it. if (otherColumn.isNullable()) { continue; } - insertContext.set(fKey.getSchema(), fKey.getTable(), otherColumn.getName(), insertContext.getEntries().get(new SimpleColumnMetadata(table.getSchema().getName(), table.getName(), myColumnName, otherColumn.getType()))); + insertContext.set(fKey.getReferencedSchema(), fKey.getReferencedTable(), otherColumn.getName(), insertContext.getEntries().get(new SimpleColumnMetadata(fKey.getReferringSchema(), fKey.getReferringTable(), myColumnName, otherColumn.getType())), InsertStrategy.PREFER_EXISTING); } } } @@ -472,15 +472,13 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { tables.add(table); }); - //sort tables based on foreign key dependencies. tables who are depended on should come last - // Build dependency graph: table -> set of tables it depends on Map> dependencyGraph = new HashMap<>(); for (SQLTable table : tables) { Set dependsOn = new HashSet<>(); - for (ForeignKey fKey : table.getForeignKeysThatReferenceThisTable()) { - SQLSchema otherSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getSchema())); - SQLTable otherTable = Objects.requireNonNull(otherSchema.getTable(fKey.getTable())); + for (ForeignKey fKey : table.getForeignKeys()) { + SQLSchema referencedSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getReferencedSchema())); + SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(fKey.getReferencedTable())); boolean addDependency = true; // if one of the linking columns is not present in the insert context, we can't add the dependency @@ -488,9 +486,9 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { Object value = insertContext.getEntries().entrySet().stream() .filter(entry -> { SimpleColumnMetadata key = entry.getKey(); - return key.schema().equals(fKey.getSchema()) && - key.table().equals(fKey.getTable()) && - key.name().equals(link.columnInReferringTable()); + return key.schema().equals(fKey.getReferencedSchema()) && + key.table().equals(fKey.getReferencedTable()) && + key.name().equals(link.columnInReferencedTable()); }) .findFirst() .orElse(null); @@ -502,7 +500,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { } if (addDependency) { - dependsOn.add(otherTable); + dependsOn.add(referencedTable); } } if (!dependsOn.isEmpty()) { @@ -525,7 +523,6 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { for (SQLTable table : tables) { topoSort(table, dependencyGraph, visited, orderedTables); } - Collections.reverse(orderedTables); List sqlStatements = new ArrayList<>(); @@ -542,25 +539,89 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { List columnsInTable = columnsToInsert.get(tableName); - StringBuilder sqlBuilder = new StringBuilder("INSERT INTO \""); - sqlBuilder.append(schemaName).append("\".\"").append(tableName).append("\" ("); + StringBuilder h2SqlBuilder = new StringBuilder("MERGE INTO \""); + h2SqlBuilder.append(schemaName).append("\".\"").append(tableName).append("\" AS target USING (VALUES ("); + Map conflicts = new HashMap<>(); + h2SqlBuilder.append("?, ".repeat(columnsInTable.size())); + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); + h2SqlBuilder.append(")) AS source ("); for (SimpleColumnMetadata column : columnsInTable) { - sqlBuilder.append("\"").append(column.name()).append("\", "); + h2SqlBuilder.append("\"").append(column.name()).append("\", "); + InsertStrategy strategy = insertContext.getInsertStrategies().get(column); + if (strategy != null) { + conflicts.put(column, strategy); + } + } + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); + h2SqlBuilder.append(") ON "); + for (ColumnMetadata idColumn : table.getIdColumns()) { + h2SqlBuilder.append("target.\"").append(idColumn.name()).append("\" = source.\"").append(idColumn.name()).append("\" AND "); + } + h2SqlBuilder.setLength(h2SqlBuilder.length() - 5); + h2SqlBuilder.append(" WHEN NOT MATCHED THEN INSERT ("); + for (SimpleColumnMetadata column : columnsInTable) { + h2SqlBuilder.append("\"").append(column.name()).append("\", "); + } + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); + h2SqlBuilder.append(") VALUES ("); + for (SimpleColumnMetadata column : columnsInTable) { + h2SqlBuilder.append("source.\"").append(column.name()).append("\", "); + } + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); + h2SqlBuilder.append(")"); + + List overwriteExisting = new ArrayList<>(); + for (Map.Entry entry : conflicts.entrySet()) { + if (entry.getValue() == InsertStrategy.OVERWRITE_EXISTING) { + overwriteExisting.add(entry.getKey()); + } + } + if (!overwriteExisting.isEmpty()) { + h2SqlBuilder.append(" WHEN MATCHED THEN UPDATE SET "); + for (SimpleColumnMetadata column : overwriteExisting) { + h2SqlBuilder.append("\"").append(column.name()).append("\" = source.\"").append(column.name()).append("\", "); + } + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(") VALUES ("); - sqlBuilder.append("?, ".repeat(columnsInTable.size())); - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(")"); - String sql = sqlBuilder.toString(); + StringBuilder pgSqlBuilder = new StringBuilder("INSERT INTO \""); + pgSqlBuilder.append(schemaName).append("\".\"").append(tableName).append("\" ("); + for (SimpleColumnMetadata column : columnsInTable) { + pgSqlBuilder.append("\"").append(column.name()).append("\", "); + } + pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); + pgSqlBuilder.append(") VALUES ("); + pgSqlBuilder.append("?, ".repeat(columnsInTable.size())); + pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); + pgSqlBuilder.append(")"); + + if (!conflicts.isEmpty()) { + pgSqlBuilder.append(" ON CONFLICT ("); + for (ColumnMetadata idColumn : table.getIdColumns()) { + pgSqlBuilder.append("\"").append(idColumn.name()).append("\", "); + } + pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); + pgSqlBuilder.append(") DO "); + if (!overwriteExisting.isEmpty()) { + pgSqlBuilder.append("UPDATE SET "); + for (SimpleColumnMetadata column : overwriteExisting) { + pgSqlBuilder.append("\"").append(column.name()).append("\" = EXCLUDED.\"").append(column.name()).append("\", "); + } + pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); + } else { + pgSqlBuilder.append("NOTHING"); + } + } + + String h2Sql = h2SqlBuilder.toString(); + String pgSql = pgSqlBuilder.toString(); List values = new ArrayList<>(); for (SimpleColumnMetadata column : columnsInTable) { Object deserializedValue = insertContext.getEntries().get(column); Object serializedValue = serialize(deserializedValue); values.add(serializedValue); } - sqlStatements.add(new SQlStatement(sql, values)); + sqlStatements.add(new SQlStatement(h2Sql, pgSql, values)); } try { @@ -605,12 +666,6 @@ public void set(String schema, String table, String column, ColumnValuePairs idC sqlBuilder = new StringBuilder().append("UPDATE \"").append(schema).append("\".\"").append(table).append("\" SET \"").append(column).append("\" = ? WHERE "); for (ColumnValuePair columnValuePair : idColumns) { String name = columnValuePair.column(); - for (ForeignKey.Link link : idColumnLinks) { - if (link.columnInReferringTable().equals(columnValuePair.column())) { - name = link.columnInReferencedTable(); - break; - } - } sqlBuilder.append("\"").append(name).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); diff --git a/src/main/java/net/staticstudios/data/PersistentValue.java b/src/main/java/net/staticstudios/data/PersistentValue.java index 5afd9cd0..27cd54c6 100644 --- a/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/src/main/java/net/staticstudios/data/PersistentValue.java @@ -18,8 +18,6 @@ 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 - //todo: insert strategy, deletion strategy - static PersistentValue of(UniqueData holder, Class dataType) { return new ProxyPersistentValue<>(holder, dataType); } diff --git a/src/main/java/net/staticstudios/data/UniqueData.java b/src/main/java/net/staticstudios/data/UniqueData.java index 300166c5..6f6171e0 100644 --- a/src/main/java/net/staticstudios/data/UniqueData.java +++ b/src/main/java/net/staticstudios/data/UniqueData.java @@ -1,10 +1,15 @@ package net.staticstudios.data; +import com.google.common.base.Preconditions; import net.staticstudios.data.util.ColumnValuePair; import net.staticstudios.data.util.ColumnValuePairs; import net.staticstudios.data.util.UniqueDataMetadata; import org.jetbrains.annotations.ApiStatus; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + public abstract class UniqueData { private ColumnValuePairs idColumns; private DataManager dataManager; @@ -40,6 +45,25 @@ public final UniqueDataMetadata getMetadata() { return dataManager.getMetadata(this.getClass()); } + public synchronized void delete() { + Preconditions.checkState(!isDeleted, "This object has already been deleted!"); + UniqueDataMetadata metadata = getMetadata(); + + StringBuilder sql = new StringBuilder("DELETE FROM \"" + metadata.schema() + "\".\"" + metadata.table() + "\" WHERE "); + List values = new ArrayList<>(); + for (ColumnValuePair idColumn : idColumns) { + sql.append("\"").append(idColumn.column()).append("\" = ? AND "); + values.add(idColumn.value()); + } + sql.setLength(sql.length() - 5); + + try { + dataManager.getDataAccessor().executeUpdate(sql.toString(), values, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index 7c7b33b6..b5290ee6 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -12,56 +12,38 @@ public class PersistentValueImpl implements PersistentValue { private final UniqueData holder; private final Class dataType; - private final String schema; - private final String table; - private final String column; - private final int updateInterval; - private final List idColumnLinks; + private final PersistentValueMetadata metadata; - private PersistentValueImpl(UniqueData holder, Class dataType, String schema, String table, String column, int updateInterval, List idColumnLinks) { + private PersistentValueImpl(UniqueData holder, Class dataType, PersistentValueMetadata metadata) { this.holder = holder; this.dataType = dataType; - this.schema = schema; - this.table = table; - this.column = column; - this.updateInterval = updateInterval; - this.idColumnLinks = idColumnLinks; + this.metadata = metadata; } public static void createAndDelegate(ProxyPersistentValue proxy, PersistentValueMetadata metadata) { - ColumnMetadata columnMetadata = metadata.getColumnMetadata(); PersistentValueImpl delegate = new PersistentValueImpl<>( proxy.getHolder(), proxy.getDataType(), - columnMetadata.schema(), - columnMetadata.table(), - columnMetadata.name(), - metadata.getUpdateInterval(), - Collections.emptyList() + metadata ); proxy.setDelegate(metadata, delegate); } - public static PersistentValueImpl create(UniqueData holder, Class dataType, String schema, String table, String column, int updateInterval, List idColumnLinks) { - return new PersistentValueImpl<>(holder, dataType, schema, table, column, updateInterval, idColumnLinks); + public static PersistentValueImpl create(UniqueData holder, Class dataType, PersistentValueMetadata metadata) { + return new PersistentValueImpl<>(holder, dataType, metadata); } 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()); - ColumnMetadata columnMetadata = pvMetadata.getColumnMetadata(); if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { PersistentValueImpl.createAndDelegate(proxyPv, pvMetadata); } else { pair.field().setAccessible(true); try { - List idColumnLinks = Collections.emptyList(); - if (pvMetadata instanceof ForeignPersistentValueMetadata foreignPvMetadata) { - idColumnLinks = foreignPvMetadata.getLinks(); - } - pair.field().set(instance, PersistentValueImpl.create(instance, ReflectionUtils.getGenericType(pair.field()), columnMetadata.schema(), columnMetadata.table(), columnMetadata.name(), pvMetadata.getUpdateInterval(), idColumnLinks)); + pair.field().set(instance, PersistentValueImpl.create(instance, ReflectionUtils.getGenericType(pair.field()), pvMetadata)); } catch (IllegalAccessException e) { throw new RuntimeException(e); } @@ -72,65 +54,68 @@ public static void delegate(T instance) { public static Map extractMetadata(String schema, String table, Class clazz) { Map metadataMap = new HashMap<>(); for (Field field : ReflectionUtils.getFields(clazz, PersistentValue.class)) { - IdColumn idColumn = field.getAnnotation(IdColumn.class); - Column columnAnnotation = field.getAnnotation(Column.class); - ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); - UpdateInterval updateIntervalAnnotation = field.getAnnotation(UpdateInterval.class); - int updateInterval = updateIntervalAnnotation != null ? updateIntervalAnnotation.value() : 0; - if (idColumn != null) { - Preconditions.checkArgument(columnAnnotation == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @Column", field.getName()); - Preconditions.checkArgument(foreignColumn == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @ForeignColumn", field.getName()); - ColumnMetadata columnMetadata = new ColumnMetadata( - schema, - table, - ValueUtils.parseValue(idColumn.name()), - ReflectionUtils.getGenericType(field), - false, - false, - "" - ); - metadataMap.put(field, new PersistentValueMetadata(clazz, columnMetadata, updateInterval)); - continue; - } - if (columnAnnotation != null) { - ColumnMetadata columnMetadata = new ColumnMetadata( - columnAnnotation.schema().isEmpty() ? schema : ValueUtils.parseValue(columnAnnotation.schema()), - columnAnnotation.table().isEmpty() ? table : ValueUtils.parseValue(columnAnnotation.table()), - ValueUtils.parseValue(columnAnnotation.name()), - ReflectionUtils.getGenericType(field), - columnAnnotation.nullable(), - columnAnnotation.index(), - columnAnnotation.defaultValue() - ); - metadataMap.put(field, new PersistentValueMetadata(clazz, columnMetadata, updateInterval)); - continue; - } - if (foreignColumn != null) { - ColumnMetadata columnMetadata = new ColumnMetadata( - foreignColumn.schema().isEmpty() ? schema : ValueUtils.parseValue(foreignColumn.schema()), - foreignColumn.table().isEmpty() ? table : ValueUtils.parseValue(foreignColumn.table()), - ValueUtils.parseValue(foreignColumn.name()), - ReflectionUtils.getGenericType(field), - foreignColumn.nullable(), - foreignColumn.index(), - foreignColumn.defaultValue() - ); - List idColumnLinks = new LinkedList<>(); - List links = StringUtils.parseCommaSeperatedList(foreignColumn.link()); - for (String link : links) { - String[] parts = link.split("="); - Preconditions.checkArgument(parts.length == 2, "ForeignColumn link must be in the format localColumn=foreignColumn, got: %s", link); - idColumnLinks.add(new ForeignKey.Link(ValueUtils.parseValue(parts[1]), ValueUtils.parseValue(parts[0]))); - } - metadataMap.put(field, new ForeignPersistentValueMetadata(clazz, columnMetadata, updateInterval, idColumnLinks)); - continue; - } - - throw new IllegalStateException("PersistentValue field %s is missing @Column annotation".formatted(field.getName())); + metadataMap.put(field, extractMetadata(schema, table, clazz, field)); } return metadataMap; } + public static PersistentValueMetadata extractMetadata(String schema, String table, Class clazz, Field field) { + IdColumn idColumn = field.getAnnotation(IdColumn.class); + Column columnAnnotation = field.getAnnotation(Column.class); + ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); + UpdateInterval updateIntervalAnnotation = field.getAnnotation(UpdateInterval.class); + DefaultValue defaultValueAnnotation = field.getAnnotation(DefaultValue.class); + String defaultValue = defaultValueAnnotation != null ? defaultValueAnnotation.value() : ""; + int updateInterval = updateIntervalAnnotation != null ? updateIntervalAnnotation.value() : 0; + if (idColumn != null) { + Preconditions.checkArgument(columnAnnotation == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @Column", field.getName()); + Preconditions.checkArgument(foreignColumn == null, "PersistentValue field %s cannot be annotated with both @IdColumn and @ForeignColumn", field.getName()); + ColumnMetadata columnMetadata = new ColumnMetadata( + schema, + table, + ValueUtils.parseValue(idColumn.name()), + ReflectionUtils.getGenericType(field), + false, + false, + "" + ); + return new PersistentValueMetadata(clazz, columnMetadata, updateInterval); + } + if (columnAnnotation != null) { + ColumnMetadata columnMetadata = new ColumnMetadata( + columnAnnotation.schema().isEmpty() ? schema : ValueUtils.parseValue(columnAnnotation.schema()), + columnAnnotation.table().isEmpty() ? table : ValueUtils.parseValue(columnAnnotation.table()), + ValueUtils.parseValue(columnAnnotation.name()), + ReflectionUtils.getGenericType(field), + columnAnnotation.nullable(), + columnAnnotation.index(), + defaultValue + ); + return new PersistentValueMetadata(clazz, columnMetadata, updateInterval); + } + if (foreignColumn != null) { + ColumnMetadata columnMetadata = new ColumnMetadata( + foreignColumn.schema().isEmpty() ? schema : ValueUtils.parseValue(foreignColumn.schema()), + foreignColumn.table().isEmpty() ? table : ValueUtils.parseValue(foreignColumn.table()), + ValueUtils.parseValue(foreignColumn.name()), + ReflectionUtils.getGenericType(field), + foreignColumn.nullable(), + foreignColumn.index(), + defaultValue + ); + List idColumnLinks = new LinkedList<>(); + List links = StringUtils.parseCommaSeperatedList(foreignColumn.link()); + for (String link : links) { + String[] parts = link.split("="); + Preconditions.checkArgument(parts.length == 2, "ForeignColumn link must be in the format localColumn=foreignColumn, got: %s", link); + idColumnLinks.add(new ForeignKey.Link(ValueUtils.parseValue(parts[1]), ValueUtils.parseValue(parts[0]))); + } + return new ForeignPersistentValueMetadata(clazz, columnMetadata, updateInterval, idColumnLinks); + } + + throw new IllegalStateException("PersistentValue field %s is missing @Column annotation".formatted(field.getName())); + } + @Override public UniqueData getHolder() { return holder; @@ -149,12 +134,19 @@ public PersistentValue onUpdate(Class holderClass, @Override public T get() { Preconditions.checkArgument(!holder.isDeleted(), "Cannot get value from a deleted UniqueData instance"); - return holder.getDataManager().get(schema, table, column, holder.getIdColumns(), idColumnLinks, dataType); + return holder.getDataManager().get(metadata.getSchema(), metadata.getTable(), metadata.getColumn(), holder.getIdColumns(), getIdColumnLinks(), dataType); } @Override public void set(T value) { Preconditions.checkArgument(!holder.isDeleted(), "Cannot set value on a deleted UniqueData instance"); - holder.getDataManager().set(schema, table, column, holder.getIdColumns(), idColumnLinks, value, updateInterval); + holder.getDataManager().set(metadata.getSchema(), metadata.getTable(), metadata.getColumn(), holder.getIdColumns(), getIdColumnLinks(), value, metadata.getUpdateInterval()); + } + + private List getIdColumnLinks() { + if (metadata instanceof ForeignPersistentValueMetadata foreignMetadata) { + return foreignMetadata.getLinks(); + } + return Collections.emptyList(); } } diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index 03b4358e..c672a59d 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -5,6 +5,7 @@ import net.staticstudios.data.DataAccessor; import net.staticstudios.data.DataManager; import net.staticstudios.data.InsertMode; +import net.staticstudios.data.impl.h2.trigger.H2UpdateHandlerTrigger; import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.parse.DDLStatement; import net.staticstudios.data.parse.SQLColumn; @@ -208,6 +209,8 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener } public synchronized void sync(List schemaTables) throws SQLException { + //todo: i would ideally like to support periodic resyncing of data. even if this means we pause everything until then. not exactly sure how this would look tho. + //todo: when we resync we should clear the task queue and steal the connection //todo: if possible, id like to pause everything else until we are done syncing dataManager.submitBlockingTask(realDbConnection -> { @@ -322,12 +325,12 @@ public void insert(List sqlStatements, InsertMode insertMode) thro connection.setAutoCommit(false); for (SQlStatement sqlStatement : sqlStatements) { - try (PreparedStatement preparedStatement = connection.prepareStatement(sqlStatement.getSql())) { + try (PreparedStatement preparedStatement = connection.prepareStatement(sqlStatement.getH2Sql())) { int i = 1; for (Object value : sqlStatement.getValues()) { preparedStatement.setObject(i++, value); } - logger.debug("[H2] {}", sqlStatement.getSql()); + logger.debug("[H2] {}", sqlStatement.getH2Sql()); preparedStatement.executeUpdate(); } } @@ -337,12 +340,13 @@ public void insert(List sqlStatements, InsertMode insertMode) thro realConnection.setAutoCommit(false); try { for (SQlStatement statement : sqlStatements) { - try (PreparedStatement preparedStatement = realConnection.prepareStatement(statement.getSql())) { + try (PreparedStatement preparedStatement = realConnection.prepareStatement(statement.getPgSql())) { List values = statement.getValues(); for (int i = 0; i < values.size(); i++) { - preparedStatement.setObject(i + 1, values.get(i)); + Object value = values.get(i); + preparedStatement.setObject(i + 1, value); } - logger.debug("[DB] {}", statement.getSql()); + logger.debug("[DB] {}", statement.getPgSql()); preparedStatement.executeUpdate(); } } @@ -400,8 +404,12 @@ public void executeUpdate(@Language("SQL") String sql, List values, int @Override public void runDDL(DDLStatement ddl) { taskQueue.submitTask(connection -> { - logger.debug("[DB] {}", ddl.postgresqlStatement()); - connection.createStatement().execute(ddl.postgresqlStatement()); + if (!ddl.postgresqlStatement().isEmpty()) { + + logger.debug("[DB] {}", ddl.postgresqlStatement()); + connection.createStatement().execute(ddl.postgresqlStatement()); + } + if (ddl.h2Statement().isEmpty()) return; try (Statement statement = getConnection().createStatement()) { logger.trace("[H2] {}", ddl.h2Statement()); statement.execute(ddl.h2Statement()); @@ -433,9 +441,9 @@ private synchronized void updateKnownTables() throws SQLException { @Language("SQL") String sql = "CREATE TRIGGER IF NOT EXISTS \"trg_%s_%s\" AFTER INSERT, UPDATE, DELETE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; try (Statement createTrigger = connection.createStatement()) { - String formatted = sql.formatted(table, randomId.toString().replace('-', '_'), schema, table, H2Trigger.class.getName()); + String formatted = sql.formatted(table, randomId.toString().replace('-', '_'), schema, table, H2UpdateHandlerTrigger.class.getName()); logger.trace("[H2] {}", formatted); - H2Trigger.registerDataManager(randomId, dataManager); + H2UpdateHandlerTrigger.registerDataManager(randomId, dataManager); createTrigger.execute(formatted); } diff --git a/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java b/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java new file mode 100644 index 00000000..ced12049 --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java @@ -0,0 +1,106 @@ +package net.staticstudios.data.impl.h2.trigger; + +import net.staticstudios.data.parse.ForeignKey; +import org.h2.api.Trigger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class H2DeleteStrategyCascadeTrigger implements Trigger { + private final Logger logger = LoggerFactory.getLogger(H2DeleteStrategyCascadeTrigger.class); + private final List columnNames = new ArrayList<>(); + private List links; + private String parentSchema; + private String parentTable; + private String targetSchema; + private String targetTable; + + @Override + public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException { + String[] parts; + String data = triggerName.split("static_data_v3_")[1]; + parts = data.split("_", 2); + int parentSchemaLength = Integer.parseInt(parts[0]); + this.parentSchema = parts[1].substring(0, parentSchemaLength); + data = parts[1].substring(parentSchemaLength + 1); + parts = data.split("_", 2); + int parentTableLength = Integer.parseInt(parts[0]); + this.parentTable = parts[1].substring(0, parentTableLength); + data = parts[1].substring(parentTableLength + 1); + parts = data.split("_", 2); + int targetSchemaLength = Integer.parseInt(parts[0]); + this.targetSchema = parts[1].substring(0, targetSchemaLength); + data = parts[1].substring(targetSchemaLength + 1); + parts = data.split("_", 2); + int targetTableLength = Integer.parseInt(parts[0]); + this.targetTable = parts[1].substring(0, targetTableLength); + data = parts[1].substring(targetTableLength); + + String encodedLinks = data.split("__delete_links__")[1]; + int linkCount = Integer.parseInt(encodedLinks.split("_", 2)[0]); + encodedLinks = encodedLinks.split("_", 2)[1]; + List links = new ArrayList<>(); + + while (links.size() < linkCount) { + String[] encodedParts = encodedLinks.split("_", 2); + int length = Integer.parseInt(encodedParts[0]); + String link = encodedParts[1].substring(0, length); + links.add(link); + encodedLinks = encodedParts[1].substring(length); + } + this.links = new ArrayList<>(); + for (int i = 0; i < links.size(); i += 2) { + this.links.add(new ForeignKey.Link(links.get(i + 1), links.get(i))); + } + } + + @Override + public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException { + 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, parentSchema); + ps.setString(2, parentTable); // H2 stores names in uppercase by default + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + columns.add(rs.getString("COLUMN_NAME")); + } + } + } + columnNames.clear(); + columnNames.addAll(columns); + } + if (newRow == null && oldRow != null) { + handleDelete(connection, oldRow); + } + } + + private void handleDelete(Connection connection, Object[] oldRow) throws SQLException { + StringBuilder sb = new StringBuilder("DELETE FROM \"").append(targetSchema).append("\".\"").append(targetTable).append("\" WHERE "); + List values = new ArrayList<>(); + for (ForeignKey.Link link : links) { + sb.append("\"").append(link.columnInReferencedTable()).append("\" = ? AND "); + int index = columnNames.indexOf(link.columnInReferringTable()); + values.add(oldRow[index]); + } + sb.setLength(sb.length() - 5); + sb.append(";"); + logger.debug("Executing cascade delete: {}", sb); + + try (PreparedStatement ps = connection.prepareStatement(sb.toString())) { + for (int i = 0; i < values.size(); i++) { + ps.setObject(i + 1, values.get(i)); + } + ps.executeUpdate(); + } + } +} diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java b/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java similarity index 93% rename from src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java rename to src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java index cd3d92ac..314f0b60 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2Trigger.java +++ b/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.impl.h2; +package net.staticstudios.data.impl.h2.trigger; import net.staticstudios.data.DataManager; import org.h2.api.Trigger; @@ -12,9 +12,9 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; -public class H2Trigger implements Trigger { +public class H2UpdateHandlerTrigger implements Trigger { private static final Map dataManagerMap = new ConcurrentHashMap<>(); - private final Logger logger = LoggerFactory.getLogger(H2Trigger.class); + private final Logger logger = LoggerFactory.getLogger(H2UpdateHandlerTrigger.class); private final List columnNames = new ArrayList<>(); private DataManager dataManager; private String schema; @@ -90,6 +90,6 @@ private void handleUpdate(Object[] oldRow, Object[] newRow) { } private void handleDelete(Object[] oldRow) { - dataManager.delete(columnNames, schema, table, oldRow); + dataManager.handleDelete(columnNames, schema, table, oldRow); } } diff --git a/src/main/java/net/staticstudios/data/insert/InsertContext.java b/src/main/java/net/staticstudios/data/insert/InsertContext.java index 170fe2a7..0a1e1723 100644 --- a/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.DataManager; import net.staticstudios.data.InsertMode; +import net.staticstudios.data.InsertStrategy; import net.staticstudios.data.UniqueData; import net.staticstudios.data.parse.SQLColumn; import net.staticstudios.data.parse.SQLSchema; @@ -17,23 +18,17 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; -public class InsertContext { //todo: insert strategy, on a per pv level. +public class InsertContext { private final AtomicBoolean inserted = new AtomicBoolean(false); private final DataManager dataManager; private final Map entries = new HashMap<>(); + private final Map insertStrategies = new HashMap<>(); public InsertContext(DataManager dataManager) { this.dataManager = dataManager; } - public InsertContext set(Class holderClass, String column, Object value) { - UniqueDataMetadata metadata = dataManager.getMetadata(holderClass); - Preconditions.checkNotNull(metadata, "Metadata not found for class: " + holderClass.getName()); - set(metadata.schema(), metadata.table(), column, value); - return this; - } - - public InsertContext set(String schema, String table, String column, @Nullable Object value) { + public InsertContext set(String schema, String table, String column, @Nullable Object value, @Nullable InsertStrategy insertStrategy) { Preconditions.checkState(!inserted.get(), "Cannot modify InsertContext after it has been inserted"); SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(schema); Preconditions.checkNotNull(sqlSchema, "Schema not found: " + schema); @@ -48,11 +43,17 @@ public InsertContext set(String schema, String table, String column, @Nullable O column, sqlColumn.getType() ); + if (value == null) { entries.remove(columnMetadata); + insertStrategies.remove(columnMetadata); return this; } + if (insertStrategy != null) { + insertStrategies.put(columnMetadata, insertStrategy); + } + Preconditions.checkArgument(sqlColumn.getType().isAssignableFrom(dataManager.getSerializedType(value.getClass())), "Value type mismatch for name " + column + " in table " + table + " schema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); entries.put(columnMetadata, dataManager.serialize(value)); @@ -63,6 +64,10 @@ public Map getEntries() { return entries; } + public Map getInsertStrategies() { + return insertStrategies; + } + public void markInserted() { inserted.set(true); } diff --git a/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/src/main/java/net/staticstudios/data/parse/ForeignKey.java index 74037357..82961a10 100644 --- a/src/main/java/net/staticstudios/data/parse/ForeignKey.java +++ b/src/main/java/net/staticstudios/data/parse/ForeignKey.java @@ -10,14 +10,18 @@ public class ForeignKey { private final Set links = new LinkedHashSet<>(); - private final String schema; - private final String table; + private final String referencedSchema; + private final String referencedTable; + private final String referringSchema; + private final String referringTable; private final OnDelete onDelete; private final OnUpdate onUpdate; - public ForeignKey(String schema, String table, OnDelete onDelete, OnUpdate onUpdate) { - this.schema = schema; - this.table = table; + public ForeignKey(String referringSchema, String referringTable, String referencedSchema, String referencedTable, OnDelete onDelete, OnUpdate onUpdate) { + this.referringSchema = referringSchema; + this.referringTable = referringTable; + this.referencedSchema = referencedSchema; + this.referencedTable = referencedTable; this.onDelete = onDelete; this.onUpdate = onUpdate; } @@ -30,12 +34,20 @@ public Set getLinkingColumns() { return Collections.unmodifiableSet(links); } - public String getSchema() { - return schema; + public String getReferencedSchema() { + return referencedSchema; } - public String getTable() { - return table; + public String getReferencedTable() { + return referencedTable; + } + + public String getReferringSchema() { + return referringSchema; + } + + public String getReferringTable() { + return referringTable; } public OnDelete getOnDelete() { @@ -53,13 +65,15 @@ public boolean equals(Object o) { return Objects.equals(onDelete, that.onDelete) && Objects.equals(onUpdate, that.onUpdate) && Objects.equals(links, that.links) && - Objects.equals(schema, that.schema) && - Objects.equals(table, that.table); + Objects.equals(referencedSchema, that.referencedSchema) && + Objects.equals(referencedTable, that.referencedTable) && + Objects.equals(referringSchema, that.referringSchema) && + Objects.equals(referringTable, that.referringTable); } @Override public int hashCode() { - return Objects.hash(links, schema, table, onDelete, onUpdate); + return Objects.hash(links, referencedSchema, referencedTable, onDelete, onUpdate, referringSchema, referringTable); } public record Link(String columnInReferencedTable, String columnInReferringTable) { diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index da397f58..e8f0a4fa 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -134,17 +134,17 @@ private List getDefs(Collection schemas) { } // define fkeys after table creation, to ensure all tables exist before adding fkeys - for (SQLSchema schema : schemas) { + for (SQLSchema schema : schemas) { //todo: what if an fkey's insert/delete strategy has changed? for (SQLTable table : schema.getTables()) { - for (ForeignKey foreignKey : table.getForeignKeysThatReferenceThisTable()) { + for (ForeignKey foreignKey : table.getForeignKeys()) { if (foreignKey == null) { continue; } - String fKeyName = "fk_" + foreignKey.getSchema() + "_" + foreignKey.getTable() + "_" + String fKeyName = "fk_" + foreignKey.getReferringSchema() + "_" + foreignKey.getReferringTable() + "_" + String.join("_", foreignKey.getLinkingColumns().stream().map(ForeignKey.Link::columnInReferringTable).toList()) - + "_to_" + table.getName() + "_" + String.join("_", foreignKey.getLinkingColumns().stream().map(ForeignKey.Link::columnInReferencedTable).toList()); + + "_to_" + foreignKey.getReferencedSchema() + "_" + foreignKey.getReferencedTable() + "_" + String.join("_", foreignKey.getLinkingColumns().stream().map(ForeignKey.Link::columnInReferencedTable).toList()); StringBuilder sb = new StringBuilder(); - sb.append("ALTER TABLE \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" "); + sb.append("ALTER TABLE \"").append(foreignKey.getReferringSchema()).append("\".\"").append(foreignKey.getReferringTable()).append("\" "); sb.append("ADD CONSTRAINT IF NOT EXISTS ").append(fKeyName).append(" "); sb.append("FOREIGN KEY ("); for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { @@ -152,7 +152,7 @@ private List getDefs(Collection schemas) { } sb.setLength(sb.length() - 2); sb.append(") "); - sb.append("REFERENCES \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ("); + sb.append("REFERENCES \"").append(foreignKey.getReferencedSchema()).append("\".\"").append(foreignKey.getReferencedTable()).append("\" ("); for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { sb.append("\"").append(link.columnInReferencedTable()).append("\", "); } @@ -163,9 +163,9 @@ private List getDefs(Collection schemas) { sb = new StringBuilder(); sb.append("DO $$ BEGIN "); - sb.append("IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = '").append(fKeyName).append("' AND table_name = '").append(foreignKey.getTable()).append("' AND constraint_schema = '").append(foreignKey.getSchema()).append("' AND constraint_type = 'FOREIGN KEY') THEN "); + sb.append("IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name = '").append(fKeyName).append("' AND table_name = '").append(foreignKey.getReferringTable()).append("' AND constraint_schema = '").append(foreignKey.getReferringSchema()).append("' AND constraint_type = 'FOREIGN KEY') THEN "); - sb.append("ALTER TABLE \"").append(foreignKey.getSchema()).append("\".\"").append(foreignKey.getTable()).append("\" "); + sb.append("ALTER TABLE \"").append(foreignKey.getReferringSchema()).append("\".\"").append(foreignKey.getReferringTable()).append("\" "); sb.append("ADD CONSTRAINT ").append(fKeyName).append(" "); sb.append("FOREIGN KEY ("); for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { @@ -173,7 +173,7 @@ private List getDefs(Collection schemas) { } sb.setLength(sb.length() - 2); sb.append(") "); - sb.append("REFERENCES \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" ("); + sb.append("REFERENCES \"").append(foreignKey.getReferencedSchema()).append("\".\"").append(foreignKey.getReferencedTable()).append("\" ("); for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { sb.append("\"").append(link.columnInReferencedTable()).append("\", "); } @@ -185,6 +185,13 @@ private List getDefs(Collection schemas) { } } } + for (SQLSchema schema : schemas) { + for (SQLTable table : schema.getTables()) { + for (SQLTrigger trigger : table.getTriggers()) { + statements.add(DDLStatement.of(trigger.getH2SQL(), trigger.getPgSQL())); + } + } + } return statements; } @@ -250,6 +257,7 @@ private void parseColumn(Class clazz, Map clazz, Map clazz, Map clazz, Map clazz, Map type = dataManager.getSerializedType(ReflectionUtils.getGenericType(field)); @@ -421,26 +433,20 @@ private void parseReference(Class clazz, Map OnDelete.CASCADE; - case NO_ACTION -> OnDelete.NO_ACTION; - default -> throw new IllegalStateException("Unexpected value: " + oneToOne.deleteStrategy()); - }; - - OnUpdate onUpdate = switch (oneToOne.updateStrategy()) { - case CASCADE -> OnUpdate.CASCADE; - case NO_ACTION -> OnUpdate.NO_ACTION; - }; - - ForeignKey foreignKey = new ForeignKey(schema.getName(), table.getName(), onDelete, onUpdate); + ForeignKey foreignKey = new ForeignKey(schema.getName(), table.getName(), referencedSchema.getName(), referencedTable.getName(), OnDelete.SET_NULL, OnUpdate.CASCADE); try { - for (ForeignKey.Link link : parseLinks(oneToOne.link())) { //reverse, since the fkey is on our table - foreignKey.addLink(new ForeignKey.Link(link.columnInReferringTable(), link.columnInReferencedTable())); + for (ForeignKey.Link link : parseLinks(oneToOne.link())) { + foreignKey.addLink(link); } } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Error parsing @OneToOne link on field " + field.getName() + " in class " + clazz.getName() + ": " + e.getMessage(), e); } //todo: all columns in the link must be unique in our table, and must be id columns in the referenced table. this will be enforced by H2 but check here for better errors - referencedTable.addForeignKeyThatReferencesThisTable(foreignKey); + table.addForeignKey(foreignKey); + + + Delete delete = field.getAnnotation(Delete.class); + DeleteStrategy deleteStrategy = delete != null ? delete.value() : DeleteStrategy.NO_ACTION; + table.addTrigger(new SQLDeleteStrategyTrigger(dataSchema, dataTable, referencedSchema.getName(), referencedTable.getName(), deleteStrategy, foreignKey.getLinkingColumns())); } private void parseLinks(ForeignKey foreignKey, String links) { @@ -455,7 +461,7 @@ private List parseLinks(String links) { for (String link : StringUtils.parseCommaSeperatedList(links)) { String[] parts = link.split("="); Preconditions.checkArgument(parts.length == 2, "Invalid link format! Expected format: localColumn=foreignColumn, got: " + link); - mappings.add(new ForeignKey.Link(ValueUtils.parseValue(parts[0].trim()), ValueUtils.parseValue(parts[1].trim()))); + mappings.add(new ForeignKey.Link(ValueUtils.parseValue(parts[1].trim()), ValueUtils.parseValue(parts[0].trim()))); } return mappings; } diff --git a/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java b/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java new file mode 100644 index 00000000..5d146761 --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java @@ -0,0 +1,78 @@ +package net.staticstudios.data.parse; + +import net.staticstudios.data.DeleteStrategy; +import net.staticstudios.data.impl.h2.trigger.H2DeleteStrategyCascadeTrigger; +import org.intellij.lang.annotations.Language; + +import java.util.Set; + +public class SQLDeleteStrategyTrigger implements SQLTrigger { + private final String parentSchema; + private final String parentTable; + private final String targetSchema; + private final String targetTable; + private final DeleteStrategy deleteStrategy; + private final Set links; + + public SQLDeleteStrategyTrigger(String parentSchema, String parentTable, String targetSchema, String targetTable, DeleteStrategy deleteStrategy, Set links) { + this.parentSchema = parentSchema; + this.parentTable = parentTable; + this.targetSchema = targetSchema; + this.targetTable = targetTable; + this.deleteStrategy = deleteStrategy; + this.links = links; + } + + @Override + public String getPgSQL() { + if (deleteStrategy == DeleteStrategy.CASCADE) { + @Language("SQL") String createTriggerFunction = """ + CREATE OR REPLACE FUNCTION static_data_v3_%s_%s_%s_%s_delete_trigger() + RETURNS TRIGGER AS $$ + BEGIN + %s + RETURN OLD; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS static_data_v3_%s_%s_%s_%s_delete_trigger ON %s.%s; + CREATE TRIGGER static_data_v3_%s_%s_%s_%s_delete_trigger + AFTER DELETE ON %s.%s + FOR EACH ROW EXECUTE FUNCTION static_data_v3_%s_%s_%s_%s_delete_trigger(); + """; + + + String action = "DELETE FROM \"" + targetSchema + "\".\"" + targetTable + "\" WHERE " + + String.join(" AND ", links.stream().map(link -> String.format("%s = OLD.%s", link.columnInReferencedTable(), link.columnInReferringTable())).toList()) + ";"; + + return String.format(createTriggerFunction, + parentSchema, parentTable, targetSchema, targetTable, + action, + parentSchema, parentTable, targetSchema, targetTable, + parentSchema, parentTable, + parentSchema, parentTable, targetSchema, targetTable, + parentSchema, parentTable, + parentSchema, parentTable, targetSchema, + targetTable + ); + } + return "DROP TRIGGER IF EXISTS static_data_v3_" + parentSchema + "_" + parentTable + "_" + targetSchema + "_" + targetTable + "_delete_trigger ON " + parentSchema + "." + parentTable + ";"; + } + + @Override + public String getH2SQL() { + if (deleteStrategy == DeleteStrategy.CASCADE) { + String triggerClass = H2DeleteStrategyCascadeTrigger.class.getName(); + String encodedLinks = String.join("", links.stream().map(link -> prefixString(link.columnInReferringTable()) + prefixString(link.columnInReferencedTable())).toList()); + + String triggerName = "static_data_v3_" + prefixString(parentSchema) + "_" + prefixString(parentTable) + "_" + prefixString(targetSchema) + "_" + prefixString(targetTable) + "__delete_links__" + (links.size() * 2) + "_" + encodedLinks; + return "CREATE TRIGGER IF NOT EXISTS \"" + triggerName + "_delete_trigger\" AFTER DELETE ON \"" + parentSchema + "\".\"" + parentTable + "\" FOR EACH ROW CALL \"" + triggerClass + "\""; + } + return ""; + } + + private String prefixString(String str) { + return str.length() + "_" + str; + } + +} diff --git a/src/main/java/net/staticstudios/data/parse/SQLTable.java b/src/main/java/net/staticstudios/data/parse/SQLTable.java index 62fb43ad..a451ed1b 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLTable.java +++ b/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -11,14 +11,16 @@ public class SQLTable { private final String name; private final List idColumns; private final Map columns; - private final Set foreignKeysThatReferenceThisTable; + private final Set foreignKeys; + private final Set triggers; public SQLTable(SQLSchema schema, String name, List idColumns) { this.schema = schema; this.name = name; this.idColumns = idColumns; this.columns = new HashMap<>(); - this.foreignKeysThatReferenceThisTable = new LinkedHashSet<>(); + this.foreignKeys = new HashSet<>(); + this.triggers = new HashSet<>(); } public SQLSchema getSchema() { @@ -37,19 +39,29 @@ public Set getColumns() { return columns.get(columnName); } - public Set getForeignKeysThatReferenceThisTable() { - return Collections.unmodifiableSet(foreignKeysThatReferenceThisTable); - } - - public void addForeignKeyThatReferencesThisTable(ForeignKey foreignKey) { - ForeignKey existingKey = foreignKeysThatReferenceThisTable.stream() - .filter(fk -> fk.getSchema().equals(foreignKey.getSchema()) && fk.getTable().equals(foreignKey.getTable())) + public void addForeignKey(ForeignKey foreignKey) { + Preconditions.checkNotNull(foreignKey, "Foreign key cannot be null"); + ForeignKey existingKey = foreignKeys.stream() + .filter(fk -> fk.getReferencedSchema().equals(foreignKey.getReferencedSchema()) && fk.getReferencedTable().equals(foreignKey.getReferencedTable()) && fk.getReferringSchema().equals(foreignKey.getReferringSchema()) && fk.getReferringTable().equals(foreignKey.getReferringTable())) .findFirst() .orElse(null); if (existingKey != null && !Objects.equals(existingKey, foreignKey)) { - throw new IllegalArgumentException("Foreign key from " + foreignKey.getSchema() + "." + foreignKey.getTable() + " already exists and is different from the one being added! Existing: " + existingKey + ", New: " + foreignKey); + throw new IllegalArgumentException("Foreign key to " + foreignKey.getReferencedSchema() + "." + foreignKey.getReferencedTable() + " already exists and is different from the one being added! Existing: " + existingKey + ", New: " + foreignKey); } - foreignKeysThatReferenceThisTable.add(foreignKey); + foreignKeys.add(foreignKey); + } + + public Set getForeignKeys() { + return Collections.unmodifiableSet(foreignKeys); + } + + public void addTrigger(SQLTrigger trigger) { + Preconditions.checkNotNull(trigger, "Trigger cannot be null"); + triggers.add(trigger); + } + + public Set getTriggers() { + return Collections.unmodifiableSet(triggers); } public List getIdColumns() { diff --git a/src/main/java/net/staticstudios/data/parse/SQLTrigger.java b/src/main/java/net/staticstudios/data/parse/SQLTrigger.java new file mode 100644 index 00000000..d27b96d7 --- /dev/null +++ b/src/main/java/net/staticstudios/data/parse/SQLTrigger.java @@ -0,0 +1,8 @@ +package net.staticstudios.data.parse; + +public interface SQLTrigger { + + String getPgSQL(); + + String getH2SQL(); +} diff --git a/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java b/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java index c66d7d19..a08048cb 100644 --- a/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java +++ b/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java @@ -18,6 +18,7 @@ public List getLinks() { return links; } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; @@ -35,6 +36,7 @@ public int hashCode() { public String toString() { return "ForeignPersistentValueMetadata[" + "columnMetadata=" + getColumnMetadata() + ", " + - "links=" + links + ']'; + "links=" + links + + "]"; } } diff --git a/src/main/java/net/staticstudios/data/util/SQlStatement.java b/src/main/java/net/staticstudios/data/util/SQlStatement.java index 1de8162e..b0a9324f 100644 --- a/src/main/java/net/staticstudios/data/util/SQlStatement.java +++ b/src/main/java/net/staticstudios/data/util/SQlStatement.java @@ -3,16 +3,22 @@ import java.util.List; public class SQlStatement { - private final String sql; + private final String h2Sql; + private final String pgSql; private final List values; - public SQlStatement(String sql, List values) { - this.sql = sql; + public SQlStatement(String h2Sql, String pgSql, List values) { + this.h2Sql = h2Sql; + this.pgSql = pgSql; this.values = values; } - public String getSql() { - return sql; + public String getH2Sql() { + return h2Sql; + } + + public String getPgSql() { + return pgSql; } public List getValues() { diff --git a/src/test/java/net/staticstudios/data/CustomTypeTest.java b/src/test/java/net/staticstudios/data/CustomTypeTest.java index 9fa705ca..0bf8c69e 100644 --- a/src/test/java/net/staticstudios/data/CustomTypeTest.java +++ b/src/test/java/net/staticstudios/data/CustomTypeTest.java @@ -409,5 +409,5 @@ public void testCustomTypeWithByteArrayPrimitive() throws SQLException { } } - //todo: test postgres listen/notify with custom types + //todo: test postgres listen/notify with custom types. specifically ensure the encode and decode functions are correct } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java index 5a5d070d..ec56fda2 100644 --- a/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -5,6 +5,7 @@ import net.staticstudios.data.mock.user.MockUser; import net.staticstudios.data.mock.user.MockUserFactory; import net.staticstudios.data.util.ColumnValuePair; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.sql.Connection; @@ -176,15 +177,15 @@ public void testReceiveInsertFromPostgres() { try (PreparedStatement preferencesStatement = getConnection().prepareStatement("INSERT INTO user_preferences (user_id, fav_color) VALUES (?, ?)"); PreparedStatement metadataStatement = getConnection().prepareStatement("INSERT INTO user_metadata (user_id, name_updates) VALUES (?, ?)"); PreparedStatement userStatement = getConnection().prepareStatement("INSERT INTO users (id, name) VALUES (?, ?)")) { - userStatement.setObject(1, id); - userStatement.setString(2, "inserted from pg"); - userStatement.executeUpdate(); preferencesStatement.setObject(1, id); preferencesStatement.setObject(2, 0); preferencesStatement.executeUpdate(); metadataStatement.setObject(1, id); metadataStatement.setInt(2, 0); metadataStatement.executeUpdate(); + userStatement.setObject(1, id); + userStatement.setString(2, "inserted from pg"); + userStatement.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(e); } @@ -228,6 +229,7 @@ public void testReceiveDeleteFromPostgres() { assertNull(mockUser); } + @Disabled //todo: known to break @Test public void testChangeIdColumn() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); @@ -257,8 +259,10 @@ public void testChangeIdColumn() { assertEquals(2, mockUser.nameUpdates.get()); } + @Disabled //todo: known to break @Test public void testChangeIdColumnInPostgres() { + //todo: this and the other id column test are failing because fkeys have been changed to be on the user table. use a trigger to update the fkeys on id change, similar to the cascade delete trigger DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); @@ -335,4 +339,170 @@ public void testUpdateInterval() throws Exception { assertEquals(4, rs.getInt("views")); } } + + @Test + public void testInsertStrategyPreferExisting() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + Connection connection = getConnection(); + UUID id = UUID.randomUUID(); + try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO public.user_preferences (user_id, fav_color) VALUES (?, ?)")) { + preparedStatement.setObject(1, id); + preparedStatement.setString(2, "blue"); + preparedStatement.executeUpdate(); + } + + MockUser user1 = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .favoriteColor("red") + .insert(InsertMode.SYNC); + assertEquals("red", user1.favoriteColor.get()); + MockUser user2 = MockUserFactory.builder(dataManager) + .id(id) + .name("test user2") + .favoriteColor("green") + .insert(InsertMode.SYNC); + assertEquals("blue", user2.favoriteColor.get()); + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = connection.prepareStatement("SELECT fav_color FROM public.user_preferences WHERE user_id = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals("blue", rs.getString("fav_color")); + } + } + + @Test + public void testInsertStrategyOverwriteExisting() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + Connection connection = getConnection(); + UUID id = UUID.randomUUID(); + try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO public.user_metadata (user_id, name_updates) VALUES (?, ?)")) { + preparedStatement.setObject(1, id); + preparedStatement.setInt(2, 5); + preparedStatement.executeUpdate(); + } + MockUser user1 = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .nameUpdates(10) + .insert(InsertMode.SYNC); + assertEquals(10, user1.nameUpdates.get()); + MockUser user2 = MockUserFactory.builder(dataManager) + .id(id) + .name("test user2") + .nameUpdates(15) + .insert(InsertMode.SYNC); + assertEquals(15, user2.nameUpdates.get()); + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = connection.prepareStatement("SELECT name_updates FROM public.user_metadata WHERE user_id = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(15, rs.getInt("name_updates")); + } + } + + @Test + public void testDeleteStrategyCascade() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + Connection h2Connection = getH2Connection(dataManager); + Connection pgConnection = getConnection(); + UUID id = UUID.randomUUID(); + MockUser user = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .favoriteColor("red") + .nameUpdates(0) + .insert(InsertMode.SYNC); + assertEquals("red", user.favoriteColor.get()); + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT \"fav_color\" FROM \"public\".\"user_preferences\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals("red", rs.getString("fav_color")); + } + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT \"fav_color\" FROM \"public\".\"user_preferences\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals("red", rs.getString("fav_color")); + } + + user.delete(); + assertTrue(user.isDeleted()); + + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT * FROM \"public\".\"user_preferences\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertFalse(rs.next()); + } + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_preferences\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertFalse(rs.next()); + } + } + + @Test + public void testDeleteStrategyNoAction() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + Connection h2Connection = getH2Connection(dataManager); + Connection pgConnection = getConnection(); + UUID id = UUID.randomUUID(); + MockUser user = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .nameUpdates(10) + .insert(InsertMode.SYNC); + assertEquals(10, user.nameUpdates.get()); + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT \"name_updates\" FROM \"public\".\"user_metadata\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(10, rs.getInt("name_updates")); + } + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT \"name_updates\" FROM \"public\".\"user_metadata\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(10, rs.getInt("name_updates")); + } + + user.delete(); + assertTrue(user.isDeleted()); + + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT \"name_updates\" FROM \"public\".\"user_metadata\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(10, rs.getInt("name_updates")); + } + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT \"name_updates\" FROM \"public\".\"user_metadata\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(10, rs.getInt("name_updates")); + } + } } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/ReferenceTest.java b/src/test/java/net/staticstudios/data/ReferenceTest.java index 453dddf1..816203a2 100644 --- a/src/test/java/net/staticstudios/data/ReferenceTest.java +++ b/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -8,6 +8,10 @@ import net.staticstudios.data.mock.user.MockUserSettingsFactory; import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; @@ -112,4 +116,56 @@ public void testChangeReference() { assertSame(settings2, user.settings.get()); assertEquals(settings2.id.get(), user.settingsId.get()); } + + @Test + public void testDeleteStrategyCascade() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + Connection h2Connection = getH2Connection(dataManager); + Connection pgConnection = getConnection(); + UUID id = UUID.randomUUID(); + MockUserSettings settings = MockUserSettingsFactory.builder(dataManager) + .id(id) + .insert(InsertMode.SYNC); + + MockUser user = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .settingsId(settings.id.get()) + .insert(InsertMode.SYNC); + assertSame(settings, user.settings.get()); + + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT \"user_id\" FROM \"public\".\"user_settings\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + } + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT user_id FROM public.user_settings WHERE user_id = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + } + + user.delete(); + assertTrue(user.isDeleted()); + assertTrue(settings.isDeleted()); + + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT \"user_id\" FROM \"public\".\"user_settings\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertFalse(rs.next()); + } + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT user_id FROM public.user_settings WHERE user_id = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertFalse(rs.next()); + } + } + } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index 641910df..4152ec24 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -93,12 +93,14 @@ public void testParse() throws Exception { ALTER TABLE ONLY social_media.posts ADD CONSTRAINT posts_pkey PRIMARY KEY (post_id); CREATE INDEX idx_social_media_posts_text_content ON social_media.posts USING btree (text_content); - ALTER TABLE ONLY social_media.posts_interactions - ADD CONSTRAINT fk_social_media_posts_interactions_post_id_to_posts_post_id FOREIGN KEY (post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY social_media.posts - ADD CONSTRAINT fk_social_media_posts_post_id_to_posts_metadata_metadata_id FOREIGN KEY (post_id) REFERENCES social_media.posts_metadata(metadata_id) ON UPDATE CASCADE; + ADD CONSTRAINT fk_social_media_posts_post_id_to_social_media_posts_interaction FOREIGN KEY (post_id) REFERENCES social_media.posts_interactions(post_id) ON UPDATE CASCADE ON DELETE CASCADE; + ALTER TABLE ONLY social_media.posts + ADD CONSTRAINT fk_social_media_posts_post_id_to_social_media_posts_metadata_me FOREIGN KEY (post_id) REFERENCES social_media.posts_metadata(metadata_id) ON UPDATE CASCADE ON DELETE SET NULL; """; assertEquals(expected.trim(), cleanedDump.toString().trim()); } + + //todo: when a delete strategy is set to no action where it was previously set to cascade, the old trigger should be dropped. Add a test for this. } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/post/MockPost.java b/src/test/java/net/staticstudios/data/mock/post/MockPost.java index e9944198..ce7a3b07 100644 --- a/src/test/java/net/staticstudios/data/mock/post/MockPost.java +++ b/src/test/java/net/staticstudios/data/mock/post/MockPost.java @@ -14,8 +14,10 @@ public class MockPost extends UniqueData { @Column(name = "text_content", index = true) public PersistentValue textContent; - @Column(name = "likes", defaultValue = "0") + @DefaultValue("0") + @Column(name = "likes") public PersistentValue likes; - @ForeignColumn(name = "interactions", table = "${POST_TABLE}_interactions", link = "${POST_ID_COLUMN}=post_id", defaultValue = "0") + @DefaultValue("0") + @ForeignColumn(name = "interactions", table = "${POST_TABLE}_interactions", link = "${POST_ID_COLUMN}=post_id") public PersistentValue interactions; } diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/src/test/java/net/staticstudios/data/mock/user/MockUser.java index f335f0f2..e43e2d2b 100644 --- a/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -6,7 +6,6 @@ //todo: heres how inheritance should look: // if the super class provides a data annotation, ignore it and use the child's annotation. it would be cool tho to allow the super class to use a @data annotation. the former is whats implemented now. if changed, update the processor. -//todo: make default values their own annotation @Data(schema = "public", table = "users") public class MockUser extends UniqueData { //todo: cached values @@ -14,21 +13,35 @@ public class MockUser extends UniqueData { //todo: note - maybe PC's add and remove handlers can be implemented using update handlers @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); + @Column(name = "settings_id", nullable = true, unique = true) public PersistentValue settingsId = PersistentValue.of(this, UUID.class); + @Column(name = "age", nullable = true) public PersistentValue age; + + @Insert(InsertStrategy.PREFER_EXISTING) + @Delete(DeleteStrategy.CASCADE) @ForeignColumn(name = "fav_color", table = "user_preferences", nullable = true, link = "id=user_id") public PersistentValue favoriteColor; - @OneToOne(link = "settings_id=user_id", deleteStrategy = DeleteStrategy.CASCADE) + + @Delete(DeleteStrategy.CASCADE) + @OneToOne(link = "settings_id=user_id") public Reference settings; - @ForeignColumn(name = "name_updates", table = "user_metadata", link = "id=user_id", defaultValue = "0") + + @Insert(InsertStrategy.OVERWRITE_EXISTING) + @Delete(DeleteStrategy.NO_ACTION) + @DefaultValue("0") + @ForeignColumn(name = "name_updates", table = "user_metadata", link = "id=user_id") public PersistentValue nameUpdates; - @Column(name = "name", index = true, defaultValue = "Unknown") + + @DefaultValue("Unknown") + @Column(name = "name", index = true) public PersistentValue name = PersistentValue.of(this, String.class) .onUpdate(MockUser.class, (user, update) -> { user.nameUpdates.set(user.getNameUpdates() + 1); }); + @UpdateInterval(5000) @Column(name = "views", nullable = true) public PersistentValue views; diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java b/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java index 66562717..0e50f59a 100644 --- a/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java +++ b/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java @@ -8,6 +8,7 @@ public class MockUserSettings extends UniqueData { @IdColumn(name = "user_id") public PersistentValue id; - @Column(name = "font_size", defaultValue = "10") + @DefaultValue("10") + @Column(name = "font_size") public PersistentValue fontSize; } From 285bf7ffc3c2771163d7961305cd899706c798b9 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 30 Sep 2025 23:52:41 -0400 Subject: [PATCH 23/75] add quotes --- .../net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java b/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java index 5d146761..b56fb1f2 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java +++ b/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java @@ -43,7 +43,7 @@ public String getPgSQL() { String action = "DELETE FROM \"" + targetSchema + "\".\"" + targetTable + "\" WHERE " + - String.join(" AND ", links.stream().map(link -> String.format("%s = OLD.%s", link.columnInReferencedTable(), link.columnInReferringTable())).toList()) + ";"; + String.join(" AND ", links.stream().map(link -> String.format("\"%s\" = OLD.\"%s\"", link.columnInReferencedTable(), link.columnInReferringTable())).toList()) + ";"; return String.format(createTriggerFunction, parentSchema, parentTable, targetSchema, targetTable, From b0ea85002f2c603d75d3db534ebaca94a08f09db Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 4 Oct 2025 02:34:15 -0400 Subject: [PATCH 24/75] begin work on collections --- .../staticstudios/data/DeleteStrategy.java | 2 + .../net/staticstudios/data/ManyToMany.java | 23 + .../net/staticstudios/data/OneToMany.java | 19 + .../net/staticstudios/data/DataManager.java | 16 +- .../data/PersistentCollection.java | 128 +++++ .../net/staticstudios/data/Reference.java | 8 + .../java/net/staticstudios/data/Relation.java | 4 - .../PersistentOneToManyCollectionImpl.java | 445 ++++++++++++++++++ .../data/impl/data/ReferenceImpl.java | 54 ++- .../staticstudios/data/parse/ForeignKey.java | 17 +- .../staticstudios/data/parse/SQLBuilder.java | 132 +++++- .../util/PersistentCollectionMetadata.java | 4 + ...PersistentOneToManyCollectionMetadata.java | 43 ++ .../net/staticstudios/data/util/Relation.java | 4 + .../data/util/UniqueDataMetadata.java | 3 +- .../PersistentOneToManyCollectionTest.java | 59 +++ .../net/staticstudios/data/SQLParseTest.java | 2 +- .../data/mock/user/MockUser.java | 9 + .../data/mock/user/MockUserSession.java | 21 + 19 files changed, 930 insertions(+), 63 deletions(-) create mode 100644 annotations/src/main/java/net/staticstudios/data/ManyToMany.java create mode 100644 annotations/src/main/java/net/staticstudios/data/OneToMany.java create mode 100644 src/main/java/net/staticstudios/data/PersistentCollection.java delete mode 100644 src/main/java/net/staticstudios/data/Relation.java create mode 100644 src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java create mode 100644 src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java create mode 100644 src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java create mode 100644 src/main/java/net/staticstudios/data/util/Relation.java create mode 100644 src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java create mode 100644 src/test/java/net/staticstudios/data/mock/user/MockUserSession.java diff --git a/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java index 9364bcd1..f47b0032 100644 --- a/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java +++ b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java @@ -7,6 +7,8 @@ public enum DeleteStrategy { CASCADE, /** * Do nothing when the parent data is deleted. + * For a one-to-many collections, this will unlink the data by setting the foreign key to null. + * For a many-to-many collection, this will remove the entries in the join table. */ NO_ACTION } diff --git a/annotations/src/main/java/net/staticstudios/data/ManyToMany.java b/annotations/src/main/java/net/staticstudios/data/ManyToMany.java new file mode 100644 index 00000000..a03ed03d --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/ManyToMany.java @@ -0,0 +1,23 @@ +package net.staticstudios.data; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ManyToMany { + /** + * How should this relation be linked? + * Format "localColumn=foreignColumn" + * + * @return The link format + */ + String link(); + + String joinTable() default ""; + + String joinTableSchema() default ""; +} diff --git a/annotations/src/main/java/net/staticstudios/data/OneToMany.java b/annotations/src/main/java/net/staticstudios/data/OneToMany.java new file mode 100644 index 00000000..3fc0e628 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/OneToMany.java @@ -0,0 +1,19 @@ +package net.staticstudios.data; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface OneToMany { + /** + * How should this relation be linked? + * Format "localColumn=foreignColumn" + * + * @return The link format + */ + String link(); +} diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index 4e9efb58..c21a05c3 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -2,6 +2,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.MapMaker; +import net.staticstudios.data.impl.data.PersistentOneToManyCollectionImpl; import net.staticstudios.data.impl.data.PersistentValueImpl; import net.staticstudios.data.impl.data.ReferenceImpl; import net.staticstudios.data.impl.h2.H2DataAccessor; @@ -201,13 +202,18 @@ public void extractMetadata(Class clazz) { Preconditions.checkArgument(!idColumns.isEmpty(), "UniqueData class %s must have at least one @IdColumn annotated PersistentValue field", clazz.getName()); String schema = ValueUtils.parseValue(dataAnnotation.schema()); String table = ValueUtils.parseValue(dataAnnotation.table()); - UniqueDataMetadata metadata = new UniqueDataMetadata(clazz, schema, table, idColumns, PersistentValueImpl.extractMetadata(schema, table, clazz), ReferenceImpl.extractMetadata(clazz)); + Map persistentCollectionMetadataMap = new HashMap<>(); + persistentCollectionMetadataMap.putAll(PersistentOneToManyCollectionImpl.extractMetadata(clazz)); //todo: add other collection types + UniqueDataMetadata metadata = new UniqueDataMetadata(clazz, schema, table, idColumns, PersistentValueImpl.extractMetadata(schema, table, clazz), ReferenceImpl.extractMetadata(clazz), persistentCollectionMetadataMap); uniqueDataMetadataMap.put(clazz, metadata); for (Field field : ReflectionUtils.getFields(clazz, Relation.class)) { - Class dependencyClass = Objects.requireNonNull(ReflectionUtils.getGenericType(field)).asSubclass(UniqueData.class); - if (!uniqueDataMetadataMap.containsKey(dependencyClass)) { - extractMetadata(dependencyClass); + Class genericType = ReflectionUtils.getGenericType(field); + if (genericType != null && UniqueData.class.isAssignableFrom(genericType)) { + Class dependencyClass = genericType.asSubclass(UniqueData.class); + if (!uniqueDataMetadataMap.containsKey(dependencyClass)) { + extractMetadata(dependencyClass); + } } } } @@ -398,6 +404,8 @@ public T getInstance(Class clazz, ColumnValuePair... i PersistentValueImpl.delegate(instance); ReferenceImpl.delegate(instance); + PersistentOneToManyCollectionImpl.delegate(instance); + //todo: other collection types uniqueDataInstanceCache.computeIfAbsent(clazz, k -> new MapMaker().weakValues().makeMap()) .put(idColumns, instance); diff --git a/src/main/java/net/staticstudios/data/PersistentCollection.java b/src/main/java/net/staticstudios/data/PersistentCollection.java new file mode 100644 index 00000000..77539dee --- /dev/null +++ b/src/main/java/net/staticstudios/data/PersistentCollection.java @@ -0,0 +1,128 @@ +package net.staticstudios.data; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.util.Relation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.AccessFlag; +import java.util.Collection; +import java.util.Iterator; + +public interface PersistentCollection extends Collection, Relation { + + static PersistentCollection of(UniqueData holder, Class referenceType) { + return new ProxyPersistentCollection<>(holder, referenceType); + } + + //todo: add and remove handlers + + UniqueData getHolder(); + + Class getReferenceType(); + + class ProxyPersistentCollection implements PersistentCollection { + private final UniqueData holder; + private final Class referenceType; + private @Nullable PersistentCollection delegate; + + public ProxyPersistentCollection(UniqueData holder, Class referenceType) { + Preconditions.checkArgument(!holder.getClass().accessFlags().contains(AccessFlag.ABSTRACT), "Holder cannot be an abstract class! Please create this collection with the real class via PersistentCollection.of(...)"); + this.holder = holder; + this.referenceType = referenceType; + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getReferenceType() { + return referenceType; + } + + public void setDelegate(PersistentCollection delegate) { + Preconditions.checkState(this.delegate == null, "Delegate has already been set"); + this.delegate = delegate; + } + + @Override + public int size() { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.size(); + } + + @Override + public boolean isEmpty() { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.contains(o); + } + + @Override + public @NotNull Iterator iterator() { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.iterator(); + } + + @Override + public @NotNull Object @NotNull [] toArray() { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.toArray(); + } + + @Override + public @NotNull T1 @NotNull [] toArray(@NotNull T1 @NotNull [] a) { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.toArray(a); + } + + @Override + public boolean add(T t) { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.add(t); + } + + @Override + public boolean remove(Object o) { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.remove(o); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.containsAll(c); + } + + @Override + public boolean addAll(@NotNull Collection c) { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.addAll(c); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.removeAll(c); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.retainAll(c); + } + + @Override + public void clear() { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + delegate.clear(); + } + } +} diff --git a/src/main/java/net/staticstudios/data/Reference.java b/src/main/java/net/staticstudios/data/Reference.java index a5da1b68..40c4aad1 100644 --- a/src/main/java/net/staticstudios/data/Reference.java +++ b/src/main/java/net/staticstudios/data/Reference.java @@ -1,10 +1,17 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; +import net.staticstudios.data.util.Relation; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.AccessFlag; + public interface Reference extends Relation { + static Reference of(UniqueData holder, Class referenceType) { + return new ProxyReference<>(holder, referenceType); + } + UniqueData getHolder(); Class getReferenceType(); @@ -19,6 +26,7 @@ class ProxyReference implements Reference { private @Nullable Reference delegate; public ProxyReference(UniqueData holder, Class referenceType) { + Preconditions.checkArgument(!holder.getClass().accessFlags().contains(AccessFlag.ABSTRACT), "Holder cannot be an abstract class! Please create this reference with the real class via Reference.of(...)"); this.holder = holder; this.referenceType = referenceType; } diff --git a/src/main/java/net/staticstudios/data/Relation.java b/src/main/java/net/staticstudios/data/Relation.java deleted file mode 100644 index 57b98b3f..00000000 --- a/src/main/java/net/staticstudios/data/Relation.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.staticstudios.data; - -public interface Relation { -} diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java new file mode 100644 index 00000000..02f7eb44 --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java @@ -0,0 +1,445 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.DataAccessor; +import net.staticstudios.data.OneToMany; +import net.staticstudios.data.PersistentCollection; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.ForeignKey; +import net.staticstudios.data.parse.SQLBuilder; +import net.staticstudios.data.util.*; +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; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +public class PersistentOneToManyCollectionImpl implements PersistentCollection { + private final UniqueData holder; + private final Class type; + private final List link; + + public PersistentOneToManyCollectionImpl(UniqueData holder, Class type, List link) { + this.holder = holder; + this.type = type; + this.link = link; + } + + public static void createAndDelegate(PersistentCollection.ProxyPersistentCollection proxy, List link) { + PersistentOneToManyCollectionImpl delegate = new PersistentOneToManyCollectionImpl<>( + proxy.getHolder(), + proxy.getReferenceType(), + link + ); + proxy.setDelegate(delegate); + } + + public static PersistentOneToManyCollectionImpl create(UniqueData holder, Class type, List link) { + return new PersistentOneToManyCollectionImpl<>(holder, type, link); + } + + 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()); + } else { + pair.field().setAccessible(true); + try { + pair.field().set(instance, create(instance, oneToManyMetadata.getDataType(), oneToManyMetadata.getLinks())); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + public static Map extractMetadata(Class clazz) { + Map metadataMap = new HashMap<>(); + for (Field field : ReflectionUtils.getFields(clazz, PersistentCollection.class)) { + OneToMany oneToManyAnnotation = field.getAnnotation(OneToMany.class); + if (oneToManyAnnotation == null) continue; + Class genericType = ReflectionUtils.getGenericType(field); + if (genericType == null || !UniqueData.class.isAssignableFrom(genericType)) continue; + Class referencedClass = genericType.asSubclass(UniqueData.class); + metadataMap.put(field, new PersistentOneToManyCollectionMetadata(referencedClass, SQLBuilder.parseLinks(oneToManyAnnotation.link()))); + } + + return metadataMap; + } + + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getReferenceType() { + return type; + } + + @Override + public int size() { + return getIds().size(); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean contains(Object o) { + if (!type.isInstance(o)) { + return false; + } + T data = type.cast(o); + List ids = getIds(); + ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + for (ColumnValuePair[] idColumns : ids) { + if (Arrays.equals(idColumns, thatIdColumns)) { + return true; + } + } + + return false; + } + + @Override + public @NotNull Iterator iterator() { + return new IteratorImpl(getIds()); + } + + @Override + public @NotNull Object @NotNull [] toArray() { + List ids = getIds(); + Object[] array = new Object[ids.size()]; + int i = 0; + for (ColumnValuePair[] idColumns : ids) { + T instance = holder.getDataManager().getInstance(type, idColumns); + array[i++] = instance; + } + return array; + } + + @SuppressWarnings("unchecked") + @Override + public @NotNull T1 @NotNull [] toArray(@NotNull T1 @NotNull [] a) { + List ids = getIds(); + if (a.length < ids.size()) { + a = (T1[]) Array.newInstance(a.getClass().getComponentType(), ids.size()); + } + int i = 0; + for (ColumnValuePair[] idColumns : ids) { + T instance = holder.getDataManager().getInstance(type, idColumns); + T1 element = (T1) instance; + a[i++] = element; + } + return a; + } + + @Override + public boolean add(T t) { + return addAll(Collections.singleton(t)); + } + + @Override + public boolean remove(Object o) { + return removeAll(Collections.singleton(o)); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + for (Object o : c) { + if (!type.isInstance(o)) { + return false; + } + } + List ids = getIds(); + for (Object o : c) { + T data = type.cast(o); + ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + boolean found = false; + for (ColumnValuePair[] idColumns : ids) { + if (Arrays.equals(idColumns, thatIdColumns)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + @Override + public boolean addAll(@NotNull Collection c) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); + for (ForeignKey.Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = ?, "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" WHERE "); + for (ColumnMetadata theirIdColumn : typeMetadata.idColumns()) { + sqlBuilder.append("\"").append(theirIdColumn.name()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String updateSql = sqlBuilder.toString(); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + List myValues = getMyLinkingValues(holderMetadata, dataAccessor); + + for (T entry : c) { + List values = new ArrayList<>(myValues); + for (ColumnValuePair idColumn : entry.getIdColumns()) { + values.add(idColumn.value()); + } + try { + dataAccessor.executeUpdate(updateSql, values, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + return !c.isEmpty(); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + List ids = new ArrayList<>(); + for (Object o : c) { + if (!type.isInstance(o)) { + continue; + } + T data = type.cast(o); + ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + ids.add(thatIdColumns); + } + removeAll(ids); + + return !ids.isEmpty(); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + //todo: this + return false; + } + + @Override + public void clear() { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); + for (ForeignKey.Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = NULL, "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" WHERE "); + for (ForeignKey.Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String updateSql = sqlBuilder.toString(); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + List values = getMyLinkingValues(holderMetadata, dataAccessor); + try { + dataAccessor.executeUpdate(updateSql, values, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private void removeAll(List ids) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); + for (ForeignKey.Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = NULL, "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" WHERE "); + for (ForeignKey.Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = ? AND "); + } + for (ColumnMetadata theirIdColumn : typeMetadata.idColumns()) { + sqlBuilder.append("\"").append(theirIdColumn.name()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String updateSql = sqlBuilder.toString(); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + List myValues = getMyLinkingValues(holderMetadata, dataAccessor); + + for (ColumnValuePair[] idColumns : ids) { + List values = new ArrayList<>(myValues); + for (ColumnValuePair idColumn : idColumns) { + values.add(idColumn.value()); + } + try { + dataAccessor.executeUpdate(updateSql, values, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } + + private List getMyLinkingValues(UniqueDataMetadata holderMetadata, DataAccessor dataAccessor) { + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ForeignKey.Link entry : link) { + String myColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(myColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String selectSql = sqlBuilder.toString(); + + List myValues = new ArrayList<>(link.size()); + try (ResultSet rs = dataAccessor.executeQuery(selectSql, holder.getIdColumns().stream().map(ColumnValuePair::value).toList())) { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (ForeignKey.Link entry : link) { + String myColumn = entry.columnInReferringTable(); + myValues.add(rs.getObject(myColumn)); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return myValues; + } + + private List getIds() { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot get entries on a deleted UniqueData instance"); + List ids = new ArrayList<>(); + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ColumnMetadata columnMetadata : typeMetadata.idColumns()) { + sqlBuilder.append("\"").append(columnMetadata.name()).append("\", "); + } + for (ColumnMetadata columnMetadata : holderMetadata.idColumns()) { + sqlBuilder.append("source.\"").append(columnMetadata.name()).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" "); + sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" AS source ON "); + for (ForeignKey.Link entry : link) { + String myColumn = entry.columnInReferringTable(); + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\".\"").append(theirColumn).append("\" = source.\"").append(myColumn).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + sqlBuilder.append(" WHERE "); + + for (ForeignKey.Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = source.\"").append(entry.columnInReferringTable()).append("\" AND "); + } + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("source.\"").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())) { + while (rs.next()) { + int i = 0; + ColumnValuePair[] idColumns = new ColumnValuePair[typeMetadata.idColumns().size()]; + for (ColumnMetadata columnMetadata : typeMetadata.idColumns()) { + Object value = rs.getObject(columnMetadata.name()); + idColumns[i++] = new ColumnValuePair(columnMetadata.name(), value); + } + ids.add(idColumns); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return ids; + } + + @Override + public boolean equals(Object obj) { + //todo: this + return super.equals(obj); + } + + @Override + public int hashCode() { + //todo: this + return super.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (T item : this) { + sb.append(item).append(", "); + } + if (!isEmpty()) { + sb.setLength(sb.length() - 2); + } + sb.append("]"); + return sb.toString(); + } + + class IteratorImpl implements Iterator { + private final List ids; + private int index = 0; + + public IteratorImpl(List ids) { + this.ids = ids; + } + + @Override + public boolean hasNext() { + return index < ids.size(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + ColumnValuePair[] idColumns = ids.get(index++); + return holder.getDataManager().getInstance(type, idColumns); + } + + @Override + public void remove() { + Preconditions.checkState(index > 0, "next() has not been called yet"); + removeAll(Collections.singletonList(ids.get(index - 1))); + } + } +} diff --git a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java index 60b42a19..41918b8b 100644 --- a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -6,6 +6,7 @@ import net.staticstudios.data.Reference; import net.staticstudios.data.UniqueData; import net.staticstudios.data.parse.ForeignKey; +import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; @@ -13,7 +14,10 @@ import java.lang.reflect.Field; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class ReferenceImpl implements Reference { private final UniqueData holder; @@ -64,14 +68,7 @@ public static Map extractMetada Preconditions.checkNotNull(oneToOneAnnotation, "Field %s in class %s is missing @OneToOne annotation".formatted(field.getName(), clazz.getName())); Class referencedClass = ReflectionUtils.getGenericType(field); Preconditions.checkNotNull(referencedClass, "Field %s in class %s is not parameterized".formatted(field.getName(), clazz.getName())); - List link = new LinkedList<>(); - for (String l : StringUtils.parseCommaSeperatedList(oneToOneAnnotation.link())) { - String[] split = l.split("="); - Preconditions.checkArgument(split.length == 2, "Invalid link format in @OneToOne annotation on field %s in class %s".formatted(field.getName(), clazz.getName())); - link.add(new ForeignKey.Link(ValueUtils.parseValue(split[1].trim()), ValueUtils.parseValue(split[0].trim()))); - } - - metadataMap.put(field, new ReferenceMetadata((Class) referencedClass, link)); + metadataMap.put(field, new ReferenceMetadata((Class) referencedClass, SQLBuilder.parseLinks(oneToOneAnnotation.link()))); } return metadataMap; @@ -94,29 +91,36 @@ public Class getReferenceType() { int i = 0; UniqueDataMetadata holderMetadata = holder.getMetadata(); DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); for (ForeignKey.Link entry : link) { String myColumn = entry.columnInReferringTable(); - String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(myColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); + + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); - StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append("SELECT \"").append(myColumn).append("\" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); - for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + @Language("SQL") String sql = sqlBuilder.toString(); + try (ResultSet rs = dataAccessor.executeQuery(sql, holder.getIdColumns().stream().map(ColumnValuePair::value).toList())) { + if (!rs.next()) { + return null; } - sqlBuilder.setLength(sqlBuilder.length() - 5); - @Language("SQL") String sql = sqlBuilder.toString(); - try (ResultSet rs = dataAccessor.executeQuery(sql, holder.getIdColumns().stream().map(ColumnValuePair::value).toList())) { - if (rs.next()) { - if (rs.getObject(myColumn) == null) { - return null; - } - idColumns[i++] = new ColumnValuePair(theirColumn, rs.getObject(myColumn)); - } else { + + for (ForeignKey.Link entry : link) { + String myColumn = entry.columnInReferringTable(); + String theirColumn = entry.columnInReferencedTable(); + if (rs.getObject(myColumn) == null) { return null; } - } catch (SQLException e) { - throw new RuntimeException(e); + idColumns[i++] = new ColumnValuePair(theirColumn, rs.getObject(myColumn)); } + } catch (SQLException e) { + throw new RuntimeException(e); } return holder.getDataManager().getInstance(type, idColumns); diff --git a/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/src/main/java/net/staticstudios/data/parse/ForeignKey.java index 82961a10..f846508e 100644 --- a/src/main/java/net/staticstudios/data/parse/ForeignKey.java +++ b/src/main/java/net/staticstudios/data/parse/ForeignKey.java @@ -17,13 +17,13 @@ public class ForeignKey { private final OnDelete onDelete; private final OnUpdate onUpdate; - public ForeignKey(String referringSchema, String referringTable, String referencedSchema, String referencedTable, OnDelete onDelete, OnUpdate onUpdate) { + public ForeignKey(String referringSchema, String referringTable, String referencedSchema, String referencedTable, OnDelete onDelete) { this.referringSchema = referringSchema; this.referringTable = referringTable; this.referencedSchema = referencedSchema; this.referencedTable = referencedTable; this.onDelete = onDelete; - this.onUpdate = onUpdate; + this.onUpdate = OnUpdate.CASCADE; } public void addLink(Link link) { @@ -76,6 +76,19 @@ public int hashCode() { return Objects.hash(links, referencedSchema, referencedTable, onDelete, onUpdate, referringSchema, referringTable); } + @Override + public String toString() { + return "ForeignKey{" + + "links=" + links + + ", referencedSchema='" + referencedSchema + '\'' + + ", referencedTable='" + referencedTable + '\'' + + ", referringSchema='" + referringSchema + '\'' + + ", referringTable='" + referringTable + '\'' + + ", onDelete=" + onDelete + + ", onUpdate=" + onUpdate + + '}'; + } + public record Link(String columnInReferencedTable, String columnInReferringTable) { } } diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index e8f0a4fa..49af436a 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -22,11 +22,45 @@ public SQLBuilder(DataManager dataManager) { this.parsedSchemas = new HashMap<>(); } + public static void parseLinks(ForeignKey foreignKey, String links) { + for (ForeignKey.Link link : parseLinks(links)) { + foreignKey.addLink(link); + } + } + + public static void parseLinksReversed(ForeignKey foreignKey, String links) { + for (ForeignKey.Link link : parseLinksReversed(links)) { + foreignKey.addLink(link); + } + } + + public static List parseLinksReversed(String links) { + List mappings = new ArrayList<>(); + for (String link : StringUtils.parseCommaSeperatedList(links)) { + String[] parts = link.split("="); + Preconditions.checkArgument(parts.length == 2, "Invalid link format! Expected format: localColumn=foreignColumn, got: " + link); + mappings.add(new ForeignKey.Link(ValueUtils.parseValue(parts[0].trim()), ValueUtils.parseValue(parts[1].trim()))); + } + return mappings; + } + + public static List parseLinks(String links) { + List mappings = new ArrayList<>(); + for (String link : StringUtils.parseCommaSeperatedList(links)) { + String[] parts = link.split("="); + Preconditions.checkArgument(parts.length == 2, "Invalid link format! Expected format: localColumn=foreignColumn, got: " + link); + mappings.add(new ForeignKey.Link(ValueUtils.parseValue(parts[1].trim()), ValueUtils.parseValue(parts[0].trim()))); + } + return mappings; + } + public List parse(Class clazz) { Preconditions.checkNotNull(clazz, "Class cannot be null"); Set> visited = walk(clazz); Map schemas = new HashMap<>(); + + // shouldn't matter the order of the new classes since we parse relations only after creating tables. so dependencies will always exist for (Class visitedClass : visited) { parseIndividualColumns(visitedClass, schemas); } @@ -34,7 +68,6 @@ public List parse(Class clazz) { parseIndividualRelations(visitedClass, schemas); } - for (SQLSchema newSchema : schemas.values()) { if (!this.parsedSchemas.containsKey(newSchema.getName())) { this.parsedSchemas.put(newSchema.getName(), newSchema); @@ -134,7 +167,7 @@ private List getDefs(Collection schemas) { } // define fkeys after table creation, to ensure all tables exist before adding fkeys - for (SQLSchema schema : schemas) { //todo: what if an fkey's insert/delete strategy has changed? + for (SQLSchema schema : schemas) { //todo: what if an fkey's on delete/ on cascade strategy has changed? for (SQLTable table : schema.getTables()) { for (ForeignKey foreignKey : table.getForeignKeys()) { if (foreignKey == null) { @@ -212,10 +245,12 @@ private void walk(Class clazz, Set related = Objects.requireNonNull(ReflectionUtils.getGenericType(field)).asSubclass(UniqueData.class); - walk(related, visited); + Class genericType = ReflectionUtils.getGenericType(field); + if (genericType == null || !UniqueData.class.isAssignableFrom(genericType)) { + continue; } + Class related = genericType.asSubclass(UniqueData.class); + walk(related, visited); } } @@ -246,6 +281,7 @@ private void parseIndividualRelations(Class clazz, Map clazz, Map clazz, Map clazz, Map parsedLinks = parseLinks(links); - for (ForeignKey.Link link : parsedLinks) { - foreignKey.addLink(link); + private void parsePersistentCollection(Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { + if (!field.getType().equals(PersistentCollection.class)) { + return; + } + Class genericType = ReflectionUtils.getGenericType(field); + Preconditions.checkNotNull(genericType, "Field " + field.getName() + " in class " + clazz.getName() + " is not parameterized!"); + OneToMany oneToMany = field.getAnnotation(OneToMany.class); + ManyToMany manyToMany = field.getAnnotation(ManyToMany.class); + int annotationsCount = 0; + if (oneToMany != null) annotationsCount++; + if (manyToMany != null) annotationsCount++; + Preconditions.checkArgument(annotationsCount == 1, "Field " + field.getName() + " in class " + clazz.getName() + " must be annotated with either @OneToMany or @ManyToMany"); + + if (oneToMany != null) { + if (UniqueData.class.isAssignableFrom(genericType)) { + parseOneToManyPersistentCollection(oneToMany, genericType.asSubclass(UniqueData.class), clazz, schemas, dataAnnotation, metadata, field); + } else { + parseOneToManyValuePersistentCollection(oneToMany, genericType, clazz, schemas, dataAnnotation, metadata, field); + } + } + if (manyToMany != null) { + Preconditions.checkArgument(UniqueData.class.isAssignableFrom(genericType), "Field " + field.getName() + " in class " + clazz.getName() + " is not parameterized with a UniqueData type! Generic type: " + genericType); + parseManyToManyPersistentCollection(manyToMany, genericType.asSubclass(UniqueData.class), clazz, schemas, dataAnnotation, metadata, field); } } - private List parseLinks(String links) { - List mappings = new ArrayList<>(); - for (String link : StringUtils.parseCommaSeperatedList(links)) { - String[] parts = link.split("="); - Preconditions.checkArgument(parts.length == 2, "Invalid link format! Expected format: localColumn=foreignColumn, got: " + link); - mappings.add(new ForeignKey.Link(ValueUtils.parseValue(parts[1].trim()), ValueUtils.parseValue(parts[0].trim()))); + private void parseOneToManyValuePersistentCollection(OneToMany oneToMany, Class genericType, Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { + //todo: this + } + + private void parseOneToManyPersistentCollection(OneToMany oneToMany, Class genericType, Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { + UniqueDataMetadata referencedMetadata = dataManager.getMetadata(genericType); + Preconditions.checkNotNull(referencedMetadata, "No metadata found for referenced class " + genericType.getName()); + + String dataSchema = ValueUtils.parseValue(dataAnnotation.schema()); + String dataTable = ValueUtils.parseValue(dataAnnotation.table()); + + 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); } - return mappings; + + SQLSchema referencedSchema = Objects.requireNonNull(schemas.get(referencedMetadata.schema())); + SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(referencedMetadata.table())); + + Delete delete = field.getAnnotation(Delete.class); + DeleteStrategy deleteStrategy = delete != null ? delete.value() : DeleteStrategy.NO_ACTION; + OnDelete onDelete = deleteStrategy == DeleteStrategy.CASCADE ? OnDelete.CASCADE : OnDelete.SET_NULL; + + // unlike a Reference, this foreign key goes on the referenced table, not our table. + // Since the foreign key is on the other table, let the foreign key handle the deletion strategy instead of a trigger. + ForeignKey foreignKey = new ForeignKey(referencedSchema.getName(), referencedTable.getName(), schema.getName(), table.getName(), onDelete); + try { + parseLinksReversed(foreignKey, oneToMany.link()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error parsing @OneToMany link on field " + field.getName() + " in class " + clazz.getName() + ": " + e.getMessage(), e); + } //todo: all columns in the link must be unique in our table, and must be id columns in the referenced table. this will be enforced by H2 but check here for better errors + referencedTable.addForeignKey(foreignKey); + } + + private void parseManyToManyPersistentCollection(ManyToMany oneToMany, Class genericType, Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { + //todo: this } } diff --git a/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java b/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java new file mode 100644 index 00000000..61dd2a5f --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.util; + +public interface PersistentCollectionMetadata { +} diff --git a/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java b/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java new file mode 100644 index 00000000..e7c19f19 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java @@ -0,0 +1,43 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.ForeignKey; + +import java.util.List; +import java.util.Objects; + +public class PersistentOneToManyCollectionMetadata implements PersistentCollectionMetadata { + private final Class dataType; + private final List links; + + public PersistentOneToManyCollectionMetadata(Class dataType, List links) { + this.dataType = dataType; + this.links = links; + } + + public Class getDataType() { + return dataType; + } + + public List getLinks() { + return links; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PersistentOneToManyCollectionMetadata that = (PersistentOneToManyCollectionMetadata) o; + return Objects.equals(dataType, that.dataType) && Objects.equals(links, that.links); + } + + @Override + public int hashCode() { + return Objects.hash(dataType, links); + } + + @Override + public String toString() { + return "PersistentOneToManyCollectionMetadata[" + + "links=" + links + ']'; + } +} diff --git a/src/main/java/net/staticstudios/data/util/Relation.java b/src/main/java/net/staticstudios/data/util/Relation.java new file mode 100644 index 00000000..fb940c3b --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/Relation.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.util; + +public interface Relation { +} diff --git a/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java b/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java index f525e9c4..9f1f05d5 100644 --- a/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java +++ b/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java @@ -9,5 +9,6 @@ public record UniqueDataMetadata(Class clazz, String schema, String table, List idColumns, Map persistentValueMetadata, - Map referenceMetadata) { + Map referenceMetadata, + Map persistentCollectionMetadata) { } diff --git a/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java new file mode 100644 index 00000000..a2451fa7 --- /dev/null +++ b/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -0,0 +1,59 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.mock.user.MockUser; +import net.staticstudios.data.mock.user.MockUserFactory; +import net.staticstudios.data.mock.user.MockUserSession; +import net.staticstudios.data.mock.user.MockUserSessionFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class PersistentOneToManyCollectionTest extends DataTest { + + private MockUser mockUser; + private DataManager dataManager; + + @BeforeEach + public void setUp() { + dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + mockUser = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); + } + + @Test + public void testEmpty() { + assertTrue(mockUser.sessions.isEmpty()); + } + + @Test + public void testAdd() { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + + assertNull(session.userId.get()); + mockUser.sessions.add(session); + assertEquals(mockUser.id.get(), session.userId.get()); + assertSame(mockUser, session.user.get()); + assertEquals(1, mockUser.sessions.size()); + assertSame(session, mockUser.sessions.iterator().next()); + assertEquals("[" + session + "]", mockUser.sessions.toString()); + //todo: validate the db + } + + //todo: add more tests + //todo: test other collection types + //todo: retainall + //todo: add/remove handlers +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index 4152ec24..09f4fb93 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -102,5 +102,5 @@ public void testParse() throws Exception { assertEquals(expected.trim(), cleanedDump.toString().trim()); } - //todo: when a delete strategy is set to no action where it was previously set to cascade, the old trigger should be dropped. Add a test for this. + //todo: when a delete strategy is set to no action where it was previously set to cascade, the old trigger should be dropped. Add a test for this. moreover, what happens when we change the name of something? will the old trigger stay or what? handle this } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/src/test/java/net/staticstudios/data/mock/user/MockUser.java index e43e2d2b..ba0b5981 100644 --- a/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -46,6 +46,15 @@ public class MockUser extends UniqueData { @Column(name = "views", nullable = true) public PersistentValue views; + //todo: on delete we need to have an option to set null. No action will handle this actually. + @Delete(DeleteStrategy.NO_ACTION) + @OneToMany(link = "id=user_id") + public PersistentCollection sessions; + + //todo: support ManyToMany + + //todo: support OneToMany Collections where the data type is not a uniquedata. in this case additional info about what table and schema to use will be required, since we will have to create this table. + public int getNameUpdates() { return nameUpdates.get(); } diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUserSession.java b/src/test/java/net/staticstudios/data/mock/user/MockUserSession.java new file mode 100644 index 00000000..ebdb3b1a --- /dev/null +++ b/src/test/java/net/staticstudios/data/mock/user/MockUserSession.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.mock.user; + +import net.staticstudios.data.*; + +import java.sql.Timestamp; +import java.util.UUID; + +@Data(schema = "public", table = "user_sessions") +public class MockUserSession extends UniqueData { + @IdColumn(name = "session_id") + public PersistentValue id; + + @Column(name = "user_id", nullable = true) + public PersistentValue userId; + + @OneToOne(link = "user_id=id") + public Reference user; + + @Column(name = "timestamp") + public PersistentValue timestamp; +} From 25d5cc0cc9c3351615faee43a8487c5653f1541a Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 7 Oct 2025 14:50:48 -0400 Subject: [PATCH 25/75] sql generation for many to many --- .../PersistentOneToManyCollectionImpl.java | 58 +++- .../staticstudios/data/parse/ForeignKey.java | 7 + .../staticstudios/data/parse/SQLBuilder.java | 92 +++++- .../staticstudios/data/parse/SQLTable.java | 7 +- .../data/query/AbstractQueryBuilder.java | 2 + .../data/util/AbstractBuilder.java | 14 - .../staticstudios/data/util/UUIDUtils.java | 21 -- .../PersistentOneToManyCollectionTest.java | 287 +++++++++++++++++- .../net/staticstudios/data/SQLParseTest.java | 12 + .../data/mock/post/MockPost.java | 3 + 10 files changed, 447 insertions(+), 56 deletions(-) delete mode 100644 src/main/java/net/staticstudios/data/util/AbstractBuilder.java delete mode 100644 src/main/java/net/staticstudios/data/util/UUIDUtils.java diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java index 02f7eb44..4ba44e03 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java @@ -102,7 +102,7 @@ public boolean contains(Object o) { return false; } T data = type.cast(o); - List ids = getIds(); + Set ids = getIds(); ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); for (ColumnValuePair[] idColumns : ids) { if (Arrays.equals(idColumns, thatIdColumns)) { @@ -120,7 +120,7 @@ public boolean contains(Object o) { @Override public @NotNull Object @NotNull [] toArray() { - List ids = getIds(); + Set ids = getIds(); Object[] array = new Object[ids.size()]; int i = 0; for (ColumnValuePair[] idColumns : ids) { @@ -133,7 +133,7 @@ public boolean contains(Object o) { @SuppressWarnings("unchecked") @Override public @NotNull T1 @NotNull [] toArray(@NotNull T1 @NotNull [] a) { - List ids = getIds(); + Set ids = getIds(); if (a.length < ids.size()) { a = (T1[]) Array.newInstance(a.getClass().getComponentType(), ids.size()); } @@ -163,7 +163,7 @@ public boolean containsAll(@NotNull Collection c) { return false; } } - List ids = getIds(); + Set ids = getIds(); for (Object o : c) { T data = type.cast(o); ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); @@ -236,7 +236,36 @@ public boolean removeAll(@NotNull Collection c) { @Override public boolean retainAll(@NotNull Collection c) { - //todo: this + Set currentIds = getIds(); + Set idsToRetain = new HashSet<>(); + for (Object o : c) { + if (!type.isInstance(o)) { + continue; + } + T data = type.cast(o); + ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + idsToRetain.add(thatIdColumns); + } + + List idsToRemove = new ArrayList<>(); + for (ColumnValuePair[] idColumns : currentIds) { + boolean found = false; + for (ColumnValuePair[] retainIdColumns : idsToRetain) { + if (Arrays.equals(idColumns, retainIdColumns)) { + found = true; + break; + } + } + if (!found) { + idsToRemove.add(idColumns); + } + } + + if (!idsToRemove.isEmpty()) { + removeAll(idsToRemove); + return true; + } + return false; } @@ -336,9 +365,9 @@ private List getMyLinkingValues(UniqueDataMetadata holderMetadata, DataA return myValues; } - private List getIds() { + private Set getIds() { Preconditions.checkArgument(!holder.isDeleted(), "Cannot get entries on a deleted UniqueData instance"); - List ids = new ArrayList<>(); + Set ids = new HashSet<>(); UniqueDataMetadata holderMetadata = holder.getMetadata(); UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); @@ -390,14 +419,16 @@ private List getIds() { @Override public boolean equals(Object obj) { - //todo: this - return super.equals(obj); + if (this == obj) return true; + if (!(obj instanceof PersistentOneToManyCollectionImpl that)) return false; + return Objects.equals(holder, that.holder) && + Objects.equals(type, that.type) && + Objects.equals(getIds(), that.getIds()); } @Override public int hashCode() { - //todo: this - return super.hashCode(); + return Objects.hash(holder, type, getIds()); } @Override @@ -418,8 +449,8 @@ class IteratorImpl implements Iterator { private final List ids; private int index = 0; - public IteratorImpl(List ids) { - this.ids = ids; + public IteratorImpl(Set ids) { + this.ids = new ArrayList<>(ids); } @Override @@ -440,6 +471,7 @@ public T next() { public void remove() { Preconditions.checkState(index > 0, "next() has not been called yet"); removeAll(Collections.singletonList(ids.get(index - 1))); + ids.remove(--index); } } } diff --git a/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/src/main/java/net/staticstudios/data/parse/ForeignKey.java index f846508e..689dc9a3 100644 --- a/src/main/java/net/staticstudios/data/parse/ForeignKey.java +++ b/src/main/java/net/staticstudios/data/parse/ForeignKey.java @@ -58,6 +58,13 @@ public OnUpdate getOnUpdate() { return onUpdate; } + public String getName() { + return "fk_" + referringSchema + "_" + referringTable + "_" + + String.join("_", links.stream().map(ForeignKey.Link::columnInReferringTable).toList()) + + "_to_" + referencedSchema + "_" + referencedTable + "_" + + String.join("_", links.stream().map(ForeignKey.Link::columnInReferencedTable).toList()); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index 49af436a..c9319bc3 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -11,6 +11,10 @@ import java.lang.reflect.Field; import java.util.*; +/** + * This class is responsible for parsing annotations and/or fields in all classes which extend {@link UniqueData}. + * This class then converts that information into SQL metadata, which is later used to generate DDL statements for both Postgres and H2. + */ public class SQLBuilder { public static final String INDENT = " "; private static final Logger logger = LoggerFactory.getLogger(SQLBuilder.class); @@ -173,9 +177,7 @@ private List getDefs(Collection schemas) { if (foreignKey == null) { continue; } - String fKeyName = "fk_" + foreignKey.getReferringSchema() + "_" + foreignKey.getReferringTable() + "_" - + String.join("_", foreignKey.getLinkingColumns().stream().map(ForeignKey.Link::columnInReferringTable).toList()) - + "_to_" + foreignKey.getReferencedSchema() + "_" + foreignKey.getReferencedTable() + "_" + String.join("_", foreignKey.getLinkingColumns().stream().map(ForeignKey.Link::columnInReferencedTable).toList()); + String fKeyName = foreignKey.getName(); StringBuilder sb = new StringBuilder(); sb.append("ALTER TABLE \"").append(foreignKey.getReferringSchema()).append("\".\"").append(foreignKey.getReferringTable()).append("\" "); sb.append("ADD CONSTRAINT IF NOT EXISTS ").append(fKeyName).append(" "); @@ -543,6 +545,88 @@ private void parseOneToManyPersistentCollection(OneToMany oneToMany, Class genericType, Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { - //todo: this + UniqueDataMetadata referencedMetadata = dataManager.getMetadata(genericType); + Preconditions.checkNotNull(referencedMetadata, "No metadata found for referenced class " + genericType.getName()); + + String dataSchema = ValueUtils.parseValue(dataAnnotation.schema()); + String dataTable = ValueUtils.parseValue(dataAnnotation.table()); + + 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); + } + + SQLSchema referencedSchema = Objects.requireNonNull(schemas.get(referencedMetadata.schema())); + SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(referencedMetadata.table())); + + String joinTableSchemaName = ValueUtils.parseValue(oneToMany.joinTableSchema()); + String joinTableName = ValueUtils.parseValue(oneToMany.joinTable()); + + if (joinTableSchemaName.isEmpty()) { + joinTableSchemaName = dataSchema; + } + if (joinTableName.isEmpty()) { + joinTableName = dataTable + "_" + referencedMetadata.table(); + } + + List joinTableToDataTableLinks = new ArrayList<>(); + List joinTableToReferencedTableLinks = new ArrayList<>(); + + String dataTableColumnPrefix = dataTable; + String referencedTableColumnPrefix = referencedTable.getName(); + if (referencedTableColumnPrefix.equals(dataTableColumnPrefix)) { + referencedTableColumnPrefix = referencedTableColumnPrefix + "_ref"; + } + try { + for (ForeignKey.Link link : parseLinks(oneToMany.link())) { + String columnInDataTable = link.columnInReferringTable(); + String dataColumnInJoinTable = dataTableColumnPrefix + "_" + columnInDataTable; + String columnInReferencedTable = link.columnInReferencedTable(); + String referencedColumnInJoinTable = referencedTableColumnPrefix + "_" + columnInReferencedTable; + + joinTableToDataTableLinks.add(new ForeignKey.Link(columnInDataTable, dataColumnInJoinTable)); + joinTableToReferencedTableLinks.add(new ForeignKey.Link(columnInReferencedTable, referencedColumnInJoinTable)); + } + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error parsing @ManyToMany link on field " + field.getName() + " in class " + clazz.getName() + ": " + e.getMessage(), e); + } + + SQLSchema joinSchema = schemas.computeIfAbsent(joinTableSchemaName, SQLSchema::new); + SQLTable joinTable = joinSchema.getTable(joinTableName); + if (joinTable == null) { + List joinTableIdColumns = new ArrayList<>(); + for (ForeignKey.Link dataLink : joinTableToDataTableLinks) { + ColumnMetadata columnMetadata = metadata.idColumns().stream() + .filter(c -> c.name().equals(dataLink.columnInReferencedTable())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Column not found in data table! " + dataLink.columnInReferringTable())); + joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, dataTableColumnPrefix + "_" + columnMetadata.name(), columnMetadata.type(), false, false, "")); + } + for (ForeignKey.Link referencedLink : joinTableToReferencedTableLinks) { + ColumnMetadata columnMetadata = referencedMetadata.idColumns().stream() + .filter(c -> c.name().equals(referencedLink.columnInReferencedTable())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Column not found in referenced table! " + referencedLink.columnInReferringTable())); + joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, referencedTableColumnPrefix + "_" + columnMetadata.name(), columnMetadata.type(), false, false, "")); + } + joinTable = new SQLTable(joinSchema, joinTableName, joinTableIdColumns); + joinSchema.addTable(joinTable); + } + + Delete delete = field.getAnnotation(Delete.class); + DeleteStrategy deleteStrategy = delete != null ? delete.value() : DeleteStrategy.NO_ACTION; + OnDelete onDelete = OnDelete.CASCADE; + //todo: deletion strategy in this case is different than in the one to many case. + // it should always cascade on the join table, but depending on the delete strategy, we may or may not delete the referenced data. + + ForeignKey foreignKeyJoinToDataTable = new ForeignKey(joinSchema.getName(), joinTable.getName(), schema.getName(), table.getName(), onDelete); + ForeignKey foreignKeyJoinToReferenceTable = new ForeignKey(joinSchema.getName(), joinTable.getName(), referencedSchema.getName(), referencedTable.getName(), onDelete); + joinTableToDataTableLinks.forEach(foreignKeyJoinToDataTable::addLink); + joinTableToReferencedTableLinks.forEach(foreignKeyJoinToReferenceTable::addLink); + joinTable.addForeignKey(foreignKeyJoinToDataTable); + joinTable.addForeignKey(foreignKeyJoinToReferenceTable); } } diff --git a/src/main/java/net/staticstudios/data/parse/SQLTable.java b/src/main/java/net/staticstudios/data/parse/SQLTable.java index a451ed1b..c6c62dc0 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLTable.java +++ b/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -42,7 +42,12 @@ public Set getColumns() { public void addForeignKey(ForeignKey foreignKey) { Preconditions.checkNotNull(foreignKey, "Foreign key cannot be null"); ForeignKey existingKey = foreignKeys.stream() - .filter(fk -> fk.getReferencedSchema().equals(foreignKey.getReferencedSchema()) && fk.getReferencedTable().equals(foreignKey.getReferencedTable()) && fk.getReferringSchema().equals(foreignKey.getReferringSchema()) && fk.getReferringTable().equals(foreignKey.getReferringTable())) + .filter(fk -> fk.getReferencedSchema().equals(foreignKey.getReferencedSchema()) && + fk.getReferencedTable().equals(foreignKey.getReferencedTable()) && + fk.getReferringSchema().equals(foreignKey.getReferringSchema()) && + fk.getReferringTable().equals(foreignKey.getReferringTable()) && + fk.getName().equals(foreignKey.getName()) + ) .findFirst() .orElse(null); if (existingKey != null && !Objects.equals(existingKey, foreignKey)) { diff --git a/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java b/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java index 67ef0c8c..4add26a6 100644 --- a/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java +++ b/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java @@ -35,6 +35,8 @@ protected AbstractQueryBuilder(DataManager dataManager, Class type) { this.dataManager = dataManager; this.type = type; } + //todo: there needs to be a way to support WHERE ( x or Y) ... + // right now you can only do WHERE x AND (y OR z) ... the first clause cannot be a parenthesis clause public Q and() { state = State.AND; diff --git a/src/main/java/net/staticstudios/data/util/AbstractBuilder.java b/src/main/java/net/staticstudios/data/util/AbstractBuilder.java deleted file mode 100644 index 66bd51bf..00000000 --- a/src/main/java/net/staticstudios/data/util/AbstractBuilder.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.staticstudios.data.util; - -import net.staticstudios.data.UniqueData; - -public class AbstractBuilder { -// private final DataManager dataManager; -// private final Class holderClass; -// private final Map toInsert; -// -// protected void set(String schema, String table, String name, Object value) { -// String key = schema + "." + table + "." + name; -// toInsert.put(key, value); -// } -} diff --git a/src/main/java/net/staticstudios/data/util/UUIDUtils.java b/src/main/java/net/staticstudios/data/util/UUIDUtils.java deleted file mode 100644 index 2eacd5c5..00000000 --- a/src/main/java/net/staticstudios/data/util/UUIDUtils.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.staticstudios.data.util; - -import java.nio.ByteBuffer; -import java.util.UUID; - -public class UUIDUtils { - - public static byte[] toBytes(UUID uuid) { - ByteBuffer bb = ByteBuffer.allocate(16) - .putLong(uuid.getMostSignificantBits()) - .putLong(uuid.getLeastSignificantBits()); - return bb.array(); - } - - public static UUID fromBytes(byte[] bytes) { - ByteBuffer bb = ByteBuffer.wrap(bytes); - long mostSigBits = bb.getLong(); - long leastSigBits = bb.getLong(); - return new UUID(mostSigBits, leastSigBits); - } -} diff --git a/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java index a2451fa7..f68deca2 100644 --- a/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java +++ b/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -1,6 +1,7 @@ package net.staticstudios.data; import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.misc.TestUtils; import net.staticstudios.data.mock.user.MockUser; import net.staticstudios.data.mock.user.MockUserFactory; import net.staticstudios.data.mock.user.MockUserSession; @@ -8,8 +9,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.Timestamp; import java.time.Instant; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; @@ -49,11 +56,285 @@ public void testAdd() { assertEquals(1, mockUser.sessions.size()); assertSame(session, mockUser.sessions.iterator().next()); assertEquals("[" + session + "]", mockUser.sessions.toString()); - //todo: validate the db + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(1, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testRemove() { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session); + waitForDataPropagation(); + assertTrue(mockUser.sessions.remove(session)); + assertFalse(mockUser.sessions.contains(session)); + assertEquals(0, mockUser.sessions.size()); + waitForDataPropagation(); + Connection pgConnection = getConnection(); + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(0, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testClear() { + MockUserSession session1 = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + MockUserSession session2 = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session1); + mockUser.sessions.add(session2); + waitForDataPropagation(); + mockUser.sessions.clear(); + assertTrue(mockUser.sessions.isEmpty()); + waitForDataPropagation(); + Connection pgConnection = getConnection(); + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(0, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testContains() { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + assertFalse(mockUser.sessions.contains(session)); + mockUser.sessions.add(session); + assertTrue(mockUser.sessions.contains(session)); + mockUser.sessions.remove(session); + assertFalse(mockUser.sessions.contains(session)); + } + + @Test + public void testSizeAndIsEmpty() { + assertTrue(mockUser.sessions.isEmpty()); + assertEquals(0, mockUser.sessions.size()); + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session); + assertFalse(mockUser.sessions.isEmpty()); + assertEquals(1, mockUser.sessions.size()); + mockUser.sessions.remove(session); + assertTrue(mockUser.sessions.isEmpty()); + assertEquals(0, mockUser.sessions.size()); + } + + @Test + public void testIterator() { + List sessions = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session); + sessions.add(session); + } + Iterator iterator = mockUser.sessions.iterator(); + int count = 0; + while (iterator.hasNext()) { + assertTrue(sessions.contains(iterator.next())); + count++; + } + assertEquals(3, count); + } + + @Test + public void testIteratorRemove() { + for (int i = 0; i < 3; i++) { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session); + } + Iterator iterator = mockUser.sessions.iterator(); + MockUserSession toRemove = iterator.next(); + iterator.remove(); + assertFalse(mockUser.sessions.contains(toRemove)); + assertEquals(2, mockUser.sessions.size()); + waitForDataPropagation(); + Connection pgConnection = getConnection(); + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(2, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testToArray() { + List sessions = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session); + sessions.add(session); + } + Object[] arr = mockUser.sessions.toArray(); + assertEquals(2, arr.length); + for (Object o : arr) { + assertTrue(sessions.contains(o)); + } + MockUserSession[] typedArr = mockUser.sessions.toArray(new MockUserSession[0]); + assertEquals(2, typedArr.length); + for (MockUserSession s : typedArr) { + assertTrue(sessions.contains(s)); + } + } + + @Test + public void testToString() { + mockUser.sessions.clear(); + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session); + String expected = String.format("[%s]", session); + assertEquals(expected, mockUser.sessions.toString()); + } + + @Test + public void testAddAll() { + List sessions = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + sessions.add(session); + } + assertTrue(mockUser.sessions.addAll(sessions)); + assertEquals(3, mockUser.sessions.size()); + for (MockUserSession session : sessions) { + assertTrue(mockUser.sessions.contains(session)); + } + waitForDataPropagation(); + Connection pgConnection = getConnection(); + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(3, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testRemoveAll() { + List sessions = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session); + sessions.add(session); + } + waitForDataPropagation(); + assertTrue(mockUser.sessions.removeAll(sessions)); + assertEquals(0, mockUser.sessions.size()); + for (MockUserSession session : sessions) { + assertFalse(mockUser.sessions.contains(session)); + } + waitForDataPropagation(); + Connection pgConnection = getConnection(); + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(0, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testRetainAll() { + List sessions = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session); + sessions.add(session); + } + List retain = List.of(sessions.get(0)); + assertTrue(mockUser.sessions.retainAll(retain)); + assertEquals(1, mockUser.sessions.size()); + assertTrue(mockUser.sessions.contains(sessions.get(0))); + assertFalse(mockUser.sessions.contains(sessions.get(1))); + assertFalse(mockUser.sessions.contains(sessions.get(2))); + waitForDataPropagation(); + Connection pgConnection = getConnection(); + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(1, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testToArrayTyped() { + List sessions = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.SYNC); + mockUser.sessions.add(session); + sessions.add(session); + } + MockUserSession[] arr = new MockUserSession[2]; + MockUserSession[] result = mockUser.sessions.toArray(arr); + assertEquals(2, result.length); + for (MockUserSession s : result) { + assertTrue(sessions.contains(s)); + } } - //todo: add more tests //todo: test other collection types - //todo: retainall //todo: add/remove handlers } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index 09f4fb93..ee5c6c7b 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -86,19 +86,31 @@ public void testParse() throws Exception { metadata_id integer NOT NULL, flag boolean NOT NULL ); + CREATE TABLE social_media.posts_related ( + posts_post_id integer NOT NULL, + posts_ref_post_id integer NOT NULL + ); ALTER TABLE ONLY social_media.posts_interactions ADD CONSTRAINT posts_interactions_pkey PRIMARY KEY (post_id); ALTER TABLE ONLY social_media.posts_metadata ADD CONSTRAINT posts_metadata_pkey PRIMARY KEY (metadata_id); ALTER TABLE ONLY social_media.posts ADD CONSTRAINT posts_pkey PRIMARY KEY (post_id); + ALTER TABLE ONLY social_media.posts_related + ADD CONSTRAINT posts_related_pkey PRIMARY KEY (posts_post_id, posts_ref_post_id); CREATE INDEX idx_social_media_posts_text_content ON social_media.posts USING btree (text_content); ALTER TABLE ONLY social_media.posts ADD CONSTRAINT fk_social_media_posts_post_id_to_social_media_posts_interaction FOREIGN KEY (post_id) REFERENCES social_media.posts_interactions(post_id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY social_media.posts ADD CONSTRAINT fk_social_media_posts_post_id_to_social_media_posts_metadata_me FOREIGN KEY (post_id) REFERENCES social_media.posts_metadata(metadata_id) ON UPDATE CASCADE ON DELETE SET NULL; + ALTER TABLE ONLY social_media.posts_related + ADD CONSTRAINT fk_social_media_posts_related_posts_post_id_to_social_media_pos FOREIGN KEY (posts_post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; + ALTER TABLE ONLY social_media.posts_related + ADD CONSTRAINT fk_social_media_posts_related_posts_ref_post_id_to_social_media FOREIGN KEY (posts_ref_post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; """; + //todo: fk names are too long and are getting cut off, shorten them + assertEquals(expected.trim(), cleanedDump.toString().trim()); } diff --git a/src/test/java/net/staticstudios/data/mock/post/MockPost.java b/src/test/java/net/staticstudios/data/mock/post/MockPost.java index ce7a3b07..8e95c618 100644 --- a/src/test/java/net/staticstudios/data/mock/post/MockPost.java +++ b/src/test/java/net/staticstudios/data/mock/post/MockPost.java @@ -20,4 +20,7 @@ public class MockPost extends UniqueData { @DefaultValue("0") @ForeignColumn(name = "interactions", table = "${POST_TABLE}_interactions", link = "${POST_ID_COLUMN}=post_id") public PersistentValue interactions; + + @ManyToMany(link = "${POST_ID_COLUMN}=${POST_ID_COLUMN}", joinTable = "${POST_TABLE}_related") + public PersistentCollection relatedPosts; } From 8d5b325864e494abc4d3ce740c095d14eb6484d1 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 7 Oct 2025 14:55:34 -0400 Subject: [PATCH 26/75] shorten fk names --- .../java/net/staticstudios/data/parse/ForeignKey.java | 6 ++++-- src/test/java/net/staticstudios/data/SQLParseTest.java | 10 ++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/src/main/java/net/staticstudios/data/parse/ForeignKey.java index 689dc9a3..eea2b51a 100644 --- a/src/main/java/net/staticstudios/data/parse/ForeignKey.java +++ b/src/main/java/net/staticstudios/data/parse/ForeignKey.java @@ -59,9 +59,11 @@ public OnUpdate getOnUpdate() { } public String getName() { - return "fk_" + referringSchema + "_" + referringTable + "_" + return "fk_" +// + referringSchema + "_" + referringTable + "_" + String.join("_", links.stream().map(ForeignKey.Link::columnInReferringTable).toList()) - + "_to_" + referencedSchema + "_" + referencedTable + "_" + + "_to_" +// + referencedSchema + "_" + referencedTable + "_" + String.join("_", links.stream().map(ForeignKey.Link::columnInReferencedTable).toList()); } diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index ee5c6c7b..92dae1ab 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -100,17 +100,15 @@ public void testParse() throws Exception { ADD CONSTRAINT posts_related_pkey PRIMARY KEY (posts_post_id, posts_ref_post_id); CREATE INDEX idx_social_media_posts_text_content ON social_media.posts USING btree (text_content); ALTER TABLE ONLY social_media.posts - ADD CONSTRAINT fk_social_media_posts_post_id_to_social_media_posts_interaction FOREIGN KEY (post_id) REFERENCES social_media.posts_interactions(post_id) ON UPDATE CASCADE ON DELETE CASCADE; + ADD CONSTRAINT fk_post_id_to_metadata_id FOREIGN KEY (post_id) REFERENCES social_media.posts_metadata(metadata_id) ON UPDATE CASCADE ON DELETE SET NULL; ALTER TABLE ONLY social_media.posts - ADD CONSTRAINT fk_social_media_posts_post_id_to_social_media_posts_metadata_me FOREIGN KEY (post_id) REFERENCES social_media.posts_metadata(metadata_id) ON UPDATE CASCADE ON DELETE SET NULL; + ADD CONSTRAINT fk_post_id_to_post_id FOREIGN KEY (post_id) REFERENCES social_media.posts_interactions(post_id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY social_media.posts_related - ADD CONSTRAINT fk_social_media_posts_related_posts_post_id_to_social_media_pos FOREIGN KEY (posts_post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; + ADD CONSTRAINT fk_posts_post_id_to_post_id FOREIGN KEY (posts_post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY social_media.posts_related - ADD CONSTRAINT fk_social_media_posts_related_posts_ref_post_id_to_social_media FOREIGN KEY (posts_ref_post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; + ADD CONSTRAINT fk_posts_ref_post_id_to_post_id FOREIGN KEY (posts_ref_post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; """; - //todo: fk names are too long and are getting cut off, shorten them - assertEquals(expected.trim(), cleanedDump.toString().trim()); } From 26b3f6b069c8098358e52a7ab9edafbc5a57a2fc Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 7 Oct 2025 18:59:47 -0400 Subject: [PATCH 27/75] update readme --- README.md | 209 ++++++++++++++++-- .../staticstudios/data/DeleteStrategy.java | 8 +- .../staticstudios/data/parse/SQLBuilder.java | 1 + .../data/mock/user/MockUser.java | 2 +- 4 files changed, 196 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 45a49f84..7e42106e 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,196 @@ -# What is static-data -`static-data` is an ORM built for a specific type of application. Origninally created for distributed Minecraft servers, this ORM avoids blocking an application's main thread when doing read or writes from a remote datasource. This is accomplished by keeping an in memory copy of the datasource, reading from there, and asynchronously disbatching writes to the datasource. The whole database is **not** kept in memory, only relevant tables are. This can use significant amounts of memory on large tables. +# static-data -## Built for distributed applications -What makes `static-data` special is that the in-memory cache is updated whenever the datasource is updated. This means that when one application instance makes a change, all other instances will update their cache. This avoids reading stale data. The various instaces do not have to contain the exact same data classes either, `static-data` receives updates based off of updated cells in the database. So if a cell is being tracked *somewhere*, the update will be received. +`static-data` is an ORM (Object-Relational Mapping) library primarily designed for Minecraft servers. It provides a +robust solution for managing database operations in distributed applications while avoiding blocking the main thread. -### PostgreSQL only -This ORM only supports PostgreSQL. The reason is that `static-data` makes use of PostgreSQL's `LISTEN / NOTIFY` commands to recieve updates rather than add an additional layer between the application and the database. +## Key Features -`static-data` was designed to interop with other ORM's such as Hibernate. Not nessicarily within the same application, but if a secondary applicaition (that uses the same tables) such as a SpringBoot API is created, `static-data` does not have to be used there. Updates made with Hibernate will still be propogated to all application instances using `static-data`, since this is done on the database level. +### In-Memory Database with PostgreSQL Backend -### Redis support -`static-data` supports using redis as a data source for simple values. Simple just means no collections/references. "Primative" types and complex types with custom `ValueSerializer`s are supported here. +`static-data` maintains a copy of the source PostgreSQL database in memory as an H2 database. This architecture: -## Data wrappers -- `PersistentValue.of(...)`: reference a column in a data object's row -- `PersistentValue.foreign(...)`: reference a column in a different table -- `CachedValue.of(...)`: reference an entry in redis (used when persistence doesn't matter) -- `Reference.of(...)`: reference another data object (one-to-one relationship) -- `PersistentCollection.of(...)`: represents a one-to-many relationship of simple data types such as Integer, Boolean, etc.. (like a `PersistentValue`, but one-to-many) -- `PersistentCollection.oneToMany(...)`: represents a one-to-many relationship of other data objects (like a `Reference`, but one-to-many) -- `PersistentCollection.manyToMany(...)`: represents a many-to-many relationship of other data objects, with the use of a junction table +- Prevents blocking the main thread during database operations +- Provides fast read access to data +- Asynchronously dispatches writes to the source database -## Data types -Any class can be used as a data type, provided it's a "Primative" or a `ValueSerializer` has been registered for it. "Primitive" types are basic types which are supported in PostgreSQL. `ValueSerializers` must convert to and from complex types to a "Primative". Current "Primitive" types include: `String`, `Character`, `Byte`, `Short`, `Integer`, `Long`, `Float`, `Double`, `Boolean`, `UUID`, `Timestamp` and `byte[]`. `null` values are allowed for some "Primative" types. Specifically, `null` values are allowed for `String`, `UUID`, `Timestamp`, and `byte[]`. To avoid autoboxing/unboxing issues, wrappers for java primatives cannot be null. +### Built for Distributed Applications -# Current limitations -Currently, `static-data` assumes that any column marked as an id column will not have its value changed. It should only ever be `null` when the row/data object doesn't exist. Changing this value will break things in many ways. +What makes `static-data` special is its ability to keep the in-memory cache updated whenever the source database +changes: +- When one application instance makes a change, all other instances update their cache +- Prevents reading stale data in distributed environments +- Different application instances can track different subsets of data + +### Comprehensive Relational Model Support + +`static-data` supports a wide range of relational database features: + +- One-to-one relationships +- One-to-many relationships +- Many-to-many relationships +- Foreign key constraints +- Custom indexes +- Default values + +### PostgreSQL Integration + +This ORM exclusively supports PostgreSQL as its source database: + +- Uses PostgreSQL's `LISTEN / NOTIFY` commands to receive updates +- Avoids adding additional layers between the application and the database +- Interoperates with other ORMs (like Hibernate) that might be used in other parts of your ecosystem + +### Redis Support + +`static-data` also supports using Redis as a data source for simple values: + +- Works with primitive types and complex types with custom `ValueSerializer`s +- Ideal for when persistence isn't a primary concern + +## Annotations and Data Wrappers + +`static-data` v3 uses a combination of annotations and wrapper classes to define data models: + +### Annotations + +- `@Data(schema = "...", table = "...")`: Defines the schema and table for a data class +- `@IdColumn(name = "...")`: Marks a field as the ID column +- `@Column(name = "...", nullable = true/false, index = true/false)`: Maps a field to a database column +- `@ForeignColumn(name = "...", table = "...", link = "...")`: Maps a field to a column in a different table +- `@OneToOne(link = "...")`: Defines a one-to-one relationship +- `@OneToMany(link = "...")`: Defines a one-to-many relationship +- `@ManyToMany(link = "...", joinTable = "...")`: Defines a many-to-many relationship +- `@DefaultValue("...")`: Sets a default value for a column +- `@Insert(InsertStrategy.PREFER_EXISTING/OVERWRITE_EXISTING)`: Controls insert behavior +- `@Delete(DeleteStrategy.CASCADE/NO_ACTION)`: Controls delete behavior +- `@UpdateInterval(milliseconds)`: Sets an interval for batching updates + +### Data Wrappers + +- `PersistentValue`: References a column in a data object's row +- `Reference`: References another data object (one-to-one relationship) +- `PersistentCollection`: Represents a collection relationship (one-to-many or many-to-many) + +### Compile-time Generated Classes + +For each data class, `static-data` generates two helper classes at compile time: + +1. **Factory**: Provides a builder pattern for creating and inserting instances + ``` + // Example: Creating and inserting a new user + User user = UserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("John Doe") + .age(30) + .insert(InsertMode.SYNC); + ``` + +2. **Query Builder**: Provides a fluent API for querying instances + ``` + // Example: Finding users by criteria + List users = UserQuery.where(dataManager) + .nameIsLike("John%") + .and() + .ageIsGreaterThan(25) + .orderByName(Order.ASC) + .limit(10) + .list(); + ``` + +Note: The query builder can use the global singleton DataManager instance if not explicitly provided. + +## Data Types + +Any class can be used as a data type, provided it's a "Primitive" or has a registered `ValueSerializer`. "Primitive" +types are basic types supported in PostgreSQL: + +- `String`, `Integer`, `Long`, `Float`, `Double`, `Boolean`, `UUID`, `Timestamp`, and `byte[]` + +Nullability is controlled through the `nullable` parameter in the `@Column` and `@ForeignColumn` annotations. For +example: + +``` +@Column(name = "age", nullable = true) +public PersistentValue age; +``` + +This flexibility allows all primitive types to be nullable when needed, while still maintaining type safety. + +## Usage Example + +``` +@Data(schema = "social_media", table = "posts") +public class Post extends UniqueData { + @IdColumn(name = "post_id") + public PersistentValue id; + + @OneToOne(link = "post_id=metadata_id") + public Reference metadata; + + @Column(name = "text_content", index = true) + public PersistentValue textContent; + + @DefaultValue("0") + @Column(name = "likes") + public PersistentValue likes; + + @ManyToMany(link = "post_id=post_id", joinTable = "posts_related") + public PersistentCollection relatedPosts; +} +``` + +## Current Limitations + +- Memory usage: While the whole database is not kept in memory, only relevant tables are, this can still use significant + amounts of memory for large tables. + +## Future Developments + +- **PostgreSQL-only mode**: A future update will add support for a PostgreSQL-only mode where `static-data` will act as + a traditional ORM without using an in-memory cache. This will provide better performance for applications that don't + need the caching benefits and will reduce memory usage. + +- **Disk-based cache**: Plans are in place to add support for a disk-based cache option (using H2 on disk instead of in + memory) to reduce memory consumption while still maintaining the benefits of the caching architecture. + +## Getting Started + +[//]: # (TODO: this section is incorrect since the impl isnt finished. update this later) + +1. Configure your data source: + +``` +DataSourceConfig config = new DataSourceConfig.Builder() + .setPostgresUrl("jdbc:postgresql://localhost:5432/mydatabase") + .setPostgresUsername("username") + .setPostgresPassword("password") + .build(); +``` + +2. Initialize the DataManager: + +``` +DataManager dataManager = new DataManager(config); +``` + +3. Define your data models using annotations and data wrappers. + +4. Load your models: + +``` +dataManager.load(Post.class, User.class); +``` + +5. Query and manipulate data: + +``` +// Get a post by ID +Post post = dataManager.getInstance(Post.class, new ColumnValuePair("post_id", 1)); + +// Update a value +post.likes.set(post.likes.get() + 1); + +// Access related objects +PostMetadata metadata = post.metadata.get(); +``` diff --git a/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java index f47b0032..365e82e2 100644 --- a/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java +++ b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java @@ -3,12 +3,16 @@ public enum DeleteStrategy { /** * When the parent data is deleted, delete this data as well. + * For all data types, this means that the referenced data will be deleted when the parent data is deleted. */ CASCADE, /** + * In the context of a persistent value or a reference:
* Do nothing when the parent data is deleted. - * For a one-to-many collections, this will unlink the data by setting the foreign key to null. - * For a many-to-many collection, this will remove the entries in the join table. + *

+ * In the context of a collection:
+ * For a one-to-many collections, this will unlink the data by setting the foreign key to null.
+ * For a many-to-many collection, this will remove the entries in the join table. */ NO_ACTION } diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index c9319bc3..bad04956 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -621,6 +621,7 @@ private void parseManyToManyPersistentCollection(ManyToMany oneToMany, Class id = PersistentValue.of(this, UUID.class); From 095217b73f815b2410c788f2662efa2425088cd2 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 7 Oct 2025 19:30:28 -0400 Subject: [PATCH 28/75] normalize string --- src/test/java/net/staticstudios/data/SQLParseTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/src/test/java/net/staticstudios/data/SQLParseTest.java index 92dae1ab..1695bb91 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -33,6 +33,10 @@ public String getEnv(String name) { }; } + private static String normalize(String str) { + return str.replace("\r\n", "\n").trim(); + } + @Test public void testParse() throws Exception { DataManager dm = getMockEnvironments().getFirst().dataManager(); @@ -109,7 +113,7 @@ public void testParse() throws Exception { ADD CONSTRAINT fk_posts_ref_post_id_to_post_id FOREIGN KEY (posts_ref_post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; """; - assertEquals(expected.trim(), cleanedDump.toString().trim()); + assertEquals(normalize(expected), normalize(cleanedDump.toString())); } //todo: when a delete strategy is set to no action where it was previously set to cascade, the old trigger should be dropped. Add a test for this. moreover, what happens when we change the name of something? will the old trigger stay or what? handle this From cc9047b4eb167f9d82571a70be0b151415dd58cd Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 7 Oct 2025 19:37:36 -0400 Subject: [PATCH 29/75] change workflows/build.yml --- .github/workflows/build.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e3a8dfe..11f4c5b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,8 +20,11 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 - - name: Grant execute permission for Gradle Wrapper + - name: Change Gradle Permissions run: chmod +x ./gradlew - - name: Build with Gradle Wrapper - run: ./gradlew build -PStaticStudiosUsername=github -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} \ No newline at end of file + - name: Build + run: ./gradlew build -PStaticStudiosUsername=github -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} -x test + + - name: Test with Gradle Wrapper + run: ./gradlew test -PStaticStudiosUsername=github -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} \ No newline at end of file From acbb2b4bd56eef9d9bf766bde758192a7bc773c3 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 7 Oct 2025 19:54:20 -0400 Subject: [PATCH 30/75] update workflows --- .github/workflows/build.yml | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 11f4c5b8..1e1bcd4e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,5 @@ name: Test build + on: pull_request: branches: [ "master" ] @@ -6,25 +7,44 @@ on: jobs: build: runs-on: ubuntu-22.04 + steps: - name: Checkout Repository uses: actions/checkout@v4 + - name: Validate Gradle Wrapper uses: gradle/actions/wrapper-validation@v3 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: 21 distribution: 'temurin' - cache: 'gradle' + cache: gradle + - name: Setup Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 - name: Change Gradle Permissions run: chmod +x ./gradlew - - name: Build - run: ./gradlew build -PStaticStudiosUsername=github -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} -x test + - name: Build (skip tests) + run: ./gradlew build -x test \ + -PStaticStudiosUsername=github \ + -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} + + - name: Run Tests + run: ./gradlew test --info --stacktrace \ + -PStaticStudiosUsername=github \ + -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} | tee gradle-test.log + continue-on-error: true - - name: Test with Gradle Wrapper - run: ./gradlew test -PStaticStudiosUsername=github -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} \ No newline at end of file + - name: Upload test logs and reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: gradle-test-artifacts + path: | + gradle-test.log + build/reports/tests/test/ + build/test-results/test/ \ No newline at end of file From d11883ed91623a5aab6fe4461964f011271fa464 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 7 Oct 2025 20:17:34 -0400 Subject: [PATCH 31/75] workflow --- .github/workflows/build.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e1bcd4e..b0cc3916 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,14 +29,10 @@ jobs: run: chmod +x ./gradlew - name: Build (skip tests) - run: ./gradlew build -x test \ - -PStaticStudiosUsername=github \ - -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} + run: ./gradlew build -x test -PStaticStudiosUsername=github -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} - name: Run Tests - run: ./gradlew test --info --stacktrace \ - -PStaticStudiosUsername=github \ - -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} | tee gradle-test.log + run: ./gradlew test --info --stacktrace -PStaticStudiosUsername=github -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} | tee gradle-test.log continue-on-error: true - name: Upload test logs and reports From 101964931c6090dbcf73ca81383f1432aceee676 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 7 Oct 2025 20:24:21 -0400 Subject: [PATCH 32/75] dont blindly succeed --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0cc3916..7a9348ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,4 +43,8 @@ jobs: path: | gradle-test.log build/reports/tests/test/ - build/test-results/test/ \ No newline at end of file + build/test-results/test/ + + - name: Fail if tests failed + if: steps.test.outcome != 'success' + run: exit 1 \ No newline at end of file From edd0de6ddad9f231ee56ae9f027b162d4d211a65 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Thu, 9 Oct 2025 15:12:15 -0400 Subject: [PATCH 33/75] update readme --- README.md | 264 ++++++++++++------ .../PersistentOneToManyCollectionTest.java | 1 + 2 files changed, 184 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 7e42106e..967dbad7 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,31 @@ # static-data -`static-data` is an ORM (Object-Relational Mapping) library primarily designed for Minecraft servers. It provides a +`static-data` is an ORM (Object-Relational Mapping) library originally designed for Minecraft servers. It provides a robust solution for managing database operations in distributed applications while avoiding blocking the main thread. +This is what makes it different from other ORMs, read and write speed is the main focus. ## Key Features ### In-Memory Database with PostgreSQL Backend -`static-data` maintains a copy of the source PostgreSQL database in memory as an H2 database. This architecture: +`static-data` maintains a copy of relevant source tables in memory. +Only the tables that are needed by the application are kept in memory, not the entire database. +If two distinct applications use `static-data` with the same data source, their in-memory +caches will differ based on the tables they use. This design offers several advantages: -- Prevents blocking the main thread during database operations -- Provides fast read access to data -- Asynchronously dispatches writes to the source database +- Prevents blocking the current thread during database operations. I/O operations are preformed asynchronously. +- Provides instant reads and writes. These operations are preformed on the embedded in-memory database. +- The source database is updated in the background, using a FIFO queue to dispatch updates. ### Built for Distributed Applications What makes `static-data` special is its ability to keep the in-memory cache updated whenever the source database changes: -- When one application instance makes a change, all other instances update their cache -- Prevents reading stale data in distributed environments -- Different application instances can track different subsets of data +- When one application instance makes a change, all other instances update their cache quickly. (The delay comes from + the latency from the application to the database) +- Prevents reading stale data in distributed environments. +- Simple developer API, `static-data` handles the complexity of keeping caches in sync. ### Comprehensive Relational Model Support @@ -37,44 +42,66 @@ changes: This ORM exclusively supports PostgreSQL as its source database: -- Uses PostgreSQL's `LISTEN / NOTIFY` commands to receive updates -- Avoids adding additional layers between the application and the database -- Interoperates with other ORMs (like Hibernate) that might be used in other parts of your ecosystem +- Uses PostgreSQL's `LISTEN / NOTIFY` commands to receive updates. This reduces complexity since there is no need to for + an additional pub/sub service. +- Interoperates with other ORMs (like Hibernate) that might be used in other parts of your ecosystem. Whenever a change + is made to the database, `static-data` will receive a notification and update its cache accordingly, there's no need + to change other applications using the same datasource. ### Redis Support `static-data` also supports using Redis as a data source for simple values: -- Works with primitive types and complex types with custom `ValueSerializer`s -- Ideal for when persistence isn't a primary concern +- Works with primitive types and complex types with custom `ValueSerializer`s. +- Ideal for when persistence isn't a primary concern. ## Annotations and Data Wrappers -`static-data` v3 uses a combination of annotations and wrapper classes to define data models: +`static-data` v3 uses a combination of annotations and wrapper classes to define data models. +The annotations are primarily used for schema definition, while the wrapper classes are used to access and manipulate +data. There is no concept of "updating" a piece of data after a change is made. +Once a data wrapper's set (or other mutating method) is called, the change is immediately reflected in the +in-memory database and queued for writing to the source database. +The goal is to make the developer experience as seamless as possible. ### Annotations -- `@Data(schema = "...", table = "...")`: Defines the schema and table for a data class -- `@IdColumn(name = "...")`: Marks a field as the ID column -- `@Column(name = "...", nullable = true/false, index = true/false)`: Maps a field to a database column -- `@ForeignColumn(name = "...", table = "...", link = "...")`: Maps a field to a column in a different table -- `@OneToOne(link = "...")`: Defines a one-to-one relationship -- `@OneToMany(link = "...")`: Defines a one-to-many relationship -- `@ManyToMany(link = "...", joinTable = "...")`: Defines a many-to-many relationship -- `@DefaultValue("...")`: Sets a default value for a column -- `@Insert(InsertStrategy.PREFER_EXISTING/OVERWRITE_EXISTING)`: Controls insert behavior -- `@Delete(DeleteStrategy.CASCADE/NO_ACTION)`: Controls delete behavior -- `@UpdateInterval(milliseconds)`: Sets an interval for batching updates +- `@Data(schema = "...", table = "...")`: Defines the schema and table for a data class. Only applicable to classes + extending `UniqueData`. +- `@IdColumn(name = "...")`: Marks a field as the ID column. There is support for multiple ID columns. +- `@Column(name = "...", nullable = true/false, index = true/false)`: Define a column in the current table. +- `@ForeignColumn(name = "...", table = "...", link = "...")`: Define a column in another table, and create a foreign + key accordingly. +- `@OneToOne(link = "...")`: Defines a one-to-one relationship for `Reference` fields. A foreign key constraint is + created accordingly. +- `@OneToMany(link = "...")`: Defines a one-to-many relationship for `PersistentCollection` fields. A foreign key + constraint is created accordingly. +- `@ManyToMany(link = "...", joinTable = "...")`: Defines a many-to-many relationship for `PersistentCollection` + fields. A join table is created accordingly, and the appropriate foreign keys are created. +- `@DefaultValue("...")`: Sets a default value for a column. Note that this is a database-level default, not a + Java-level default. Only Strings are supported, and they must be valid SQL literals. For example, for an integer + column, you would use `@DefaultValue("0")`. +- `@Insert(InsertStrategy.PREFER_EXISTING/OVERWRITE_EXISTING)`: Controls insert behavior for `Reference` and foreign + columns. +- `@Delete(DeleteStrategy.CASCADE/NO_ACTION)`: Controls delete behavior. This has different behavior depending on the + relationship type, refer to the javadoc on each `DeleteStrategy` enum value for more information. +- `@UpdateInterval(milliseconds)`: Used on `PersistentValue` fields to control how often changes are flushed to the + source database. The default is 0 milliseconds. Since a FIFO queue (one connection to the source database) is used to + dispatch updates, frequent updates may clog up the queue. When the update interval is set to a non-zero value, only + the latest change within the interval is queued for writing to the source database. ### Data Wrappers -- `PersistentValue`: References a column in a data object's row -- `Reference`: References another data object (one-to-one relationship) -- `PersistentCollection`: Represents a collection relationship (one-to-many or many-to-many) +- `PersistentValue`: References a column in a table. Requires one of the annotations: `@IdColumn`, `@Column`, or + `@ForeignColumn`. +- `Reference`: References another data object (one-to-one relationship). Requires the `@OneToOne` annotation. +- `PersistentCollection`: Represents a collection relationship (one-to-many or many-to-many). Requires either the + `@OneToMany` or + `@ManyToMany` annotation. ### Compile-time Generated Classes -For each data class, `static-data` generates two helper classes at compile time: +For each data class, `static-data` generates two helper classes at compile time, for typesafe operations: 1. **Factory**: Provides a builder pattern for creating and inserting instances ``` @@ -86,6 +113,10 @@ For each data class, `static-data` generates two helper classes at compile time: .insert(InsertMode.SYNC); ``` +Note: The factory can use the global singleton DataManager instance if not explicitly provided. +In the above example, `UserFactory.builder(dataManager)` can be replaced with `UserFactory.builder()` if the global +instance is set. + 2. **Query Builder**: Provides a fluent API for querying instances ``` // Example: Finding users by criteria @@ -99,6 +130,8 @@ For each data class, `static-data` generates two helper classes at compile time: ``` Note: The query builder can use the global singleton DataManager instance if not explicitly provided. +In the above example, `UserQuery.where(dataManager)` can be replaced with `UserQuery.where()` if the global instance +is set. ## Data Types @@ -119,25 +152,129 @@ This flexibility allows all primitive types to be nullable when needed, while st ## Usage Example -``` -@Data(schema = "social_media", table = "posts") -public class Post extends UniqueData { - @IdColumn(name = "post_id") - public PersistentValue id; +[//]: # (TODO: validate the create method calls and ensure theyre correct, i did thie from memory) + +```java + +@Data(schema = "my_app", table = "users") +public class User extends UniqueData { + @IdColumn(name = "id") + private PersistentValue id; + + @OneToOne(link = "id=user_id") + private Reference metadata; + + @Column(name = "name", index = true, nullable = false) + private PersistentValue name; + + @ManyToMany(link = "id=id", joinTable = "user_friends") + private PersistentCollection friends; + + /** + * Create a new user and their metadata in one transaction. + * @param id the user ID + * @return the created user + */ + public static User create(int id) { + InsertContext ctx = DataManager.getInstance().createInsertContext(); + UserMetadataFactory.builder() + .id(UUID.randomUUID()) + .userId(id) + .createdAt(new Timestamp(System.currentTimeMillis())) + .insert(InsertMode.SYNC, ctx); + UserFactory factory = UserFactory.builder() + .id(id) + .name("User #" + id) + .insert(InsertMode.SYNC, ctx); + + ctx.insert(); + return factory.get(User.class); + } + + public int getId() { + return id.get(); + } + + public void setId(int id) { + this.id.set(id); + } + + public String getName() { + return name.get(); + } + + public void setName(String name) { + this.name.set(name); + } + + public UserMetadata getMetadata() { + return metadata.get(); + } + + public void setMetadata(UserMetadata metadata) { + this.metadata.set(metadata); + } + + public Collection getFriends() { + return friends.get(); + } + + public void addFriend(User friend) { + this.friends.add(friend); + } + + public void removeFriend(User friend) { + this.friends.remove(friend); + } +} + +@Data(schema = "my_app", table = "user_metadata") +public class UserMetadata extends UniqueData { + @IdColumn(name = "id") + private PersistentValue id; + + @OneToOne(link = "id=id") + private Reference user; + + @Column(name = "user_id", index = true, nullable = false) + private PersistentValue userId; + + @Column(name = "created_at", nullable = false) + private PersistentValue createdAt; + + public UUID getId() { + return id.get(); + } - @OneToOne(link = "post_id=metadata_id") - public Reference metadata; + public void setId(UUID id) { + this.id.set(id); + } - @Column(name = "text_content", index = true) - public PersistentValue textContent; + public User getUser() { + return user.get(); + } - @DefaultValue("0") - @Column(name = "likes") - public PersistentValue likes; + public void setUser(User user) { + this.user.set(user); + } - @ManyToMany(link = "post_id=post_id", joinTable = "posts_related") - public PersistentCollection relatedPosts; + public int getUserId() { + return userId.get(); + } + + public void setUserId(int userId) { + this.userId.set(userId); + } + + public Timestamp getCreatedAt() { + return createdAt.get(); + } + + public void setCreatedAt(Timestamp createdAt) { + this.createdAt.set(createdAt); + } } + ``` ## Current Limitations @@ -149,48 +286,13 @@ public class Post extends UniqueData { - **PostgreSQL-only mode**: A future update will add support for a PostgreSQL-only mode where `static-data` will act as a traditional ORM without using an in-memory cache. This will provide better performance for applications that don't - need the caching benefits and will reduce memory usage. + need the caching benefits and will reduce memory usage, while still providing the same developer experience. - **Disk-based cache**: Plans are in place to add support for a disk-based cache option (using H2 on disk instead of in memory) to reduce memory consumption while still maintaining the benefits of the caching architecture. ## Getting Started -[//]: # (TODO: this section is incorrect since the impl isnt finished. update this later) - -1. Configure your data source: - -``` -DataSourceConfig config = new DataSourceConfig.Builder() - .setPostgresUrl("jdbc:postgresql://localhost:5432/mydatabase") - .setPostgresUsername("username") - .setPostgresPassword("password") - .build(); -``` - -2. Initialize the DataManager: - -``` -DataManager dataManager = new DataManager(config); -``` - -3. Define your data models using annotations and data wrappers. +[//]: # (TODO: this section is incomplete since the impl isnt finished. update this later) -4. Load your models: - -``` -dataManager.load(Post.class, User.class); -``` - -5. Query and manipulate data: - -``` -// Get a post by ID -Post post = dataManager.getInstance(Post.class, new ColumnValuePair("post_id", 1)); - -// Update a value -post.likes.set(post.likes.get() + 1); - -// Access related objects -PostMetadata metadata = post.metadata.get(); -``` +[//]: # (TODO: talk about update handlers, & add/remove handlers) \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java index f68deca2..5708ca00 100644 --- a/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java +++ b/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -337,4 +337,5 @@ public void testToArrayTyped() { //todo: test other collection types //todo: add/remove handlers + //todo: for many-to-many, id like to support following and followers, using the same join table. } \ No newline at end of file From eb86e8b3a211c1436c5d3321bc429a8000ab4e39 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Fri, 17 Oct 2025 10:48:03 -0400 Subject: [PATCH 34/75] many to many impl and transactions for one-to-many methods --- .../net/staticstudios/data/DataAccessor.java | 9 +- .../net/staticstudios/data/DataManager.java | 3 + .../data/PersistentCollection.java | 3 - .../staticstudios/data/SQLTransaction.java | 85 ++ .../PersistentManyToManyCollectionImpl.java | 789 ++++++++++++++++++ .../PersistentOneToManyCollectionImpl.java | 256 +++--- .../data/impl/h2/H2DataAccessor.java | 93 ++- .../staticstudios/data/parse/SQLBuilder.java | 37 +- ...ersistentManyToManyCollectionMetadata.java | 60 ++ .../PersistentManyToManyCollectionTest.java | 312 +++++++ .../PersistentOneToManyCollectionTest.java | 388 ++++----- .../net/staticstudios/data/misc/DataTest.java | 1 + .../data/mock/user/MockUser.java | 5 +- 13 files changed, 1673 insertions(+), 368 deletions(-) create mode 100644 src/main/java/net/staticstudios/data/SQLTransaction.java create mode 100644 src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java create mode 100644 src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java create mode 100644 src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java diff --git a/src/main/java/net/staticstudios/data/DataAccessor.java b/src/main/java/net/staticstudios/data/DataAccessor.java index 67c04bfe..7113917f 100644 --- a/src/main/java/net/staticstudios/data/DataAccessor.java +++ b/src/main/java/net/staticstudios/data/DataAccessor.java @@ -9,13 +9,14 @@ import java.util.List; public interface DataAccessor { -// PreparedStatement prepareStatement(@Language("SQL") String sql) throws SQLException; - -// PersistentValue createPersistentValue(PrimaryKey primaryKey, Class dataType, String schema, String table, String dataColumn); ResultSet executeQuery(@Language("SQL") String sql, List values) throws SQLException; - void executeUpdate(@Language("SQL") String sql, List values, int delay) throws SQLException; + default void executeUpdate(@Language("SQL") String sql, List values, int delay) throws SQLException { + executeTransaction(new SQLTransaction().update(SQLTransaction.Statement.of(sql, sql), values), delay); + } + + void executeTransaction(SQLTransaction transaction, int delay) throws SQLException; void insert(List sqlStatements, InsertMode insertMode) throws SQLException; diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java index c21a05c3..b64b2618 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/src/main/java/net/staticstudios/data/DataManager.java @@ -2,6 +2,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.MapMaker; +import net.staticstudios.data.impl.data.PersistentManyToManyCollectionImpl; import net.staticstudios.data.impl.data.PersistentOneToManyCollectionImpl; import net.staticstudios.data.impl.data.PersistentValueImpl; import net.staticstudios.data.impl.data.ReferenceImpl; @@ -204,6 +205,7 @@ public void extractMetadata(Class clazz) { String table = ValueUtils.parseValue(dataAnnotation.table()); Map persistentCollectionMetadataMap = new HashMap<>(); persistentCollectionMetadataMap.putAll(PersistentOneToManyCollectionImpl.extractMetadata(clazz)); //todo: add other collection types + persistentCollectionMetadataMap.putAll(PersistentManyToManyCollectionImpl.extractMetadata(clazz)); UniqueDataMetadata metadata = new UniqueDataMetadata(clazz, schema, table, idColumns, PersistentValueImpl.extractMetadata(schema, table, clazz), ReferenceImpl.extractMetadata(clazz), persistentCollectionMetadataMap); uniqueDataMetadataMap.put(clazz, metadata); @@ -405,6 +407,7 @@ public T getInstance(Class clazz, ColumnValuePair... i PersistentValueImpl.delegate(instance); ReferenceImpl.delegate(instance); PersistentOneToManyCollectionImpl.delegate(instance); + PersistentManyToManyCollectionImpl.delegate(instance); //todo: other collection types uniqueDataInstanceCache.computeIfAbsent(clazz, k -> new MapMaker().weakValues().makeMap()) diff --git a/src/main/java/net/staticstudios/data/PersistentCollection.java b/src/main/java/net/staticstudios/data/PersistentCollection.java index 77539dee..02ed20a9 100644 --- a/src/main/java/net/staticstudios/data/PersistentCollection.java +++ b/src/main/java/net/staticstudios/data/PersistentCollection.java @@ -19,8 +19,6 @@ static PersistentCollection of(UniqueData holder, Class referenceType) UniqueData getHolder(); - Class getReferenceType(); - class ProxyPersistentCollection implements PersistentCollection { private final UniqueData holder; private final Class referenceType; @@ -37,7 +35,6 @@ public UniqueData getHolder() { return holder; } - @Override public Class getReferenceType() { return referenceType; } diff --git a/src/main/java/net/staticstudios/data/SQLTransaction.java b/src/main/java/net/staticstudios/data/SQLTransaction.java new file mode 100644 index 00000000..abf6ba87 --- /dev/null +++ b/src/main/java/net/staticstudios/data/SQLTransaction.java @@ -0,0 +1,85 @@ +package net.staticstudios.data; + +import com.google.common.base.Preconditions; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class SQLTransaction { + private final List operations = new ArrayList<>(); + + public SQLTransaction() { + } + + public List getOperations() { + return operations; + } + + public SQLTransaction query(Statement statement, Supplier> valuesSupplier, @NotNull Consumer resultHandler) { + Preconditions.checkNotNull(resultHandler, "Use update() method for statements without result handlers"); + operations.add(new Operation(statement, valuesSupplier, resultHandler)); + return this; + } + + public SQLTransaction update(Statement statement, Supplier> valuesSupplier) { + operations.add(new Operation(statement, valuesSupplier, null)); + return this; + } + + public SQLTransaction update(Statement statement, List values) { + operations.add(new Operation(statement, () -> values, null)); + return this; + } + + public static class Operation { + private final Statement statement; + private final Supplier> valuesSupplier; + private final @Nullable Consumer resultHandler; + + public Operation(Statement statement, Supplier> valuesSupplier, @Nullable Consumer resultHandler) { + this.statement = statement; + this.valuesSupplier = valuesSupplier; + this.resultHandler = resultHandler; + } + + public Statement getStatement() { + return statement; + } + + public Supplier> getValuesSupplier() { + return valuesSupplier; + } + + public @Nullable Consumer getResultHandler() { + return resultHandler; + } + } + + public static class Statement { + private final @Language("SQL") String h2Sql; + private final @Language("SQL") String pgSql; + + public Statement(@Language("SQL") String h2Sql, @Language("SQL") String pgSql) { + this.h2Sql = h2Sql; + this.pgSql = pgSql; + } + + public static Statement of(@Language("SQL") String h2Sql, @Language("SQL") String pgSql) { + return new Statement(h2Sql, pgSql); + } + + public @Language("SQL") String getH2Sql() { + return h2Sql; + } + + public @Language("SQL") String getPgSql() { + return pgSql; + } + } +} diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java new file mode 100644 index 00000000..4b30956d --- /dev/null +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java @@ -0,0 +1,789 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.*; +import net.staticstudios.data.parse.ForeignKey; +import net.staticstudios.data.parse.SQLBuilder; +import net.staticstudios.data.util.*; +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; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +public class PersistentManyToManyCollectionImpl implements PersistentCollection { + private final UniqueData holder; + private final Class type; + private final String parsedJoinTableSchema; + private final String parsedJoinTableName; + private final String links; // since we need information about the column prefixes in the join table, we have to compute these at runtime + private @Nullable List cachedJoinTableToDataTableLinks = null; + private @Nullable List cachedJoinTableToReferencedTableLinks = null; + + public PersistentManyToManyCollectionImpl(UniqueData holder, Class type, String parsedJoinTableSchema, String parsedJoinTableName, String links) { + this.holder = holder; + this.type = type; + this.parsedJoinTableSchema = parsedJoinTableSchema; + this.parsedJoinTableName = parsedJoinTableName; + this.links = links; + } + + public static void createAndDelegate(ProxyPersistentCollection proxy, PersistentManyToManyCollectionMetadata metadata) { + PersistentManyToManyCollectionImpl impl = new PersistentManyToManyCollectionImpl<>(proxy.getHolder(), proxy.getReferenceType(), metadata.getParsedJoinTableSchema(), metadata.getParsedJoinTableName(), metadata.getLinks()); + proxy.setDelegate(impl); + } + + @SuppressWarnings("unchecked") + public static PersistentManyToManyCollectionImpl create(UniqueData holder, PersistentManyToManyCollectionMetadata metadata) { + return new PersistentManyToManyCollectionImpl<>(holder, (Class) metadata.getDataType(), metadata.getParsedJoinTableSchema(), metadata.getParsedJoinTableName(), metadata.getLinks()); + } + + 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); + } + } + } + } + + public static Map extractMetadata(Class clazz) { + Map metadataMap = new HashMap<>(); + for (Field field : ReflectionUtils.getFields(clazz, PersistentCollection.class)) { + ManyToMany manyToManyAnnotation = field.getAnnotation(ManyToMany.class); + if (manyToManyAnnotation == null) continue; + Class genericType = ReflectionUtils.getGenericType(field); + if (genericType == null) continue; + Class referencedClass = genericType.asSubclass(UniqueData.class); + String parsedJoinTableSchemaName = ValueUtils.parseValue(manyToManyAnnotation.joinTableSchema()); + String parsedJoinTableName = ValueUtils.parseValue(manyToManyAnnotation.joinTable()); + String links = manyToManyAnnotation.link(); + PersistentManyToManyCollectionMetadata metadata = new PersistentManyToManyCollectionMetadata(referencedClass, parsedJoinTableSchemaName, parsedJoinTableName, links); + metadataMap.put(field, metadata); + } + + return metadataMap; + } + + public static String getJoinTableSchema(String parsedJoinTableSchema, String dataSchema) { + if (!parsedJoinTableSchema.isEmpty()) { + return parsedJoinTableSchema; + } else { + return dataSchema; + } + } + + public static String getJoinTableName(String parsedJoinTableName, String dataTable, String referencedTable) { + if (!parsedJoinTableName.isEmpty()) { + return parsedJoinTableName; + } else { + return dataTable + "_" + referencedTable; + } + } + + public static String getDataTableColumnPrefix(String dataTable) { + return dataTable; + } + + public static String getReferencedTableColumnPrefix(String dataTable, String referencedTable) { + if (dataTable.equals(referencedTable)) { + return referencedTable + "_ref"; + } + return ""; + } + + public static List getJoinTableToDataTableLinks(String dataTable, String links) { + List joinTableToDataTableLinks = new ArrayList<>(); + String dataTableColumnPrefix = getDataTableColumnPrefix(dataTable); + for (ForeignKey.Link link : SQLBuilder.parseLinks(links)) { + String columnInDataTable = link.columnInReferringTable(); + String dataColumnInJoinTable = dataTableColumnPrefix + "_" + columnInDataTable; + + joinTableToDataTableLinks.add(new ForeignKey.Link(columnInDataTable, dataColumnInJoinTable)); + } + return joinTableToDataTableLinks; + } + + public static List getJoinTableToReferencedTableLinks(String dataTable, String referencedTable, String links) { + List joinTableToReferencedTableLinks = new ArrayList<>(); + String referencedTableColumnPrefix = getReferencedTableColumnPrefix(dataTable, referencedTable); + for (ForeignKey.Link link : SQLBuilder.parseLinks(links)) { + String columnInReferencedTable = link.columnInReferencedTable(); + String referencedColumnInJoinTable = referencedTableColumnPrefix + "_" + columnInReferencedTable; + + joinTableToReferencedTableLinks.add(new ForeignKey.Link(columnInReferencedTable, referencedColumnInJoinTable)); + } + return joinTableToReferencedTableLinks; + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public int size() { + return getIds().size(); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean contains(Object o) { + if (!type.isInstance(o)) { + return false; + } + T data = type.cast(o); + Set ids = getIds(); + ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + for (ColumnValuePair[] idColumns : ids) { + if (Arrays.equals(idColumns, thatIdColumns)) { + return true; + } + } + + return false; + } + + @Override + public @NotNull Iterator iterator() { + return new IteratorImpl(getIds()); + } + + @Override + public @NotNull Object @NotNull [] toArray() { + Set ids = getIds(); + Object[] array = new Object[ids.size()]; + int i = 0; + for (ColumnValuePair[] idColumns : ids) { + T instance = holder.getDataManager().getInstance(type, idColumns); + array[i++] = instance; + } + return array; + } + + @SuppressWarnings("unchecked") + @Override + public @NotNull T1 @NotNull [] toArray(@NotNull T1 @NotNull [] a) { + Set ids = getIds(); + if (a.length < ids.size()) { + a = (T1[]) Array.newInstance(a.getClass().getComponentType(), ids.size()); + } + int i = 0; + for (ColumnValuePair[] idColumns : ids) { + T instance = holder.getDataManager().getInstance(type, idColumns); + T1 element = (T1) instance; + a[i++] = element; + } + return a; + } + + @Override + public boolean add(T t) { + return addAll(Collections.singleton(t)); + } + + @Override + public boolean remove(Object o) { + return removeAll(Collections.singleton(o)); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + for (Object o : c) { + if (!type.isInstance(o)) { + return false; + } + } + Set ids = getIds(); + for (Object o : c) { + T data = type.cast(o); + ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + boolean found = false; + for (ColumnValuePair[] idColumns : ids) { + if (Arrays.equals(idColumns, thatIdColumns)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + @Override + public boolean addAll(@NotNull Collection c) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); + if (c.isEmpty()) { //this operation isn't cheap, so we should avoid it if we can + return false; + } + + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); + SQLTransaction.Statement selectReferencedIdsStatement = buildSelectReferencedIdsStatement(); + SQLTransaction.Statement updateStatement = buildUpdateStatement(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + + List holderLinkingValues = new ArrayList<>(joinTableToDataTableLinks.size()); + List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String dataColumn = entry.columnInReferencedTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + + for (T entry : c) { + List referencedIdValues = entry.getIdColumns().stream().map(ColumnValuePair::value).toList(); + List referencedLinkingValues = new ArrayList<>(joinTableToReferencedTableLinks.size()); + transaction.query(selectReferencedIdsStatement, () -> referencedIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find referenced row in database"); + for (ForeignKey.Link _entry : joinTableToReferencedTableLinks) { + String referencedColumn = _entry.columnInReferencedTable(); + Object value = rs.getObject(referencedColumn); + referencedLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + transaction.update(updateStatement, () -> { + List values = new ArrayList<>(holderLinkingValues.size() + referencedLinkingValues.size()); + values.addAll(holderLinkingValues); + values.addAll(referencedLinkingValues); + return values; + }); + } + + try { + dataAccessor.executeTransaction(transaction, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return !c.isEmpty(); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + List idsToRemove = new ArrayList<>(); + for (Object o : c) { + if (!type.isInstance(o)) { + continue; + } + T data = type.cast(o); + ColumnValuePair[] idColumns = data.getIdColumns().getPairs(); + idsToRemove.add(idColumns); + } + return removeIds(idsToRemove); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + Set currentIds = getIds(); + Set idsToRetain = new HashSet<>(); + for (Object o : c) { + if (!type.isInstance(o)) { + continue; + } + T data = type.cast(o); + ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + idsToRetain.add(thatIdColumns); + } + + List idsToRemove = new ArrayList<>(); + for (ColumnValuePair[] idColumns : currentIds) { + boolean found = false; + for (ColumnValuePair[] retainIdColumns : idsToRetain) { + if (Arrays.equals(idColumns, retainIdColumns)) { + found = true; + break; + } + } + if (!found) { + idsToRemove.add(idColumns); + } + } + + if (!idsToRemove.isEmpty()) { + removeIds(idsToRemove); + return true; + } + + return false; + } + + @Override + public void clear() { + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); + SQLTransaction.Statement clearStatement = buildClearStatement(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + + List holderLinkingValues = new ArrayList<>(joinTableToDataTableLinks.size()); + List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String dataColumn = entry.columnInReferencedTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + transaction.update(clearStatement, holderLinkingValues); + + try { + dataAccessor.executeTransaction(transaction, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + public boolean removeIds(List idsToRemove) { + if (idsToRemove.isEmpty()) { //this operation isn't cheap, so we should avoid it if we can + return false; + } + + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); + SQLTransaction.Statement selectReferencedIdsStatement = buildSelectReferencedIdsStatement(); + SQLTransaction.Statement removeStatement = buildRemoveStatement(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + + List holderLinkingValues = new ArrayList<>(joinTableToDataTableLinks.size()); + List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String dataColumn = entry.columnInReferencedTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + + for (ColumnValuePair[] idColumns : idsToRemove) { + List referencedIdValues = Arrays.stream(idColumns).map(ColumnValuePair::value).toList(); + List referencedLinkingValues = new ArrayList<>(joinTableToReferencedTableLinks.size()); + transaction.query(selectReferencedIdsStatement, () -> referencedIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find referenced row in database"); + for (ForeignKey.Link _entry : joinTableToReferencedTableLinks) { + String referencedColumn = _entry.columnInReferencedTable(); + Object value = rs.getObject(referencedColumn); + referencedLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + transaction.update(removeStatement, () -> { + List values = new ArrayList<>(holderLinkingValues.size() + referencedLinkingValues.size()); + values.addAll(holderLinkingValues); + values.addAll(referencedLinkingValues); + return values; + }); + } + + try { + dataAccessor.executeTransaction(transaction, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return true; + } + + /** + * get the ids for the referenced type that are linked to the holder + * + * @return set of id column value pairs for the referenced type + */ + private 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(); + UniqueDataMetadata target = holder.getDataManager().getMetadata(type); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + + String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); + String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), target.table()); + List joinTableToDataTableLinks = getJoinTableToDataTableLinks(holderMetadata.table(), links); + List joinTableToReferencedTableLinks = getJoinTableToReferencedTableLinks(holderMetadata.table(), target.table(), links); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ColumnMetadata columnMetadata : target.idColumns()) { + sqlBuilder.append("_target.\"").append(columnMetadata.name()).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 "); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + String dataColumn = entry.columnInReferencedTable(); + sqlBuilder.append("_data.\"").append(dataColumn).append("\" = \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\".\"").append(joinColumn).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + sqlBuilder.append(" INNER JOIN \"").append(target.schema()).append("\".\"").append(target.table()).append("\" _target ON "); + for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + String referencedColumn = entry.columnInReferencedTable(); + sqlBuilder.append("_target.\"").append(referencedColumn).append("\" = \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\".\"").append(joinColumn).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + + sqlBuilder.append(" WHERE "); + + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("_data.\"").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())) { + while (rs.next()) { + int i = 0; + ColumnValuePair[] idColumns = new ColumnValuePair[target.idColumns().size()]; + for (ColumnMetadata columnMetadata : target.idColumns()) { + Object value = rs.getObject(columnMetadata.name()); + idColumns[i++] = new ColumnValuePair(columnMetadata.name(), value); + } + ids.add(idColumns); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return ids; + } + + + private SQLTransaction.Statement buildSelectDataIdsStatement() { + UniqueDataMetadata holderMetadata = holder.getMetadata(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String dataColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(dataColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); + } + + private SQLTransaction.Statement buildSelectReferencedIdsStatement() { + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + String referencedColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(referencedColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" WHERE "); + for (ColumnMetadata theirIdColumn : typeMetadata.idColumns()) { + sqlBuilder.append("\"").append(theirIdColumn.name()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); + } + + + private SQLTransaction.Statement buildUpdateStatement() { + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + + String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); + String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("MERGE INTO \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" AS _target USING (VALUES ("); + sqlBuilder.append("?, ".repeat(Math.max(0, joinTableToDataTableLinks.size() + joinTableToReferencedTableLinks.size()))); + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(")) AS _source ("); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") ON "); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("_target.\"").append(joinColumn).append("\" = _source.\"").append(joinColumn).append("\" AND "); + } + for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("_target.\"").append(joinColumn).append("\" = _source.\"").append(joinColumn).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + sqlBuilder.append(" WHEN NOT MATCHED THEN INSERT ("); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") VALUES ("); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("_source.\"").append(joinColumn).append("\", "); + } + for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("_source.\"").append(joinColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(")"); + @Language("SQL") String h2MergeSql = sqlBuilder.toString(); + + sqlBuilder.setLength(0); + sqlBuilder.append("INSERT INTO \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" ("); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") VALUES ("); + sqlBuilder.append("?, ".repeat(Math.max(0, joinTableToDataTableLinks.size() + joinTableToReferencedTableLinks.size()))); + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") ON CONFLICT DO NOTHING"); + @Language("SQL") String pgInsertSql = sqlBuilder.toString(); + + return SQLTransaction.Statement.of(h2MergeSql, pgInsertSql); + } + + private SQLTransaction.Statement buildRemoveStatement() { + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + + String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); + String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("DELETE FROM \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" WHERE "); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\" = ? AND "); + } + for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); + } + + private SQLTransaction.Statement buildClearStatement() { + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + + String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); + String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("DELETE FROM \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" WHERE "); + for (ForeignKey.Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); + } + + private List getCachedJoinTableToDataTableLinks() { + if (cachedJoinTableToDataTableLinks == null) { + UniqueDataMetadata holderMetadata = holder.getMetadata(); + cachedJoinTableToDataTableLinks = getJoinTableToDataTableLinks(holderMetadata.table(), links); + } + return cachedJoinTableToDataTableLinks; + } + + private List getCachedJoinTableToReferencedTableLinks() { + if (cachedJoinTableToReferencedTableLinks == null) { + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + cachedJoinTableToReferencedTableLinks = getJoinTableToReferencedTableLinks(holderMetadata.table(), typeMetadata.table(), links); + } + return cachedJoinTableToReferencedTableLinks; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof PersistentManyToManyCollectionImpl that)) return false; + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata thatHolderMetadata = that.holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + boolean equals = Objects.equals(type, that.type) && + Objects.equals(getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()), + getJoinTableSchema(that.parsedJoinTableSchema, thatHolderMetadata.schema())) && + Objects.equals(getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()), + getJoinTableName(that.parsedJoinTableName, thatHolderMetadata.table(), typeMetadata.table())) && + Objects.equals(getJoinTableToDataTableLinks(holderMetadata.table(), links), + getJoinTableToDataTableLinks(thatHolderMetadata.table(), that.links)) && + Objects.equals(getJoinTableToReferencedTableLinks(holderMetadata.table(), typeMetadata.table(), links), + getJoinTableToReferencedTableLinks(thatHolderMetadata.table(), typeMetadata.table(), that.links)); + + if (!equals) { + return false; + } + + Set ids = getIds(); + Set thatIds = that.getIds(); + if (ids.size() != thatIds.size()) { + return false; + } + + for (ColumnValuePair[] idColumns : ids) { + boolean found = false; + for (ColumnValuePair[] thatIdColumns : thatIds) { + if (Arrays.equals(idColumns, thatIdColumns)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + + return true; + } + + @Override + public int hashCode() { + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + int hash = Objects.hash(type, + getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()), + getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()), + getJoinTableToDataTableLinks(holderMetadata.table(), links), + getJoinTableToReferencedTableLinks(holderMetadata.table(), typeMetadata.table(), links)); + + int arrayHash = 0; // the ids will not always be in the same order, so ensure this is commutative + for (ColumnValuePair[] idColumns : getIds()) { + arrayHash += Arrays.hashCode(idColumns); + } + + hash = 31 * hash + arrayHash; + return hash; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (T item : this) { + sb.append(item).append(", "); + } + if (!isEmpty()) { + sb.setLength(sb.length() - 2); + } + sb.append("]"); + return sb.toString(); + } + + class IteratorImpl implements Iterator { + private final List ids; + private int index = 0; + + public IteratorImpl(Set ids) { + this.ids = new ArrayList<>(ids); + } + + @Override + public boolean hasNext() { + return index < ids.size(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + ColumnValuePair[] idColumns = ids.get(index++); + return holder.getDataManager().getInstance(type, idColumns); + } + + @Override + public void remove() { + Preconditions.checkState(index > 0, "next() has not been called yet"); + removeIds(Collections.singletonList(ids.get(index - 1))); + ids.remove(--index); + } + } +} diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java b/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java index 4ba44e03..a102d029 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java +++ b/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java @@ -1,10 +1,7 @@ package net.staticstudios.data.impl.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.DataAccessor; -import net.staticstudios.data.OneToMany; -import net.staticstudios.data.PersistentCollection; -import net.staticstudios.data.UniqueData; +import net.staticstudios.data.*; import net.staticstudios.data.parse.ForeignKey; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; @@ -81,11 +78,6 @@ public UniqueData getHolder() { return holder; } - @Override - public Class getReferenceType() { - return type; - } - @Override public int size() { return getIds().size(); @@ -184,38 +176,48 @@ public boolean containsAll(@NotNull Collection c) { @Override public boolean addAll(@NotNull Collection c) { Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); - UniqueDataMetadata holderMetadata = holder.getMetadata(); - UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - - StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); - for (ForeignKey.Link entry : link) { - String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(theirColumn).append("\" = ?, "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(" WHERE "); - for (ColumnMetadata theirIdColumn : typeMetadata.idColumns()) { - sqlBuilder.append("\"").append(theirIdColumn.name()).append("\" = ? AND "); + if (c.isEmpty()) { + return false; } - sqlBuilder.setLength(sqlBuilder.length() - 5); - @Language("SQL") String updateSql = sqlBuilder.toString(); DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); - List myValues = getMyLinkingValues(holderMetadata, dataAccessor); - for (T entry : c) { - List values = new ArrayList<>(myValues); - for (ColumnValuePair idColumn : entry.getIdColumns()) { - values.add(idColumn.value()); - } + SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); + SQLTransaction.Statement updateStatement = buildUpdateStatement(); + + + List holderLinkingValues = new ArrayList<>(link.size()); + List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { try { - dataAccessor.executeUpdate(updateSql, values, 0); + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (ForeignKey.Link entry : link) { + String dataColumn = entry.columnInReferringTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } } catch (SQLException e) { throw new RuntimeException(e); } + }); + + for (T entry : c) { + transaction.update(updateStatement, () -> { + List values = new ArrayList<>(holderLinkingValues); + for (ColumnValuePair idColumn : entry.getIdColumns()) { + values.add(idColumn.value()); + } + return values; + }); + } + try { + dataAccessor.executeTransaction(transaction, 0); + } catch (SQLException e) { + throw new RuntimeException(e); } - return !c.isEmpty(); + return true; } @Override @@ -229,7 +231,7 @@ public boolean removeAll(@NotNull Collection c) { ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); ids.add(thatIdColumns); } - removeAll(ids); + removeIds(ids); return !ids.isEmpty(); } @@ -262,7 +264,7 @@ public boolean retainAll(@NotNull Collection c) { } if (!idsToRemove.isEmpty()) { - removeAll(idsToRemove); + removeIds(idsToRemove); return true; } @@ -271,101 +273,146 @@ public boolean retainAll(@NotNull Collection c) { @Override public void clear() { - Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); - UniqueDataMetadata holderMetadata = holder.getMetadata(); - UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + Preconditions.checkArgument(!holder.isDeleted(), "Cannot clear entries on a deleted UniqueData instance"); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); - StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); - for (ForeignKey.Link entry : link) { - String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(theirColumn).append("\" = NULL, "); + SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); + SQLTransaction.Statement clearStatement = buildClearStatement(); + + + List holderLinkingValues = new ArrayList<>(link.size()); + List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (ForeignKey.Link entry : link) { + String dataColumn = entry.columnInReferringTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + transaction.update(clearStatement, () -> holderLinkingValues); + try { + dataAccessor.executeTransaction(transaction, 0); + } catch (SQLException e) { + throw new RuntimeException(e); } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(" WHERE "); - for (ForeignKey.Link entry : link) { - String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(theirColumn).append("\" = ? AND "); + } + + private void removeIds(List ids) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); + if (ids.isEmpty()) { + return; } - sqlBuilder.setLength(sqlBuilder.length() - 5); - @Language("SQL") String updateSql = sqlBuilder.toString(); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); - List values = getMyLinkingValues(holderMetadata, dataAccessor); + + SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); + SQLTransaction.Statement updateStatement = buildUpdateStatement(); + + + List holderLinkingValues = new ArrayList<>(link.size()); + List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (ForeignKey.Link entry : link) { + String dataColumn = entry.columnInReferringTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + for (ColumnValuePair[] idColumns : ids) { + transaction.update(updateStatement, () -> { + List values = new ArrayList<>(); + for (Object holderLinkingValue : holderLinkingValues) { + values.add(null); + } + for (ColumnValuePair idColumn : idColumns) { + values.add(idColumn.value()); + } + return values; + }); + } try { - dataAccessor.executeUpdate(updateSql, values, 0); + dataAccessor.executeTransaction(transaction, 0); } catch (SQLException e) { throw new RuntimeException(e); } } - private void removeAll(List ids) { - Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); + private SQLTransaction.Statement buildSelectDataIdsStatement() { UniqueDataMetadata holderMetadata = holder.getMetadata(); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ForeignKey.Link entry : link) { + String dataColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(dataColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); + } + + private SQLTransaction.Statement buildUpdateStatement() { UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); for (ForeignKey.Link entry : link) { String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(theirColumn).append("\" = NULL, "); + sqlBuilder.append("\"").append(theirColumn).append("\" = ?, "); } sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(" WHERE "); - for (ForeignKey.Link entry : link) { - String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(theirColumn).append("\" = ? AND "); - } for (ColumnMetadata theirIdColumn : typeMetadata.idColumns()) { sqlBuilder.append("\"").append(theirIdColumn.name()).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); - @Language("SQL") String updateSql = sqlBuilder.toString(); - DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); - List myValues = getMyLinkingValues(holderMetadata, dataAccessor); - - for (ColumnValuePair[] idColumns : ids) { - List values = new ArrayList<>(myValues); - for (ColumnValuePair idColumn : idColumns) { - values.add(idColumn.value()); - } - try { - dataAccessor.executeUpdate(updateSql, values, 0); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); } - private List getMyLinkingValues(UniqueDataMetadata holderMetadata, DataAccessor dataAccessor) { + private SQLTransaction.Statement buildClearStatement() { + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append("SELECT "); + sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); for (ForeignKey.Link entry : link) { - String myColumn = entry.columnInReferringTable(); - sqlBuilder.append("\"").append(myColumn).append("\", "); + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = NULL, "); } sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); - for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + sqlBuilder.append(" WHERE "); + for (ForeignKey.Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); - @Language("SQL") String selectSql = sqlBuilder.toString(); - - List myValues = new ArrayList<>(link.size()); - try (ResultSet rs = dataAccessor.executeQuery(selectSql, holder.getIdColumns().stream().map(ColumnValuePair::value).toList())) { - Preconditions.checkState(rs.next(), "Could not find holder row in database"); - for (ForeignKey.Link entry : link) { - String myColumn = entry.columnInReferringTable(); - myValues.add(rs.getObject(myColumn)); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - - return myValues; + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); } private Set getIds() { + // note: we need the join since we support linking on non-id columns Preconditions.checkArgument(!holder.isDeleted(), "Cannot get entries on a deleted UniqueData instance"); Set ids = new HashSet<>(); UniqueDataMetadata holderMetadata = holder.getMetadata(); @@ -377,25 +424,25 @@ private Set getIds() { sqlBuilder.append("\"").append(columnMetadata.name()).append("\", "); } for (ColumnMetadata columnMetadata : holderMetadata.idColumns()) { - sqlBuilder.append("source.\"").append(columnMetadata.name()).append("\", "); + sqlBuilder.append("_source.\"").append(columnMetadata.name()).append("\", "); } sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(" FROM \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" "); - sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" AS source ON "); + sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" AS _source ON "); for (ForeignKey.Link entry : link) { String myColumn = entry.columnInReferringTable(); String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\".\"").append(theirColumn).append("\" = source.\"").append(myColumn).append("\" AND "); + sqlBuilder.append("\"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\".\"").append(theirColumn).append("\" = _source.\"").append(myColumn).append("\" AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); sqlBuilder.append(" WHERE "); for (ForeignKey.Link entry : link) { String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(theirColumn).append("\" = source.\"").append(entry.columnInReferringTable()).append("\" AND "); + sqlBuilder.append("\"").append(theirColumn).append("\" = _source.\"").append(entry.columnInReferringTable()).append("\" AND "); } for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - sqlBuilder.append("source.\"").append(columnValuePair.column()).append("\" = ? AND "); + sqlBuilder.append("_source.\"").append(columnValuePair.column()).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); @@ -421,14 +468,15 @@ private Set getIds() { public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof PersistentOneToManyCollectionImpl that)) return false; - return Objects.equals(holder, that.holder) && - Objects.equals(type, that.type) && - Objects.equals(getIds(), that.getIds()); + //due to the nature of how one-to-many collections work, this will == that only if the holder is the same. no need to compare contents. + return Objects.equals(type, that.type) && Objects.equals(holder, that.holder); } @Override public int hashCode() { - return Objects.hash(holder, type, getIds()); + //due to the nature of how one-to-many collections work, this will == that only if the holder is the same. no need to compare contents. + //for this reason, we can use just the type and holder for the hashcode. + return Objects.hash(type, holder); } @Override @@ -470,7 +518,7 @@ public T next() { @Override public void remove() { Preconditions.checkState(index > 0, "next() has not been called yet"); - removeAll(Collections.singletonList(ids.get(index - 1))); + removeIds(Collections.singletonList(ids.get(index - 1))); ids.remove(--index); } } diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java index c672a59d..bbcc584c 100644 --- a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java +++ b/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -5,6 +5,7 @@ import net.staticstudios.data.DataAccessor; import net.staticstudios.data.DataManager; import net.staticstudios.data.InsertMode; +import net.staticstudios.data.SQLTransaction; import net.staticstudios.data.impl.h2.trigger.H2UpdateHandlerTrigger; import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.parse.DDLStatement; @@ -30,6 +31,7 @@ import java.sql.*; import java.util.*; import java.util.concurrent.*; +import java.util.function.Consumer; /** * This data accessor uses a write-behind caching strategy with an in-memory H2 database to optimize read and write operations. @@ -47,7 +49,7 @@ public class H2DataAccessor implements DataAccessor { private final Set knownTables = new HashSet<>(); private final DataManager dataManager; private final PostgresListener postgresListener; - private final Map delayedTasks = new ConcurrentHashMap<>(); + private final Map>, Runnable> delayedTasks = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(thread -> { Thread t = new Thread(thread); t.setName(H2DataAccessor.class.getSimpleName() + "-ScheduledExecutor"); @@ -57,7 +59,7 @@ public class H2DataAccessor implements DataAccessor { public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener, TaskQueue taskQueue) { this.taskQueue = taskQueue; this.postgresListener = postgresListener; - this.jdbcUrl = "jdbc:h2:mem:static-data-cache;DB_CLOSE_DELAY=-1;LOCK_MODE=0;CACHE_SIZE=65536"; + 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; postgresListener.addHandler(notification -> { @@ -387,18 +389,41 @@ public ResultSet executeQuery(@Language("SQL") String sql, List values) } @Override - public void executeUpdate(@Language("SQL") String sql, List values, int delay) throws SQLException { - PreparedStatement cachePreparedStatement = prepareStatement(sql); - for (int i = 0; i < values.size(); i++) { - cachePreparedStatement.setObject(i + 1, values.get(i)); - } - logger.debug("[H2] {}", sql); - cachePreparedStatement.executeUpdate(); - if (!getConnection().getAutoCommit()) { - getConnection().commit(); + public void executeTransaction(SQLTransaction transaction, int delay) throws SQLException { + Connection connection = getConnection(); + boolean autoCommit = connection.getAutoCommit(); + try { + connection.setAutoCommit(false); + for (SQLTransaction.Operation operation : transaction.getOperations()) { + SQLTransaction.Statement sqlStatement = operation.getStatement(); + String h2Sql = sqlStatement.getH2Sql(); + PreparedStatement cachePreparedStatement = prepareStatement(h2Sql); + int i = 0; + List values = operation.getValuesSupplier().get(); + for (Object value : values) { + cachePreparedStatement.setObject(++i, value); + } + logger.debug("[H2] {}", h2Sql); + Consumer resultHandler = operation.getResultHandler(); + if (resultHandler == null) { + cachePreparedStatement.executeUpdate(); + } else { + try (ResultSet rs = cachePreparedStatement.executeQuery()) { + resultHandler.accept(rs); + } + } + } + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + throw e; + } finally { + if (autoCommit) { + connection.setAutoCommit(true); + } } - runDatabaseTask(sql, values.toArray(), delay); + runDatabaseTask(transaction, delay); } @Override @@ -473,23 +498,51 @@ private List getColumnsInTable(String schema, String table) throws SQLEx return columns; } - private void runDatabaseTask(String sql, Object[] params, int delay) { + private void runDatabaseTask(SQLTransaction transaction, int delay) { + List> key = new ArrayList<>(transaction.getOperations().size()); + for (SQLTransaction.Operation operation : transaction.getOperations()) { + SQLTransaction.Statement sqlStatement = operation.getStatement(); + key.add(Pair.of(sqlStatement.getH2Sql(), sqlStatement.getPgSql())); + } + Runnable runnable = () -> taskQueue.submitTask(connection -> { - PreparedStatement realPreparedStatement = connection.prepareStatement(sql); - for (int i = 0; i < params.length; i++) { - realPreparedStatement.setObject(i + 1, params[i]); + boolean autoCommit = connection.getAutoCommit(); + try { + connection.setAutoCommit(false); + for (SQLTransaction.Operation operation : transaction.getOperations()) { + if (operation.getResultHandler() != null) { + // we don't support queries on the real db + continue; + } + SQLTransaction.Statement statement = operation.getStatement(); + PreparedStatement realPreparedStatement = connection.prepareStatement(statement.getPgSql()); + int i = 0; + List values = operation.getValuesSupplier().get(); + for (Object value : values) { + realPreparedStatement.setObject(++i, value); + } + logger.debug("[DB] {}", statement.getPgSql()); + realPreparedStatement.executeUpdate(); + } + connection.commit(); + } catch (SQLException e) { + connection.rollback(); + //todo: ideally this should trigger an error which causes us to resync the h2 db + logger.error("Error updating the real db", e); + } finally { + if (autoCommit) { + connection.setAutoCommit(true); + } } - logger.debug("[DB] {}}", sql); - realPreparedStatement.executeUpdate(); }); if (delay <= 0) { runnable.run(); return; } - if (delayedTasks.put(sql, runnable) == null) { + if (delayedTasks.put(key, runnable) == null) { scheduledExecutorService.schedule(() -> { - Runnable removed = delayedTasks.remove(sql); + Runnable removed = delayedTasks.remove(key); if (removed != null) { removed.run(); } diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java index bad04956..1083db9d 100644 --- a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -2,6 +2,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.*; +import net.staticstudios.data.impl.data.PersistentManyToManyCollectionImpl; import net.staticstudios.data.util.*; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; @@ -544,7 +545,7 @@ private void parseOneToManyPersistentCollection(OneToMany oneToMany, Class genericType, Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { + private void parseManyToManyPersistentCollection(ManyToMany manyToMany, Class genericType, Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { UniqueDataMetadata referencedMetadata = dataManager.getMetadata(genericType); Preconditions.checkNotNull(referencedMetadata, "No metadata found for referenced class " + genericType.getName()); @@ -562,38 +563,22 @@ private void parseManyToManyPersistentCollection(ManyToMany oneToMany, Class joinTableToDataTableLinks = new ArrayList<>(); - List joinTableToReferencedTableLinks = new ArrayList<>(); + List joinTableToDataTableLinks; + List joinTableToReferencedTableLinks; - String dataTableColumnPrefix = dataTable; - String referencedTableColumnPrefix = referencedTable.getName(); - if (referencedTableColumnPrefix.equals(dataTableColumnPrefix)) { - referencedTableColumnPrefix = referencedTableColumnPrefix + "_ref"; - } try { - for (ForeignKey.Link link : parseLinks(oneToMany.link())) { - String columnInDataTable = link.columnInReferringTable(); - String dataColumnInJoinTable = dataTableColumnPrefix + "_" + columnInDataTable; - String columnInReferencedTable = link.columnInReferencedTable(); - String referencedColumnInJoinTable = referencedTableColumnPrefix + "_" + columnInReferencedTable; - - joinTableToDataTableLinks.add(new ForeignKey.Link(columnInDataTable, dataColumnInJoinTable)); - joinTableToReferencedTableLinks.add(new ForeignKey.Link(columnInReferencedTable, referencedColumnInJoinTable)); - } + joinTableToDataTableLinks = PersistentManyToManyCollectionImpl.getJoinTableToDataTableLinks(dataTable, manyToMany.link()); + joinTableToReferencedTableLinks = PersistentManyToManyCollectionImpl.getJoinTableToReferencedTableLinks(dataTable, referencedTable.getName(), manyToMany.link()); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Error parsing @ManyToMany link on field " + field.getName() + " in class " + clazz.getName() + ": " + e.getMessage(), e); } + String referencedTableColumnPrefix = PersistentManyToManyCollectionImpl.getReferencedTableColumnPrefix(dataTable, referencedTable.getName()); + String dataTableColumnPrefix = PersistentManyToManyCollectionImpl.getDataTableColumnPrefix(dataTable); + SQLSchema joinSchema = schemas.computeIfAbsent(joinTableSchemaName, SQLSchema::new); SQLTable joinTable = joinSchema.getTable(joinTableName); if (joinTable == null) { diff --git a/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java b/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java new file mode 100644 index 00000000..0e409856 --- /dev/null +++ b/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java @@ -0,0 +1,60 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +import java.util.Objects; + +public class PersistentManyToManyCollectionMetadata implements PersistentCollectionMetadata { + private final Class dataType; + private final String parsedJoinTableSchema; + private final String parsedJoinTableName; + private final String links; + + public PersistentManyToManyCollectionMetadata(Class dataType, String parsedJoinTableSchema, String parsedJoinTableName, String links) { + this.dataType = dataType; + this.parsedJoinTableSchema = parsedJoinTableSchema; + this.parsedJoinTableName = parsedJoinTableName; + this.links = links; + } + + public Class getDataType() { + return dataType; + } + + public String getParsedJoinTableSchema() { + return parsedJoinTableSchema; + } + + public String getParsedJoinTableName() { + return parsedJoinTableName; + } + + public String getLinks() { + return links; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PersistentManyToManyCollectionMetadata that = (PersistentManyToManyCollectionMetadata) o; + return Objects.equals(dataType, that.dataType) && + Objects.equals(parsedJoinTableSchema, that.parsedJoinTableSchema) && + Objects.equals(parsedJoinTableName, that.parsedJoinTableName) && + Objects.equals(links, that.links); + } + + @Override + public int hashCode() { + return Objects.hash(dataType, parsedJoinTableSchema, parsedJoinTableName, links); + } + + @Override + public String toString() { + return "PersistentManyToManyCollectionMetadata{" + + "dataType=" + dataType + + ", parsedJoinTableSchema='" + parsedJoinTableSchema + '\'' + + ", parsedJoinTableName='" + parsedJoinTableName + '\'' + + ", links='" + links + '\'' + + '}'; + } +} diff --git a/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java b/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java new file mode 100644 index 00000000..5ef1b760 --- /dev/null +++ b/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java @@ -0,0 +1,312 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.misc.TestUtils; +import net.staticstudios.data.mock.user.MockUser; +import net.staticstudios.data.mock.user.MockUserFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class PersistentManyToManyCollectionTest extends DataTest { + private static final int FRIEND_COUNT = 20; + + private MockUser mockUser; + private DataManager dataManager; + + @BeforeEach + public void setUp() { + dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + mockUser = MockUserFactory.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); + } + + private List createFriends(int count) { + List friends = new ArrayList<>(); + for (int i = 0; i < count; i++) { + MockUser friend = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("friend " + i) + .insert(InsertMode.ASYNC); + friends.add(friend); + } + return friends; + } + + @Test + public void testAdd() { + List friends = createFriends(FRIEND_COUNT); + + int size = mockUser.friends.size(); + for (MockUser friend : friends) { + assertTrue(mockUser.friends.add(friend)); + assertEquals(++size, mockUser.friends.size()); + } + + for (MockUser friend : friends) { + assertTrue(mockUser.friends.contains(friend)); + } + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_friends\" WHERE \"users_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(friends.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testAddAll() { + List friends = createFriends(FRIEND_COUNT); + + assertTrue(mockUser.friends.addAll(friends)); + assertEquals(friends.size(), mockUser.friends.size()); + + for (MockUser friend : friends) { + assertTrue(mockUser.friends.contains(friend)); + } + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_friends\" WHERE \"users_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(friends.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testRemove() { + List friends = createFriends(FRIEND_COUNT); + mockUser.friends.addAll(friends); + + for (MockUser friend : friends) { + assertTrue(mockUser.friends.contains(friend)); + } + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_friends\" WHERE \"users_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(friends.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + int size = mockUser.friends.size(); + for (MockUser friend : friends) { + assertTrue(mockUser.friends.remove(friend)); + assertEquals(--size, mockUser.friends.size()); + } + + for (MockUser friend : friends) { + assertFalse(mockUser.friends.contains(friend)); + } + + assertTrue(mockUser.friends.isEmpty()); + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_friends\" WHERE \"users_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(0, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("SuspiciousMethodCalls") + @Test + public void testRemoveAll() { + assertFalse(mockUser.friends.removeAll(new ArrayList<>())); + assertFalse(mockUser.friends.removeAll(List.of("not a user"))); + + List friends = createFriends(FRIEND_COUNT); + mockUser.friends.addAll(friends); + + for (MockUser friend : friends) { + assertTrue(mockUser.friends.contains(friend)); + } + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_friends\" WHERE \"users_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(friends.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + assertTrue(mockUser.friends.removeAll(friends)); + assertEquals(0, mockUser.friends.size()); + + for (MockUser friend : friends) { + assertFalse(mockUser.friends.contains(friend)); + } + + assertTrue(mockUser.friends.isEmpty()); + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_friends\" WHERE \"users_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(0, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testContains() { + List friends = createFriends(FRIEND_COUNT); + for (MockUser friend : friends) { + assertFalse(mockUser.friends.contains(friend)); + mockUser.friends.add(friend); + assertTrue(mockUser.friends.contains(friend)); + } + + MockUser nonFriend = createFriends(1).getFirst(); + assertFalse(mockUser.friends.contains(nonFriend)); + + for (MockUser friend : friends) { + mockUser.friends.remove(friend); + assertFalse(mockUser.friends.contains(friend)); + } + } + + @SuppressWarnings("SuspiciousMethodCalls") + @Test + public void testContainsAll() { + assertTrue(mockUser.friends.containsAll(new ArrayList<>())); + assertFalse(mockUser.friends.containsAll(List.of("not a user", "another non user"))); + List friends = createFriends(FRIEND_COUNT); + mockUser.friends.addAll(friends); + assertTrue(mockUser.friends.containsAll(friends)); + List nonFriends = createFriends(3); + List mixed = new ArrayList<>(friends.subList(0, FRIEND_COUNT - 2)); + mixed.addAll(nonFriends); + assertFalse(mockUser.friends.containsAll(mixed)); + } + + @Test + public void testClear() { + List friends = createFriends(FRIEND_COUNT); + mockUser.friends.addAll(friends); + assertEquals(FRIEND_COUNT, mockUser.friends.size()); + mockUser.friends.clear(); + assertTrue(mockUser.friends.isEmpty()); + } + + @Test + public void testRetainAll() { + List friends = createFriends(FRIEND_COUNT); + mockUser.friends.addAll(friends); + + List toRetain = friends.subList(0, FRIEND_COUNT / 2); + int nonFriends = 3; + List junk = createFriends(nonFriends); + toRetain.addAll(junk); + assertTrue(mockUser.friends.retainAll(toRetain)); + assertEquals(toRetain.size() - nonFriends, mockUser.friends.size()); + for (int i = 0; i < toRetain.size() - nonFriends; i++) { + assertTrue(mockUser.friends.contains(friends.get(i))); + } + } + + @Test + public void testToArray() { + List friends = createFriends(FRIEND_COUNT); + mockUser.friends.addAll(friends); + Object[] arr = mockUser.friends.toArray(); + assertEquals(FRIEND_COUNT, arr.length); + for (Object o : arr) { + assertTrue(friends.contains(o)); + } + + MockUser[] typedArr = mockUser.friends.toArray(new MockUser[0]); + assertEquals(FRIEND_COUNT, typedArr.length); + for (MockUser u : typedArr) { + assertTrue(friends.contains(u)); + } + } + + @Test + public void testIterator() { + List friends = createFriends(FRIEND_COUNT); + mockUser.friends.addAll(friends); + Iterator iterator = mockUser.friends.iterator(); + int count = 0; + while (iterator.hasNext()) { + assertTrue(friends.contains(iterator.next())); + count++; + } + + iterator = mockUser.friends.iterator(); + count = 0; + while (iterator.hasNext()) { + MockUser friend = iterator.next(); + assertTrue(friends.contains(friend)); + iterator.remove(); + assertFalse(mockUser.friends.contains(friend)); + count++; + } + + assertEquals(FRIEND_COUNT, count); + assertTrue(mockUser.friends.isEmpty()); + } + + @Test + public void testEqualsAndHashCode() { + MockUser anotherUser = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("another user") + .insert(InsertMode.SYNC); + + assertEquals(mockUser.friends, anotherUser.friends); + assertEquals(mockUser.friends.hashCode(), anotherUser.friends.hashCode()); + + List friends = createFriends(FRIEND_COUNT); + mockUser.friends.addAll(friends); + assertNotEquals(mockUser.friends, anotherUser.friends); + assertNotEquals(mockUser.friends.hashCode(), anotherUser.friends.hashCode()); + anotherUser.friends.addAll(friends); + assertEquals(mockUser.friends, anotherUser.friends); + assertEquals(mockUser.friends.hashCode(), anotherUser.friends.hashCode()); + } +} \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java index 5708ca00..4f8d470b 100644 --- a/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java +++ b/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -6,6 +6,7 @@ import net.staticstudios.data.mock.user.MockUserFactory; import net.staticstudios.data.mock.user.MockUserSession; import net.staticstudios.data.mock.user.MockUserSessionFactory; +import net.staticstudios.utils.RandomUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.*; public class PersistentOneToManyCollectionTest extends DataTest { + private static final int SESSION_COUNT = 20; private MockUser mockUser; private DataManager dataManager; @@ -37,34 +39,42 @@ public void setUp() { .insert(InsertMode.SYNC); } - @Test - public void testEmpty() { - assertTrue(mockUser.sessions.isEmpty()); + private List createSessions(int count) { + List sessions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + MockUserSession session = MockUserSessionFactory.builder(dataManager) + .id(UUID.randomUUID()) + .timestamp(Timestamp.from(Instant.ofEpochSecond(RandomUtils.randomInt(0, 1_000_000_000)))) + .insert(InsertMode.ASYNC); + sessions.add(session); + } + return sessions; } @Test public void testAdd() { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); + List sessions = createSessions(SESSION_COUNT); - assertNull(session.userId.get()); - mockUser.sessions.add(session); - assertEquals(mockUser.id.get(), session.userId.get()); - assertSame(mockUser, session.user.get()); - assertEquals(1, mockUser.sessions.size()); - assertSame(session, mockUser.sessions.iterator().next()); - assertEquals("[" + session + "]", mockUser.sessions.toString()); + int size = mockUser.sessions.size(); + for (MockUserSession session : sessions) { + assertNotEquals(mockUser.id.get(), session.userId.get()); + assertTrue(mockUser.sessions.add(session)); + assertEquals(mockUser.id.get(), session.userId.get()); + assertEquals(++size, mockUser.sessions.size()); + } + + for (MockUserSession session : sessions) { + assertTrue(mockUser.sessions.contains(session)); + } waitForDataPropagation(); Connection pgConnection = getConnection(); - try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_sessions\" WHERE \"user_id\" = ?")) { preparedStatement.setObject(1, mockUser.id.get()); try (ResultSet resultSet = preparedStatement.executeQuery()) { - assertEquals(1, TestUtils.getResultCount(resultSet)); + assertEquals(sessions.size(), TestUtils.getResultCount(resultSet)); } } catch (Exception e) { throw new RuntimeException(e); @@ -72,22 +82,24 @@ public void testAdd() { } @Test - public void testRemove() { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - mockUser.sessions.add(session); - waitForDataPropagation(); - assertTrue(mockUser.sessions.remove(session)); - assertFalse(mockUser.sessions.contains(session)); - assertEquals(0, mockUser.sessions.size()); + public void testAddAll() { + List sessions = createSessions(SESSION_COUNT); + + assertTrue(mockUser.sessions.addAll(sessions)); + assertEquals(SESSION_COUNT, mockUser.sessions.size()); + + for (MockUserSession session : sessions) { + assertTrue(mockUser.sessions.contains(session)); + } + waitForDataPropagation(); + Connection pgConnection = getConnection(); - try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_sessions\" WHERE \"user_id\" = ?")) { preparedStatement.setObject(1, mockUser.id.get()); try (ResultSet resultSet = preparedStatement.executeQuery()) { - assertEquals(0, TestUtils.getResultCount(resultSet)); + assertEquals(sessions.size(), TestUtils.getResultCount(resultSet)); } } catch (Exception e) { throw new RuntimeException(e); @@ -95,189 +107,86 @@ public void testRemove() { } @Test - public void testClear() { - MockUserSession session1 = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - MockUserSession session2 = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - mockUser.sessions.add(session1); - mockUser.sessions.add(session2); - waitForDataPropagation(); - mockUser.sessions.clear(); - assertTrue(mockUser.sessions.isEmpty()); + public void testRemove() { + List sessions = createSessions(SESSION_COUNT); + mockUser.sessions.addAll(sessions); + + for (MockUserSession session : sessions) { + assertTrue(mockUser.sessions.contains(session)); + } + waitForDataPropagation(); + Connection pgConnection = getConnection(); - try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_sessions\" WHERE \"user_id\" = ?")) { preparedStatement.setObject(1, mockUser.id.get()); try (ResultSet resultSet = preparedStatement.executeQuery()) { - assertEquals(0, TestUtils.getResultCount(resultSet)); + assertEquals(sessions.size(), TestUtils.getResultCount(resultSet)); } } catch (Exception e) { throw new RuntimeException(e); } - } - - @Test - public void testContains() { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - assertFalse(mockUser.sessions.contains(session)); - mockUser.sessions.add(session); - assertTrue(mockUser.sessions.contains(session)); - mockUser.sessions.remove(session); - assertFalse(mockUser.sessions.contains(session)); - } - - @Test - public void testSizeAndIsEmpty() { - assertTrue(mockUser.sessions.isEmpty()); - assertEquals(0, mockUser.sessions.size()); - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - mockUser.sessions.add(session); - assertFalse(mockUser.sessions.isEmpty()); - assertEquals(1, mockUser.sessions.size()); - mockUser.sessions.remove(session); - assertTrue(mockUser.sessions.isEmpty()); - assertEquals(0, mockUser.sessions.size()); - } - @Test - public void testIterator() { - List sessions = new ArrayList<>(); - for (int i = 0; i < 3; i++) { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - mockUser.sessions.add(session); - sessions.add(session); - } - Iterator iterator = mockUser.sessions.iterator(); - int count = 0; - while (iterator.hasNext()) { - assertTrue(sessions.contains(iterator.next())); - count++; + int size = mockUser.sessions.size(); + for (MockUserSession session : sessions) { + assertTrue(mockUser.sessions.remove(session)); + assertEquals(--size, mockUser.sessions.size()); } - assertEquals(3, count); - } - @Test - public void testIteratorRemove() { - for (int i = 0; i < 3; i++) { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - mockUser.sessions.add(session); + for (MockUserSession session : sessions) { + assertFalse(mockUser.sessions.contains(session)); } - Iterator iterator = mockUser.sessions.iterator(); - MockUserSession toRemove = iterator.next(); - iterator.remove(); - assertFalse(mockUser.sessions.contains(toRemove)); - assertEquals(2, mockUser.sessions.size()); + + assertTrue(mockUser.sessions.isEmpty()); + waitForDataPropagation(); - Connection pgConnection = getConnection(); - try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_sessions\" WHERE \"user_id\" = ?")) { preparedStatement.setObject(1, mockUser.id.get()); try (ResultSet resultSet = preparedStatement.executeQuery()) { - assertEquals(2, TestUtils.getResultCount(resultSet)); + assertEquals(0, TestUtils.getResultCount(resultSet)); } } catch (Exception e) { throw new RuntimeException(e); } } + @SuppressWarnings("SuspiciousMethodCalls") @Test - public void testToArray() { - List sessions = new ArrayList<>(); - for (int i = 0; i < 2; i++) { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - mockUser.sessions.add(session); - sessions.add(session); - } - Object[] arr = mockUser.sessions.toArray(); - assertEquals(2, arr.length); - for (Object o : arr) { - assertTrue(sessions.contains(o)); - } - MockUserSession[] typedArr = mockUser.sessions.toArray(new MockUserSession[0]); - assertEquals(2, typedArr.length); - for (MockUserSession s : typedArr) { - assertTrue(sessions.contains(s)); - } - } - - @Test - public void testToString() { - mockUser.sessions.clear(); - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - mockUser.sessions.add(session); - String expected = String.format("[%s]", session); - assertEquals(expected, mockUser.sessions.toString()); - } + public void testRemoveAll() { + List sessions = createSessions(SESSION_COUNT); + mockUser.sessions.addAll(sessions); - @Test - public void testAddAll() { - List sessions = new ArrayList<>(); - for (int i = 0; i < 3; i++) { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - sessions.add(session); - } - assertTrue(mockUser.sessions.addAll(sessions)); - assertEquals(3, mockUser.sessions.size()); for (MockUserSession session : sessions) { assertTrue(mockUser.sessions.contains(session)); } + waitForDataPropagation(); + Connection pgConnection = getConnection(); - try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_sessions\" WHERE \"user_id\" = ?")) { preparedStatement.setObject(1, mockUser.id.get()); try (ResultSet resultSet = preparedStatement.executeQuery()) { - assertEquals(3, TestUtils.getResultCount(resultSet)); + assertEquals(sessions.size(), TestUtils.getResultCount(resultSet)); } } catch (Exception e) { throw new RuntimeException(e); } - } - @Test - public void testRemoveAll() { - List sessions = new ArrayList<>(); - for (int i = 0; i < 3; i++) { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - mockUser.sessions.add(session); - sessions.add(session); - } - waitForDataPropagation(); assertTrue(mockUser.sessions.removeAll(sessions)); assertEquals(0, mockUser.sessions.size()); + for (MockUserSession session : sessions) { assertFalse(mockUser.sessions.contains(session)); } + + assertTrue(mockUser.sessions.isEmpty()); + waitForDataPropagation(); - Connection pgConnection = getConnection(); - try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_sessions\" WHERE \"user_id\" = ?")) { preparedStatement.setObject(1, mockUser.id.get()); try (ResultSet resultSet = preparedStatement.executeQuery()) { assertEquals(0, TestUtils.getResultCount(resultSet)); @@ -288,54 +197,115 @@ public void testRemoveAll() { } @Test - public void testRetainAll() { - List sessions = new ArrayList<>(); - for (int i = 0; i < 3; i++) { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); + public void testContains() { + List sessions = createSessions(SESSION_COUNT); + for (MockUserSession session : sessions) { + assertFalse(mockUser.sessions.contains(session)); mockUser.sessions.add(session); - sessions.add(session); + assertTrue(mockUser.sessions.contains(session)); } - List retain = List.of(sessions.get(0)); - assertTrue(mockUser.sessions.retainAll(retain)); - assertEquals(1, mockUser.sessions.size()); - assertTrue(mockUser.sessions.contains(sessions.get(0))); - assertFalse(mockUser.sessions.contains(sessions.get(1))); - assertFalse(mockUser.sessions.contains(sessions.get(2))); - waitForDataPropagation(); - Connection pgConnection = getConnection(); - try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM public.user_sessions WHERE user_id = ?")) { - preparedStatement.setObject(1, mockUser.id.get()); - try (ResultSet resultSet = preparedStatement.executeQuery()) { - assertEquals(1, TestUtils.getResultCount(resultSet)); - } - } catch (Exception e) { - throw new RuntimeException(e); + + MockUserSession notAddedSession = createSessions(1).getFirst(); + assertFalse(mockUser.sessions.contains(notAddedSession)); + + for (MockUserSession session : sessions) { + mockUser.sessions.remove(session); + assertFalse(mockUser.sessions.contains(session)); } } + @SuppressWarnings("SuspiciousMethodCalls") @Test - public void testToArrayTyped() { - List sessions = new ArrayList<>(); - for (int i = 0; i < 2; i++) { - MockUserSession session = MockUserSessionFactory.builder(dataManager) - .id(UUID.randomUUID()) - .timestamp(Timestamp.from(Instant.now())) - .insert(InsertMode.SYNC); - mockUser.sessions.add(session); - sessions.add(session); + public void testContainsAll() { + assertTrue(mockUser.sessions.containsAll(new ArrayList<>())); + assertFalse(mockUser.sessions.containsAll(List.of("not a session", "another non session"))); + List sessions = createSessions(SESSION_COUNT); + mockUser.sessions.addAll(sessions); + assertTrue(mockUser.sessions.containsAll(sessions)); + List nonAddedSessions = createSessions(3); + List mixedSessions = new ArrayList<>(sessions.subList(0, SESSION_COUNT - 2)); + mixedSessions.addAll(nonAddedSessions); + assertFalse(mockUser.sessions.containsAll(mixedSessions)); + } + + @Test + public void testClear() { + List sessions = createSessions(SESSION_COUNT); + mockUser.sessions.addAll(sessions); + assertEquals(SESSION_COUNT, mockUser.sessions.size()); + mockUser.sessions.clear(); + assertTrue(mockUser.sessions.isEmpty()); + } + + @Test + public void testRetainAll() { + List sessions = createSessions(SESSION_COUNT); + mockUser.sessions.addAll(sessions); + + List toRetain = sessions.subList(0, SESSION_COUNT / 2); + int nonAddedCount = 3; + List junk = createSessions(nonAddedCount); + toRetain.addAll(junk); + assertTrue(mockUser.sessions.retainAll(toRetain)); + assertEquals(toRetain.size() - nonAddedCount, mockUser.sessions.size()); + for (int i = 0; i < toRetain.size() - nonAddedCount; i++) { + assertTrue(mockUser.sessions.contains(sessions.get(i))); + } + } + + @Test + public void testToArray() { + List sessions = createSessions(SESSION_COUNT); + mockUser.sessions.addAll(sessions); + Object[] arr = mockUser.sessions.toArray(); + assertEquals(SESSION_COUNT, arr.length); + for (Object o : arr) { + assertTrue(sessions.contains(o)); } - MockUserSession[] arr = new MockUserSession[2]; - MockUserSession[] result = mockUser.sessions.toArray(arr); - assertEquals(2, result.length); - for (MockUserSession s : result) { + + MockUserSession[] typedArr = mockUser.sessions.toArray(new MockUserSession[0]); + assertEquals(SESSION_COUNT, typedArr.length); + for (MockUserSession s : typedArr) { assertTrue(sessions.contains(s)); } } - //todo: test other collection types - //todo: add/remove handlers - //todo: for many-to-many, id like to support following and followers, using the same join table. + @Test + public void testIterator() { + List sessions = createSessions(SESSION_COUNT); + mockUser.sessions.addAll(sessions); + Iterator iterator = mockUser.sessions.iterator(); + int count = 0; + while (iterator.hasNext()) { + assertTrue(sessions.contains(iterator.next())); + count++; + } + + iterator = mockUser.sessions.iterator(); + count = 0; + while (iterator.hasNext()) { + MockUserSession session = iterator.next(); + assertTrue(sessions.contains(session)); + iterator.remove(); + assertFalse(mockUser.sessions.contains(session)); + count++; + } + + assertEquals(SESSION_COUNT, count); + assertTrue(mockUser.sessions.isEmpty()); + } + + @Test + public void testEqualsAndHashCode() { + MockUser anotherMockUser = MockUserFactory.builder(dataManager) + .id(UUID.randomUUID()) + .name("another user") + .insert(InsertMode.SYNC); + assertEquals(mockUser.sessions, mockUser.sessions); + assertFalse(mockUser.sessions.equals(anotherMockUser.sessions)); + + } + + //todo: test other collection types (one to many valued collections is all thats left. this ont to many valued collection is important since it is the simplest to use, and makes things like contains very straightforward.) + //todo: test add/remove handlers } \ No newline at end of file diff --git a/src/test/java/net/staticstudios/data/misc/DataTest.java b/src/test/java/net/staticstudios/data/misc/DataTest.java index 1e56e362..f4665663 100644 --- a/src/test/java/net/staticstudios/data/misc/DataTest.java +++ b/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -22,6 +22,7 @@ import java.util.Objects; public class DataTest { + //todo: performance test static-data using: java microbenchmarking harness public static int NUM_ENVIRONMENTS = 1; public static RedisContainer redis; public static PostgreSQLContainer postgres = new PostgreSQLContainer<>( diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/src/test/java/net/staticstudios/data/mock/user/MockUser.java index d58c7ee8..4d10e541 100644 --- a/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -9,7 +9,6 @@ @Data(schema = "public", table = "users") public class MockUser extends UniqueData { //todo: cached values - //todo: @OneToMany, @ManyToMany, @ManyToOne (many to one should be used for references, not collections i think) //todo: note - maybe PC's add and remove handlers can be implemented using update handlers @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); @@ -51,7 +50,9 @@ public class MockUser extends UniqueData { @OneToMany(link = "id=user_id") public PersistentCollection sessions; - //todo: support ManyToMany + @Delete(DeleteStrategy.CASCADE) //todo: impl delete strategy for collections + @ManyToMany(link = "id=id", joinTable = "user_friends") + public PersistentCollection friends; //todo: support OneToMany Collections where the data type is not a uniquedata. in this case additional info about what table and schema to use will be required, since we will have to create this table. From 3a97c5542db265c7be9e6803399178d83ce00f6c Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Thu, 23 Oct 2025 01:31:15 -0400 Subject: [PATCH 35/75] begin work on intellij and java-c plugins. - the intellij plugin *should* be basically done. - remove v2 code - bump gradle --- annotations/build.gradle | 8 +- .../java/net/staticstudios/data/Data.java | 1 + benchmark/build.gradle | 64 + .../data/benchmark/StaticDataBenchmark.java | 34 + .../benchmark/StaticDataBenchmarkState.java | 59 + .../data/benchmark/data/SkyblockPlayer.java | 16 + .../src/jmh}/resources/log4j.properties | 2 +- build.gradle | 107 +- core/build.gradle | 155 ++ .../net/staticstudios/data/DataAccessor.java | 0 .../net/staticstudios/data/DataManager.java | 2 +- .../java/net/staticstudios/data}/Order.java | 2 +- .../data/PersistentCollection.java | 0 .../staticstudios/data/PersistentValue.java | 0 .../net/staticstudios/data/Reference.java | 0 .../staticstudios/data/SQLTransaction.java | 0 .../net/staticstudios/data/UniqueData.java | 0 .../staticstudios/data/ValueSerializer.java | 0 .../PersistentManyToManyCollectionImpl.java | 0 .../PersistentOneToManyCollectionImpl.java | 0 .../data/impl/data/PersistentValueImpl.java | 8 + .../data/impl/data/ReferenceImpl.java | 0 .../data/impl/h2/DelayedDatabaseTask.java | 0 .../data/impl/h2/H2DataAccessor.java | 0 .../H2DeleteStrategyCascadeTrigger.java | 0 .../h2/trigger/H2UpdateHandlerTrigger.java | 0 .../data/impl/pg/PostgresData.java | 0 .../data/impl/pg/PostgresListener.java | 0 .../data/impl/pg/PostgresNotification.java | 0 .../data/impl/pg/PostgresOperation.java | 0 .../data/insert/InsertContext.java | 0 .../data/parse/DDLStatement.java | 0 .../staticstudios/data/parse/ForeignKey.java | 0 .../staticstudios/data/parse/SQLBuilder.java | 0 .../staticstudios/data/parse/SQLColumn.java | 0 .../data/parse/SQLDeleteStrategyTrigger.java | 0 .../staticstudios/data/parse/SQLSchema.java | 0 .../staticstudios/data/parse/SQLTable.java | 0 .../staticstudios/data/parse/SQLTrigger.java | 0 .../data/primative/Primitive.java | 0 .../data/primative/PrimitiveBuilder.java | 0 .../data/primative/Primitives.java | 0 .../query/AbstractConditionalBuilder.java | 1 + .../data/query/AbstractQueryBuilder.java | 1 + .../staticstudios/data/query/InnerJoin.java | 0 .../net/staticstudios/data/query/Query.java | 0 .../staticstudios/data/query/QueryLike.java | 0 .../data/query/clause/AndClause.java | 0 .../data/query/clause/BetweenClause.java | 0 .../data/query/clause/Clause.java | 0 .../data/query/clause/CompositeClause.java | 0 .../data/query/clause/EqualsClause.java | 0 .../data/query/clause/GreaterThanClause.java | 0 .../clause/GreaterThanOrEqualToClause.java | 0 .../data/query/clause/InClause.java | 0 .../data/query/clause/LessThanClause.java | 0 .../query/clause/LessThanOrEqualToClause.java | 0 .../data/query/clause/LikeClause.java | 0 .../data/query/clause/NotEqualsClause.java | 0 .../data/query/clause/NotInClause.java | 0 .../data/query/clause/NotLikeClause.java | 0 .../data/query/clause/NotNullClause.java | 0 .../data/query/clause/NullClause.java | 0 .../data/query/clause/OrClause.java | 0 .../data/query/clause/ParenthesisClause.java | 0 .../data/query/clause/ValueClause.java | 0 .../data/util/ColumnMetadata.java | 0 .../data/util/ColumnValuePair.java | 0 .../data/util/ColumnValuePairs.java | 0 .../data/util/ConnectionConsumer.java | 0 .../data/util/ConnectionJedisConsumer.java | 0 .../data/util/DataSourceConfig.java | 0 .../util/EnvironmentVariableAccessor.java | 0 .../data/util/FieldInstancePair.java | 0 .../util/ForeignPersistentValueMetadata.java | 0 .../net/staticstudios/data/util/OnDelete.java | 0 .../net/staticstudios/data/util/OnUpdate.java | 0 .../util/PersistentCollectionMetadata.java | 0 ...ersistentManyToManyCollectionMetadata.java | 0 ...PersistentOneToManyCollectionMetadata.java | 0 .../data/util/PersistentValueMetadata.java | 0 .../data/util/PostgresUtils.java | 0 .../data/util/ReferenceMetadata.java | 0 .../data/util/ReflectionUtils.java | 0 .../net/staticstudios/data/util/Relation.java | 0 .../net/staticstudios/data/util/SQLUtils.java | 0 .../staticstudios/data/util/SQlStatement.java | 0 .../staticstudios/data/util/SchemaTable.java | 0 .../data/util/SimpleColumnMetadata.java | 0 .../staticstudios/data/util/StringUtils.java | 0 .../staticstudios/data/util/TaskQueue.java | 0 .../data/util/UniqueDataMetadata.java | 0 .../net/staticstudios/data/util/Value.java | 0 .../staticstudios/data/util/ValueUpdate.java | 0 .../data/util/ValueUpdateHandler.java | 0 .../ValueUpdateHandlerNonStaticException.java | 0 .../data/util/ValueUpdateHandlerWrapper.java | 0 .../staticstudios/data/util/ValueUtils.java | 0 .../staticstudios/data/CustomTypeTest.java | 0 .../PersistentManyToManyCollectionTest.java | 0 .../PersistentOneToManyCollectionTest.java | 0 .../data/PersistentValueTest.java | 0 .../staticstudios/data/PrimitivesTest.java | 0 .../net/staticstudios/data/QueryTest.java | 1 - .../net/staticstudios/data/ReferenceTest.java | 0 .../net/staticstudios/data/SQLParseTest.java | 61 +- .../staticstudios/data/ValueParseTest.java | 0 .../net/staticstudios/data/misc/DataTest.java | 0 .../data/misc/MockEnvironment.java | 0 .../data/misc/MockThreadProvider.java | 0 .../data/misc/MultiEnvironmentTest.java | 0 .../staticstudios/data/misc/TestUtils.java | 0 .../data/mock/account/AccountDetails.java | 0 .../AccountDetailsValueSerializer.java | 0 .../data/mock/account/AccountSettings.java | 0 .../AccountSettingsValueSerializer.java | 0 .../data/mock/account/MockAccount.java | 0 .../data/mock/post/MockPost.java | 0 .../data/mock/post/MockPostMetadata.java | 0 .../data/mock/user/MockUser.java | 0 .../data/mock/user/MockUserSession.java | 0 .../data/mock/user/MockUserSettings.java | 0 .../booleanprimitive/BooleanWrapper.java | 0 .../BooleanWrapperDataClass.java | 0 .../BooleanWrapperValueSerializer.java | 0 .../bytearrayprimitive/ByteArrayWrapper.java | 0 .../ByteArrayWrapperDataClass.java | 0 .../ByteArrayWrapperValueSerializer.java | 0 .../doubleprimitive/DoubleWrapper.java | 0 .../DoubleWrapperDataClass.java | 0 .../DoubleWrapperValueSerializer.java | 0 .../wrapper/floatprimitive/FloatWrapper.java | 0 .../floatprimitive/FloatWrapperDataClass.java | 0 .../FloatWrapperValueSerializer.java | 0 .../integerprimitive/IntegerWrapper.java | 0 .../IntegerWrapperDataClass.java | 0 .../IntegerWrapperValueSerializer.java | 0 .../wrapper/longprimitive/LongWrapper.java | 0 .../longprimitive/LongWrapperDataClass.java | 0 .../LongWrapperValueSerializer.java | 0 .../stringprimitive/StringWrapper.java | 0 .../StringWrapperDataClass.java | 0 .../StringWrapperValueSerializer.java | 0 .../timestampprimitive/TimestampWrapper.java | 0 .../TimestampWrapperDataClass.java | 0 .../TimestampWrapperValueSerializer.java | 0 .../wrapper/uuidprimitive/UUIDWrapper.java | 0 .../uuidprimitive/UUIDWrapperDataClass.java | 0 .../UUIDWrapperValueSerializer.java | 0 .../src/test}/resources/log4j.properties | 0 gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 41 +- gradlew.bat | 35 +- intellij-plugin/build.gradle | 26 + .../data/ide/intellij/Constants.java | 16 + .../ide/intellij/DataPsiAugmentProvider.java | 273 ++ .../ide/intellij/SyntheticBuilderClass.java | 138 + .../data/ide/intellij/SyntheticMethod.java | 111 + .../data/ide/intellij/Utils.java | 81 + .../ide/intellij/query/NumericClause.java | 14 + .../ide/intellij/query/QueryBuilderUtils.java | 77 + .../data/ide/intellij/query/QueryClause.java | 16 + .../query/clause/IsBetweenClause.java | 20 + .../ide/intellij/query/clause/IsClause.java | 26 + .../query/clause/IsGreaterThanClause.java | 20 + .../clause/IsGreaterThanOrEqualToClause.java | 20 + .../query/clause/IsLessThanClause.java | 20 + .../clause/IsLessThanOrEqualToClause.java | 20 + .../intellij/query/clause/IsLikeClause.java | 27 + .../query/clause/IsNotBetweenClause.java | 20 + .../intellij/query/clause/IsNotClause.java | 26 + .../query/clause/IsNotLikeClause.java | 27 + .../query/clause/IsNotNullClause.java | 27 + .../intellij/query/clause/IsNullClause.java | 27 + .../src/main/resources/META-INF/plugin.xml | 15 + javac-plugin/build.gradle | 32 + .../compiler/javac/StaticDataJavacPlugin.java | 102 + .../services/com.sun.source.util.Plugin | 1 + .../net/staticstudios/data/CachedValue.java | 285 -- .../net/staticstudios/data/DataManager.java | 1148 -------- .../data/PersistentCollection.java | 187 -- .../staticstudios/data/PersistentValue.java | 343 --- .../net/staticstudios/data/Reference.java | 220 -- .../net/staticstudios/data/UniqueData.java | 157 -- .../staticstudios/data/ValueSerializer.java | 63 - .../net/staticstudios/data/data/Data.java | 35 - .../staticstudios/data/data/DataHolder.java | 21 - .../staticstudios/data/data/Deletable.java | 21 - .../staticstudios/data/data/InitialValue.java | 9 - .../data/data/collection/CollectionEntry.java | 12 - .../collection/CollectionEntryIdentifier.java | 55 - .../PersistentCollectionChangeHandler.java | 10 - .../PersistentManyToManyCollection.java | 407 --- .../PersistentUniqueDataCollection.java | 409 --- .../collection/PersistentValueCollection.java | 374 --- .../SimplePersistentCollection.java | 80 - .../data/data/value/InitialCachedValue.java | 22 - .../data/value/InitialPersistentValue.java | 22 - .../staticstudios/data/data/value/Value.java | 22 - .../data/impl/CachedValueManager.java | 141 - .../impl/PersistentCollectionManager.java | 1074 -------- .../data/impl/PersistentValueManager.java | 387 --- .../data/impl/pg/PostgresListener.java | 190 -- .../net/staticstudios/data/key/CellKey.java | 48 - .../staticstudios/data/key/CollectionKey.java | 39 - .../net/staticstudios/data/key/DataKey.java | 37 - .../staticstudios/data/key/DatabaseKey.java | 11 - .../net/staticstudios/data/key/RedisKey.java | 80 - .../data/key/UniqueIdentifier.java | 55 - .../data/primative/Primitive.java | 47 - .../data/primative/PrimitiveBuilder.java | 60 - .../data/primative/Primitives.java | 143 - .../staticstudios/data/util/BatchInsert.java | 124 - .../staticstudios/data/util/CacheEntry.java | 14 - .../data/util/DataDoesNotExistException.java | 7 - .../data/util/DeleteContext.java | 11 - .../data/util/DeletionStrategy.java | 20 - .../data/util/InsertContext.java | 16 - .../data/util/InsertionStrategy.java | 12 - .../data/util/JunctionTable.java | 70 - .../data/util/ReflectionUtils.java | 24 - .../staticstudios/data/util/SQLLogger.java | 12 - .../data/util/ValueUpdateHandler.java | 11 - .../staticstudios/data/CachedValueTest.java | 226 -- .../net/staticstudios/data/DeletionTest.java | 301 --- .../net/staticstudios/data/InsertionTest.java | 69 - .../data/PersistentCollectionTest.java | 2382 ----------------- .../data/PersistentValueTest.java | 354 --- .../data/PostgresListenerTest.java | 108 - .../staticstudios/data/PrimitivesTest.java | 415 --- .../net/staticstudios/data/ReferenceTest.java | 127 - .../net/staticstudios/data/misc/DataTest.java | 112 - .../data/misc/MockThreadProvider.java | 125 - .../data/mock/cachedvalue/RedditUser.java | 72 - .../data/mock/deletions/MinecraftServer.java | 30 - .../data/mock/deletions/MinecraftSkin.java | 30 - .../deletions/MinecraftUserStatistics.java | 19 - ...ecraftUserWithCascadeDeletionStrategy.java | 37 - ...craftUserWithNoActionDeletionStrategy.java | 37 - ...necraftUserWithUnlinkDeletionStrategy.java | 37 - .../mock/insertions/TwitchChatMessage.java | 23 - .../data/mock/insertions/TwitchUser.java | 25 - .../persistentcollection/FacebookPost.java | 51 - .../persistentcollection/FacebookUser.java | 60 - .../mock/persistentvalue/DiscordUser.java | 66 - .../persistentvalue/DiscordUserSettings.java | 32 - .../primative/BooleanPrimitiveTestObject.java | 26 - .../ByteArrayPrimitiveTestObject.java | 26 - .../primative/BytePrimitiveTestObject.java | 26 - .../CharacterPrimitiveTestObject.java | 26 - .../primative/DoublePrimitiveTestObject.java | 26 - .../primative/FloatPrimitiveTestObject.java | 26 - .../primative/IntegerPrimitiveTestObject.java | 26 - .../primative/LongPrimitiveTestObject.java | 26 - .../primative/ShortPrimitiveTestObject.java | 26 - .../primative/StringPrimitiveTestObject.java | 26 - .../TimestampPrimitiveTestObject.java | 27 - .../primative/UUIDPrimitiveTestObject.java | 26 - .../data/mock/reference/SnapchatUser.java | 47 - .../mock/reference/SnapchatUserSettings.java | 37 - .../data/processor/DataProcessor.java | 2 +- .../data/processor/QueryFactory.java | 2 +- settings.gradle | 6 +- .../data/impl/pg/PostgresData.java | 9 - .../data/impl/pg/PostgresNotification.java | 50 - .../data/impl/pg/PostgresOperation.java | 7 - .../data/util/ConnectionConsumer.java | 8 - .../data/util/ConnectionJedisConsumer.java | 10 - .../data/util/DataSourceConfig.java | 12 - .../data/util/PostgresUtils.java | 22 - .../staticstudios/data/util/TaskQueue.java | 111 - .../staticstudios/data/util/ValueUpdate.java | 6 - .../data/misc/MockEnvironment.java | 10 - .../staticstudios/data/misc/TestUtils.java | 26 - 275 files changed, 1651 insertions(+), 12085 deletions(-) create mode 100644 benchmark/build.gradle create mode 100644 benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java create mode 100644 benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmarkState.java create mode 100644 benchmark/src/jmh/java/net/staticstudios/data/benchmark/data/SkyblockPlayer.java rename {src/test => benchmark/src/jmh}/resources/log4j.properties (86%) create mode 100644 core/build.gradle rename {src => core/src}/main/java/net/staticstudios/data/DataAccessor.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/DataManager.java (99%) rename {src/main/java/net/staticstudios/data/query => core/src/main/java/net/staticstudios/data}/Order.java (58%) rename {src => core/src}/main/java/net/staticstudios/data/PersistentCollection.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/PersistentValue.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/Reference.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/SQLTransaction.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/UniqueData.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/ValueSerializer.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java (97%) rename {src => core/src}/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java (100%) rename {oldsrc/og => core/src/main}/java/net/staticstudios/data/impl/pg/PostgresData.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/impl/pg/PostgresListener.java (100%) rename {oldsrc/og => core/src/main}/java/net/staticstudios/data/impl/pg/PostgresNotification.java (100%) rename {oldsrc/og => core/src/main}/java/net/staticstudios/data/impl/pg/PostgresOperation.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/insert/InsertContext.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/parse/DDLStatement.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/parse/ForeignKey.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/parse/SQLBuilder.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/parse/SQLColumn.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/parse/SQLSchema.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/parse/SQLTable.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/parse/SQLTrigger.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/primative/Primitive.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/primative/Primitives.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java (98%) rename {src => core/src}/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java (99%) rename {src => core/src}/main/java/net/staticstudios/data/query/InnerJoin.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/Query.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/QueryLike.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/AndClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/BetweenClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/Clause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/CompositeClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/EqualsClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/InClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/LessThanClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/LikeClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/NotInClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/NotLikeClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/NotNullClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/NullClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/OrClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/query/clause/ValueClause.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ColumnMetadata.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ColumnValuePair.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ColumnValuePairs.java (100%) rename {oldsrc/og => core/src/main}/java/net/staticstudios/data/util/ConnectionConsumer.java (100%) rename {oldsrc/og => core/src/main}/java/net/staticstudios/data/util/ConnectionJedisConsumer.java (100%) rename {oldsrc/og => core/src/main}/java/net/staticstudios/data/util/DataSourceConfig.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/FieldInstancePair.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/OnDelete.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/OnUpdate.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/PersistentValueMetadata.java (100%) rename {oldsrc/og => core/src/main}/java/net/staticstudios/data/util/PostgresUtils.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ReferenceMetadata.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ReflectionUtils.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/Relation.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/SQLUtils.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/SQlStatement.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/SchemaTable.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/StringUtils.java (100%) rename {oldsrc/og => core/src/main}/java/net/staticstudios/data/util/TaskQueue.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/UniqueDataMetadata.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/Value.java (100%) rename {oldsrc/og => core/src/main}/java/net/staticstudios/data/util/ValueUpdate.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ValueUpdateHandler.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java (100%) rename {src => core/src}/main/java/net/staticstudios/data/util/ValueUtils.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/CustomTypeTest.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/PersistentValueTest.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/PrimitivesTest.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/QueryTest.java (99%) rename {src => core/src}/test/java/net/staticstudios/data/ReferenceTest.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/SQLParseTest.java (67%) rename {src => core/src}/test/java/net/staticstudios/data/ValueParseTest.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/misc/DataTest.java (100%) rename {oldsrc/ogtest => core/src/test}/java/net/staticstudios/data/misc/MockEnvironment.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/misc/MockThreadProvider.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java (100%) rename {oldsrc/ogtest => core/src/test}/java/net/staticstudios/data/misc/TestUtils.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/account/AccountDetails.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/account/AccountSettings.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/account/MockAccount.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/post/MockPost.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/user/MockUser.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/user/MockUserSession.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/user/MockUserSettings.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java (100%) rename {src => core/src}/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java (100%) rename {oldsrc/ogtest => core/src/test}/resources/log4j.properties (100%) create mode 100644 intellij-plugin/build.gradle create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Constants.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/DataPsiAugmentProvider.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticBuilderClass.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticMethod.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Utils.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/NumericClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryBuilderUtils.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsBetweenClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanOrEqualToClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanOrEqualToClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotBetweenClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotNullClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNullClause.java create mode 100644 intellij-plugin/src/main/resources/META-INF/plugin.xml create mode 100644 javac-plugin/build.gradle create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java create mode 100644 javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin delete mode 100644 oldsrc/og/java/net/staticstudios/data/CachedValue.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/DataManager.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/PersistentCollection.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/PersistentValue.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/Reference.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/UniqueData.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/ValueSerializer.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/Data.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/DataHolder.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/Deletable.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/InitialValue.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntry.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/value/InitialCachedValue.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/data/value/Value.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/impl/CachedValueManager.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/impl/PersistentValueManager.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresListener.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/key/CellKey.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/key/CollectionKey.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/key/DataKey.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/key/DatabaseKey.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/key/RedisKey.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/key/UniqueIdentifier.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/primative/Primitive.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/primative/Primitives.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/BatchInsert.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/CacheEntry.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/DataDoesNotExistException.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/DeleteContext.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/DeletionStrategy.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/InsertContext.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/InsertionStrategy.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/JunctionTable.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/ReflectionUtils.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/SQLLogger.java delete mode 100644 oldsrc/og/java/net/staticstudios/data/util/ValueUpdateHandler.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/CachedValueTest.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/DeletionTest.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/InsertionTest.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/PersistentValueTest.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/PostgresListenerTest.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/PrimitivesTest.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/ReferenceTest.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/misc/DataTest.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java delete mode 100644 oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java delete mode 100644 src/main/java/net/staticstudios/data/impl/pg/PostgresData.java delete mode 100644 src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java delete mode 100644 src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java delete mode 100644 src/main/java/net/staticstudios/data/util/ConnectionConsumer.java delete mode 100644 src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java delete mode 100644 src/main/java/net/staticstudios/data/util/DataSourceConfig.java delete mode 100644 src/main/java/net/staticstudios/data/util/PostgresUtils.java delete mode 100644 src/main/java/net/staticstudios/data/util/TaskQueue.java delete mode 100644 src/main/java/net/staticstudios/data/util/ValueUpdate.java delete mode 100644 src/test/java/net/staticstudios/data/misc/MockEnvironment.java delete mode 100644 src/test/java/net/staticstudios/data/misc/TestUtils.java diff --git a/annotations/build.gradle b/annotations/build.gradle index aa09b1d3..75526fba 100644 --- a/annotations/build.gradle +++ b/annotations/build.gradle @@ -16,4 +16,10 @@ dependencies { test { useJUnitPlatform() -} \ No newline at end of file +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} diff --git a/annotations/src/main/java/net/staticstudios/data/Data.java b/annotations/src/main/java/net/staticstudios/data/Data.java index 834cb1e2..457aeefc 100644 --- a/annotations/src/main/java/net/staticstudios/data/Data.java +++ b/annotations/src/main/java/net/staticstudios/data/Data.java @@ -1,4 +1,5 @@ package net.staticstudios.data; +//todo: the annotations package can be removed and everything can be put back into core once the processor is gone import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/benchmark/build.gradle b/benchmark/build.gradle new file mode 100644 index 00000000..38b0b6b8 --- /dev/null +++ b/benchmark/build.gradle @@ -0,0 +1,64 @@ +plugins { + id 'java' + id 'me.champeau.jmh' version '0.7.3' +} + +repositories { + mavenCentral() + maven { + name = "StaticStudios" + url = 'https://repo.staticstudios.net/snapshots/' + } +} + +dependencies { + implementation("org.openjdk.jmh:jmh-core:1.37") + implementation("org.openjdk.jmh:jmh-generator-annprocess:1.37") + implementation(project(":core")) + implementation 'net.staticstudios:static-utils:1.0.6-SNAPSHOT' + implementation("org.testcontainers:postgresql:1.19.8") + implementation("com.redis:testcontainers-redis:2.2.2") + implementation("org.slf4j:slf4j-log4j12:2.0.16") + + annotationProcessor project(":processor") + compileOnly project(":processor") +} + +def generatedSourcesDir = file("$buildDir/generated/sources/annotations") +sourceSets.main.java.srcDir(generatedSourcesDir) + + +tasks.register('runAnnotationProcessor', JavaCompile) { + group = 'build' + description = 'Run annotation processor manually (codegen only)' + + source = sourceSets.main.java + classpath = configurations.compileClasspath + configurations.annotationProcessor + destinationDir = file("$buildDir/tmp/classes/manual") // arbitrary, ignored with -proc:only + + options.annotationProcessorPath = configurations.annotationProcessor + options.compilerArgs += [ + '-proc:only', + '-processor', 'net.staticstudios.data.processor.DataProcessor', + '-s', generatedSourcesDir + ] +} + +tasks.named('jmhRunBytecodeGenerator') { + outputs.upToDateWhen { false } +} + +tasks.named('jmh') { + jvmArgs = [ + '-Xms1g', + '-Xmx1g', + '-XX:+AlwaysPreTouch', + '-Djmh.ignoreLock=true' + ] +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} diff --git a/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java new file mode 100644 index 00000000..95b94a59 --- /dev/null +++ b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.benchmark; + +import org.openjdk.jmh.annotations.*; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Fork(1) +@Warmup(iterations = 3) +@Measurement(iterations = 10) +public class StaticDataBenchmark { + + @Benchmark + public void sampleBenchmark(StaticDataBenchmarkState state) { + // Sample benchmark method + int sum = 0; + for (int i = 0; i < 1000; i++) { + sum += i; + } + } + + @Benchmark + public void testPersistentValueRead(StaticDataBenchmarkState state) { + } + + @Benchmark + public void testUniqueDataInsertAsync(StaticDataBenchmarkState state) { + } + + @Benchmark + public void testPersistentValueWrite(StaticDataBenchmarkState state) { + } +} diff --git a/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmarkState.java b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmarkState.java new file mode 100644 index 00000000..8c473b61 --- /dev/null +++ b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmarkState.java @@ -0,0 +1,59 @@ +package net.staticstudios.data.benchmark; + +import com.redis.testcontainers.RedisContainer; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.benchmark.data.SkyblockPlayer; +import net.staticstudios.data.util.DataSourceConfig; +import net.staticstudios.utils.ThreadUtilProvider; +import net.staticstudios.utils.ThreadUtils; +import org.openjdk.jmh.annotations.*; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +@State(Scope.Benchmark) +public class StaticDataBenchmarkState { + public static RedisContainer redis; + private static PostgreSQLContainer postgres; + + @Setup(Level.Trial) + public void setup() throws Exception { + if (postgres == null) { + redis = new RedisContainer(DockerImageName.parse("redis:6.2.6")); + redis.start(); + + redis.execInContainer("redis-cli", "config", "set", "notify-keyspace-events", "KEA"); + + postgres = new PostgreSQLContainer<>("postgres:16.2") + .withExposedPorts(5432) + .withPassword("password") + .withUsername("postgres") + .withDatabaseName("postgres"); + postgres.start(); + + ThreadUtils.setProvider(ThreadUtilProvider.builder().build()); + DataSourceConfig dataSourceConfig = new DataSourceConfig( + postgres.getHost(), + postgres.getFirstMappedPort(), + postgres.getDatabaseName(), + postgres.getUsername(), + postgres.getPassword(), + redis.getHost(), + redis.getRedisPort()); + + DataManager dataManager = new DataManager(dataSourceConfig, true); + dataManager.load(SkyblockPlayer.class); + } + } + + @TearDown(Level.Trial) + public void tearDown() throws Exception { + ThreadUtils.shutdown(); + if (postgres != null) { + postgres.stop(); + } + + if (redis != null) { + redis.stop(); + } + } +} diff --git a/benchmark/src/jmh/java/net/staticstudios/data/benchmark/data/SkyblockPlayer.java b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/data/SkyblockPlayer.java new file mode 100644 index 00000000..fee78c26 --- /dev/null +++ b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/data/SkyblockPlayer.java @@ -0,0 +1,16 @@ +package net.staticstudios.data.benchmark.data; + +import net.staticstudios.data.*; + +import java.util.UUID; + +@Data(schema = "skyblock", table = "players") +public class SkyblockPlayer extends UniqueData { + + @IdColumn(name = "id") + public PersistentValue id; + + + @Column(name = "name") + public PersistentValue name; +} diff --git a/src/test/resources/log4j.properties b/benchmark/src/jmh/resources/log4j.properties similarity index 86% rename from src/test/resources/log4j.properties rename to benchmark/src/jmh/resources/log4j.properties index c87dc64f..d54b8ca4 100644 --- a/src/test/resources/log4j.properties +++ b/benchmark/src/jmh/resources/log4j.properties @@ -1,5 +1,5 @@ log4j.rootLogger=INFO, STDOUT -log4j.logger.net.staticstudios=TRACE +log4j.logger.net.staticstudios=INFO log4j.logger.deng=INFO log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout diff --git a/build.gradle b/build.gradle index a13f542b..45220657 100644 --- a/build.gradle +++ b/build.gradle @@ -1,112 +1,13 @@ plugins { - id 'java-library' - id 'maven-publish' - id 'com.gradleup.shadow' version '8.3.3' + id 'java' } group = 'net.staticstudios' version = '3.0.0-SNAPSHOT' -repositories { - mavenCentral() - maven { - name = "StaticStudios" - url = 'https://repo.staticstudios.net/private/' - // To set up credentials go to the .gradle directory in you user home and create a gradle.properties file. - // There you put a "StaticStudiosUsername" field with your username and a "StaticStudiosPassword" field with the repo secret. - credentials(org.gradle.api.credentials.PasswordCredentials.class) - } -} - -dependencies { - implementation 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.9' - implementation 'com.zaxxer:HikariCP:5.1.0' - implementation 'redis.clients:jedis:5.1.2' - implementation 'net.staticstudios:static-utils:1.0.1' - implementation 'com.h2database:h2:2.3.232' -// implementation 'org.xerial:sqlite-jdbc:3.45.1.0' - implementation 'org.jetbrains:annotations:24.0.1' - api project(":annotations") - implementation project(":annotations") - annotationProcessor project(":processor") - compileOnly project(":processor") - - - testAnnotationProcessor project(":processor") - testCompileOnly project(":processor") - -// testImplementation(platform('org.junit:junit-bom:5.10.3')) -// testImplementation('org.junit.jupiter:junit-jupiter') -// testImplementation('org.junit-pioneer:junit-pioneer:2.3.0'); -// testRuntimeOnly('org.junit.platform:junit-platform-launcher') - - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' - - testImplementation("org.testcontainers:postgresql:1.19.8") - testImplementation("com.redis:testcontainers-redis:2.2.2") - testImplementation("org.slf4j:slf4j-log4j12:2.0.16") -} - -tasks.named('build') { - dependsOn(shadowJar) -} - -tasks.named("publish") { - dependsOn(build) -} -test { - useJUnitPlatform() -} - -java { - withSourcesJar() - withJavadocJar() -} - -javadoc { - options.tags = ["implSpec", "apiNote", "implNote"] -} - -publishing { - repositories { - maven { - credentials(org.gradle.api.credentials.PasswordCredentials.class) - name = "StaticStudios" - setUrl("https://repo.staticstudios.net/private/") - } - } - publications { - maven(MavenPublication) { - from components.java - pom { - name = 'Static Data' - description = 'Data library used by StaticStudios.' - url = 'https://github.com/StaticStudios/static-data' - developers { - developer { - id = 'staticstudios' - name = 'Static Studios' - email = 'support@staticstudios.net' - } - } - scm { - connection = 'scm:git:git://github.com/StaticStudios/static-data.git' - developerConnection = 'scm:git:ssh://github.com:StaticStudios/static-data.git' - url = 'https://github.com/StaticStudios/static-data' - } - } - } - } -} - -def targetJavaVersion = 21 java { - def javaVersion = JavaVersion.toVersion(targetJavaVersion) - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - if (JavaVersion.current() < javaVersion) { - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + toolchain { + languageVersion = JavaLanguageVersion.of(21) } -} +} \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 00000000..77f5f521 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,155 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'com.gradleup.shadow' version '8.3.3' +} + +group = 'net.staticstudios' +version = '3.0.0-SNAPSHOT' + +repositories { + mavenCentral() + maven { + name = "StaticStudios" + url = 'https://repo.staticstudios.net/snapshots/' + } +} + +dependencies { + implementation 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.9' + implementation 'com.zaxxer:HikariCP:5.1.0' + implementation 'redis.clients:jedis:5.1.2' + implementation 'net.staticstudios:static-utils:1.0.6-SNAPSHOT' + implementation 'com.h2database:h2:2.3.232' + implementation 'org.jetbrains:annotations:24.0.1' + api project(":annotations") + implementation project(":annotations") + annotationProcessor project(":processor") + compileOnly project(":processor") + + + testAnnotationProcessor project(":processor") + testCompileOnly project(":processor") + +// testImplementation(platform('org.junit:junit-bom:5.10.3')) +// testImplementation('org.junit.jupiter:junit-jupiter') +// testImplementation('org.junit-pioneer:junit-pioneer:2.3.0'); +// testRuntimeOnly('org.junit.platform:junit-platform-launcher') + + 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" + + testImplementation("org.testcontainers:postgresql:1.19.8") + testImplementation("com.redis:testcontainers-redis:2.2.2") + testImplementation("org.slf4j:slf4j-log4j12:2.0.16") +// compileOnly project(':javac-plugin') //TODO: java-c +// annotationProcessor project(':javac-plugin') +// testCompileOnly project(':javac-plugin') +// testAnnotationProcessor project(':javac-plugin') +} + +//tasks.named('compileJava').configure { +// options.fork = true +// options.forkOptions.jvmArgs += [ +// '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', +// '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' +// ] +// +// options.compilerArgs += [ +// '-Xplugin:StaticDataJavacPlugin' +// ] +//} +// +//tasks.named('compileTestJava').configure { +// options.fork = true +// options.forkOptions.jvmArgs += [ +// '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', +// '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', +// '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' +// ] +// +// options.compilerArgs += [ +// '-Xplugin:StaticDataJavacPlugin' +// ] +//} + + +tasks.named('build') { + dependsOn(shadowJar) + +} + +tasks.named("publish") { + dependsOn(build) +} + +test { + useJUnitPlatform() +} + +java { + withSourcesJar() + withJavadocJar() +} + +javadoc { + options.tags = ["implSpec", "apiNote", "implNote"] +} + +publishing { + repositories { + maven { + credentials(org.gradle.api.credentials.PasswordCredentials.class) + name = "StaticStudios" + setUrl("https://repo.staticstudios.net/private/") + } + } + publications { + maven(MavenPublication) { + from components.java + pom { + name = 'Static Data' + description = 'Data library used by StaticStudios.' + url = 'https://github.com/StaticStudios/static-data' + developers { + developer { + id = 'staticstudios' + name = 'Static Studios' + email = 'support@staticstudios.net' + } + } + scm { + connection = 'scm:git:git://github.com/StaticStudios/static-data.git' + developerConnection = 'scm:git:ssh://github.com:StaticStudios/static-data.git' + url = 'https://github.com/StaticStudios/static-data' + } + } + } + } +} + +def targetJavaVersion = 21 +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) +} diff --git a/src/main/java/net/staticstudios/data/DataAccessor.java b/core/src/main/java/net/staticstudios/data/DataAccessor.java similarity index 100% rename from src/main/java/net/staticstudios/data/DataAccessor.java rename to core/src/main/java/net/staticstudios/data/DataAccessor.java diff --git a/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java similarity index 99% rename from src/main/java/net/staticstudios/data/DataManager.java rename to core/src/main/java/net/staticstudios/data/DataManager.java index b64b2618..b761ccd9 100644 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -50,7 +50,7 @@ public DataManager(DataSourceConfig dataSourceConfig) { public DataManager(DataSourceConfig dataSourceConfig, boolean setGlobal) { if (setGlobal) { - if (DataManager.useGlobal == false) { + if (Boolean.FALSE.equals(DataManager.useGlobal)) { throw new IllegalStateException("DataManager global instance has been disabled"); } Preconditions.checkArgument(instance == null, "DataManager instance already exists"); diff --git a/src/main/java/net/staticstudios/data/query/Order.java b/core/src/main/java/net/staticstudios/data/Order.java similarity index 58% rename from src/main/java/net/staticstudios/data/query/Order.java rename to core/src/main/java/net/staticstudios/data/Order.java index b3318c7b..3fc1067d 100644 --- a/src/main/java/net/staticstudios/data/query/Order.java +++ b/core/src/main/java/net/staticstudios/data/Order.java @@ -1,4 +1,4 @@ -package net.staticstudios.data.query; +package net.staticstudios.data; public enum Order { ASCENDING, diff --git a/src/main/java/net/staticstudios/data/PersistentCollection.java b/core/src/main/java/net/staticstudios/data/PersistentCollection.java similarity index 100% rename from src/main/java/net/staticstudios/data/PersistentCollection.java rename to core/src/main/java/net/staticstudios/data/PersistentCollection.java diff --git a/src/main/java/net/staticstudios/data/PersistentValue.java b/core/src/main/java/net/staticstudios/data/PersistentValue.java similarity index 100% rename from src/main/java/net/staticstudios/data/PersistentValue.java rename to core/src/main/java/net/staticstudios/data/PersistentValue.java diff --git a/src/main/java/net/staticstudios/data/Reference.java b/core/src/main/java/net/staticstudios/data/Reference.java similarity index 100% rename from src/main/java/net/staticstudios/data/Reference.java rename to core/src/main/java/net/staticstudios/data/Reference.java diff --git a/src/main/java/net/staticstudios/data/SQLTransaction.java b/core/src/main/java/net/staticstudios/data/SQLTransaction.java similarity index 100% rename from src/main/java/net/staticstudios/data/SQLTransaction.java rename to core/src/main/java/net/staticstudios/data/SQLTransaction.java diff --git a/src/main/java/net/staticstudios/data/UniqueData.java b/core/src/main/java/net/staticstudios/data/UniqueData.java similarity index 100% rename from src/main/java/net/staticstudios/data/UniqueData.java rename to core/src/main/java/net/staticstudios/data/UniqueData.java diff --git a/src/main/java/net/staticstudios/data/ValueSerializer.java b/core/src/main/java/net/staticstudios/data/ValueSerializer.java similarity index 100% rename from src/main/java/net/staticstudios/data/ValueSerializer.java rename to core/src/main/java/net/staticstudios/data/ValueSerializer.java diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java rename to core/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java rename to core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java diff --git a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java similarity index 97% rename from src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java rename to core/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java index b5290ee6..75ad3c9a 100644 --- a/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -149,4 +149,12 @@ private List getIdColumnLinks() { } return Collections.emptyList(); } + + @Override + public String toString() { + if (holder.isDeleted()) { + return "[DELETED]"; + } + return String.valueOf(get()); + } } diff --git a/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java rename to core/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java diff --git a/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java b/core/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java rename to core/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java diff --git a/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java b/core/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java rename to core/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java diff --git a/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java rename to core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java diff --git a/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java rename to core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java diff --git a/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresData.java b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresData.java similarity index 100% rename from oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresData.java rename to core/src/main/java/net/staticstudios/data/impl/pg/PostgresData.java diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java similarity index 100% rename from src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java rename to core/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java diff --git a/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresNotification.java b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java similarity index 100% rename from oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresNotification.java rename to core/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java diff --git a/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresOperation.java b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java similarity index 100% rename from oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresOperation.java rename to core/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java diff --git a/src/main/java/net/staticstudios/data/insert/InsertContext.java b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java similarity index 100% rename from src/main/java/net/staticstudios/data/insert/InsertContext.java rename to core/src/main/java/net/staticstudios/data/insert/InsertContext.java diff --git a/src/main/java/net/staticstudios/data/parse/DDLStatement.java b/core/src/main/java/net/staticstudios/data/parse/DDLStatement.java similarity index 100% rename from src/main/java/net/staticstudios/data/parse/DDLStatement.java rename to core/src/main/java/net/staticstudios/data/parse/DDLStatement.java diff --git a/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/core/src/main/java/net/staticstudios/data/parse/ForeignKey.java similarity index 100% rename from src/main/java/net/staticstudios/data/parse/ForeignKey.java rename to core/src/main/java/net/staticstudios/data/parse/ForeignKey.java diff --git a/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java similarity index 100% rename from src/main/java/net/staticstudios/data/parse/SQLBuilder.java rename to core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java diff --git a/src/main/java/net/staticstudios/data/parse/SQLColumn.java b/core/src/main/java/net/staticstudios/data/parse/SQLColumn.java similarity index 100% rename from src/main/java/net/staticstudios/data/parse/SQLColumn.java rename to core/src/main/java/net/staticstudios/data/parse/SQLColumn.java diff --git a/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java b/core/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java similarity index 100% rename from src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java rename to core/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java diff --git a/src/main/java/net/staticstudios/data/parse/SQLSchema.java b/core/src/main/java/net/staticstudios/data/parse/SQLSchema.java similarity index 100% rename from src/main/java/net/staticstudios/data/parse/SQLSchema.java rename to core/src/main/java/net/staticstudios/data/parse/SQLSchema.java diff --git a/src/main/java/net/staticstudios/data/parse/SQLTable.java b/core/src/main/java/net/staticstudios/data/parse/SQLTable.java similarity index 100% rename from src/main/java/net/staticstudios/data/parse/SQLTable.java rename to core/src/main/java/net/staticstudios/data/parse/SQLTable.java diff --git a/src/main/java/net/staticstudios/data/parse/SQLTrigger.java b/core/src/main/java/net/staticstudios/data/parse/SQLTrigger.java similarity index 100% rename from src/main/java/net/staticstudios/data/parse/SQLTrigger.java rename to core/src/main/java/net/staticstudios/data/parse/SQLTrigger.java diff --git a/src/main/java/net/staticstudios/data/primative/Primitive.java b/core/src/main/java/net/staticstudios/data/primative/Primitive.java similarity index 100% rename from src/main/java/net/staticstudios/data/primative/Primitive.java rename to core/src/main/java/net/staticstudios/data/primative/Primitive.java diff --git a/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java b/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java similarity index 100% rename from src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java rename to core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java diff --git a/src/main/java/net/staticstudios/data/primative/Primitives.java b/core/src/main/java/net/staticstudios/data/primative/Primitives.java similarity index 100% rename from src/main/java/net/staticstudios/data/primative/Primitives.java rename to core/src/main/java/net/staticstudios/data/primative/Primitives.java diff --git a/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java b/core/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java similarity index 98% rename from src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java rename to core/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java index 095cb97d..42ae745b 100644 --- a/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java +++ b/core/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java @@ -1,6 +1,7 @@ package net.staticstudios.data.query; import com.google.common.base.Preconditions; +import net.staticstudios.data.Order; import net.staticstudios.data.UniqueData; import net.staticstudios.data.query.clause.AndClause; import net.staticstudios.data.query.clause.OrClause; diff --git a/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java b/core/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java similarity index 99% rename from src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java rename to core/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java index 4add26a6..a7aae215 100644 --- a/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java +++ b/core/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java @@ -2,6 +2,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.DataManager; +import net.staticstudios.data.Order; import net.staticstudios.data.UniqueData; import net.staticstudios.data.query.clause.AndClause; import net.staticstudios.data.query.clause.Clause; diff --git a/src/main/java/net/staticstudios/data/query/InnerJoin.java b/core/src/main/java/net/staticstudios/data/query/InnerJoin.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/InnerJoin.java rename to core/src/main/java/net/staticstudios/data/query/InnerJoin.java diff --git a/src/main/java/net/staticstudios/data/query/Query.java b/core/src/main/java/net/staticstudios/data/query/Query.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/Query.java rename to core/src/main/java/net/staticstudios/data/query/Query.java diff --git a/src/main/java/net/staticstudios/data/query/QueryLike.java b/core/src/main/java/net/staticstudios/data/query/QueryLike.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/QueryLike.java rename to core/src/main/java/net/staticstudios/data/query/QueryLike.java diff --git a/src/main/java/net/staticstudios/data/query/clause/AndClause.java b/core/src/main/java/net/staticstudios/data/query/clause/AndClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/AndClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/AndClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java b/core/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/BetweenClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/Clause.java b/core/src/main/java/net/staticstudios/data/query/clause/Clause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/Clause.java rename to core/src/main/java/net/staticstudios/data/query/clause/Clause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java b/core/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/CompositeClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java b/core/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/EqualsClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java b/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java b/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/InClause.java b/core/src/main/java/net/staticstudios/data/query/clause/InClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/InClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/InClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java b/core/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/LessThanClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java b/core/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/LikeClause.java b/core/src/main/java/net/staticstudios/data/query/clause/LikeClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/LikeClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/LikeClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/NotInClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotInClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/NotInClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/NotInClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/NotNullClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotNullClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/NotNullClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/NotNullClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/NullClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NullClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/NullClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/NullClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/OrClause.java b/core/src/main/java/net/staticstudios/data/query/clause/OrClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/OrClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/OrClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java b/core/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java diff --git a/src/main/java/net/staticstudios/data/query/clause/ValueClause.java b/core/src/main/java/net/staticstudios/data/query/clause/ValueClause.java similarity index 100% rename from src/main/java/net/staticstudios/data/query/clause/ValueClause.java rename to core/src/main/java/net/staticstudios/data/query/clause/ValueClause.java diff --git a/src/main/java/net/staticstudios/data/util/ColumnMetadata.java b/core/src/main/java/net/staticstudios/data/util/ColumnMetadata.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ColumnMetadata.java rename to core/src/main/java/net/staticstudios/data/util/ColumnMetadata.java diff --git a/src/main/java/net/staticstudios/data/util/ColumnValuePair.java b/core/src/main/java/net/staticstudios/data/util/ColumnValuePair.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ColumnValuePair.java rename to core/src/main/java/net/staticstudios/data/util/ColumnValuePair.java diff --git a/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java b/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ColumnValuePairs.java rename to core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java diff --git a/oldsrc/og/java/net/staticstudios/data/util/ConnectionConsumer.java b/core/src/main/java/net/staticstudios/data/util/ConnectionConsumer.java similarity index 100% rename from oldsrc/og/java/net/staticstudios/data/util/ConnectionConsumer.java rename to core/src/main/java/net/staticstudios/data/util/ConnectionConsumer.java diff --git a/oldsrc/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java b/core/src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java similarity index 100% rename from oldsrc/og/java/net/staticstudios/data/util/ConnectionJedisConsumer.java rename to core/src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java diff --git a/oldsrc/og/java/net/staticstudios/data/util/DataSourceConfig.java b/core/src/main/java/net/staticstudios/data/util/DataSourceConfig.java similarity index 100% rename from oldsrc/og/java/net/staticstudios/data/util/DataSourceConfig.java rename to core/src/main/java/net/staticstudios/data/util/DataSourceConfig.java diff --git a/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java b/core/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java rename to core/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java diff --git a/src/main/java/net/staticstudios/data/util/FieldInstancePair.java b/core/src/main/java/net/staticstudios/data/util/FieldInstancePair.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/FieldInstancePair.java rename to core/src/main/java/net/staticstudios/data/util/FieldInstancePair.java diff --git a/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java b/core/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java rename to core/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java diff --git a/src/main/java/net/staticstudios/data/util/OnDelete.java b/core/src/main/java/net/staticstudios/data/util/OnDelete.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/OnDelete.java rename to core/src/main/java/net/staticstudios/data/util/OnDelete.java diff --git a/src/main/java/net/staticstudios/data/util/OnUpdate.java b/core/src/main/java/net/staticstudios/data/util/OnUpdate.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/OnUpdate.java rename to core/src/main/java/net/staticstudios/data/util/OnUpdate.java diff --git a/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java rename to core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java diff --git a/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java rename to core/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java diff --git a/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java rename to core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java diff --git a/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java rename to core/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java diff --git a/oldsrc/og/java/net/staticstudios/data/util/PostgresUtils.java b/core/src/main/java/net/staticstudios/data/util/PostgresUtils.java similarity index 100% rename from oldsrc/og/java/net/staticstudios/data/util/PostgresUtils.java rename to core/src/main/java/net/staticstudios/data/util/PostgresUtils.java diff --git a/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java b/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ReferenceMetadata.java rename to core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java diff --git a/src/main/java/net/staticstudios/data/util/ReflectionUtils.java b/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ReflectionUtils.java rename to core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java diff --git a/src/main/java/net/staticstudios/data/util/Relation.java b/core/src/main/java/net/staticstudios/data/util/Relation.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/Relation.java rename to core/src/main/java/net/staticstudios/data/util/Relation.java diff --git a/src/main/java/net/staticstudios/data/util/SQLUtils.java b/core/src/main/java/net/staticstudios/data/util/SQLUtils.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/SQLUtils.java rename to core/src/main/java/net/staticstudios/data/util/SQLUtils.java diff --git a/src/main/java/net/staticstudios/data/util/SQlStatement.java b/core/src/main/java/net/staticstudios/data/util/SQlStatement.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/SQlStatement.java rename to core/src/main/java/net/staticstudios/data/util/SQlStatement.java diff --git a/src/main/java/net/staticstudios/data/util/SchemaTable.java b/core/src/main/java/net/staticstudios/data/util/SchemaTable.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/SchemaTable.java rename to core/src/main/java/net/staticstudios/data/util/SchemaTable.java diff --git a/src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java b/core/src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java rename to core/src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java diff --git a/src/main/java/net/staticstudios/data/util/StringUtils.java b/core/src/main/java/net/staticstudios/data/util/StringUtils.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/StringUtils.java rename to core/src/main/java/net/staticstudios/data/util/StringUtils.java diff --git a/oldsrc/og/java/net/staticstudios/data/util/TaskQueue.java b/core/src/main/java/net/staticstudios/data/util/TaskQueue.java similarity index 100% rename from oldsrc/og/java/net/staticstudios/data/util/TaskQueue.java rename to core/src/main/java/net/staticstudios/data/util/TaskQueue.java diff --git a/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java b/core/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java rename to core/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java diff --git a/src/main/java/net/staticstudios/data/util/Value.java b/core/src/main/java/net/staticstudios/data/util/Value.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/Value.java rename to core/src/main/java/net/staticstudios/data/util/Value.java diff --git a/oldsrc/og/java/net/staticstudios/data/util/ValueUpdate.java b/core/src/main/java/net/staticstudios/data/util/ValueUpdate.java similarity index 100% rename from oldsrc/og/java/net/staticstudios/data/util/ValueUpdate.java rename to core/src/main/java/net/staticstudios/data/util/ValueUpdate.java diff --git a/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java rename to core/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java diff --git a/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java rename to core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java diff --git a/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java rename to core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java diff --git a/src/main/java/net/staticstudios/data/util/ValueUtils.java b/core/src/main/java/net/staticstudios/data/util/ValueUtils.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ValueUtils.java rename to core/src/main/java/net/staticstudios/data/util/ValueUtils.java diff --git a/src/test/java/net/staticstudios/data/CustomTypeTest.java b/core/src/test/java/net/staticstudios/data/CustomTypeTest.java similarity index 100% rename from src/test/java/net/staticstudios/data/CustomTypeTest.java rename to core/src/test/java/net/staticstudios/data/CustomTypeTest.java diff --git a/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java similarity index 100% rename from src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java rename to core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java diff --git a/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java similarity index 100% rename from src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java rename to core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java diff --git a/src/test/java/net/staticstudios/data/PersistentValueTest.java b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java similarity index 100% rename from src/test/java/net/staticstudios/data/PersistentValueTest.java rename to core/src/test/java/net/staticstudios/data/PersistentValueTest.java diff --git a/src/test/java/net/staticstudios/data/PrimitivesTest.java b/core/src/test/java/net/staticstudios/data/PrimitivesTest.java similarity index 100% rename from src/test/java/net/staticstudios/data/PrimitivesTest.java rename to core/src/test/java/net/staticstudios/data/PrimitivesTest.java diff --git a/src/test/java/net/staticstudios/data/QueryTest.java b/core/src/test/java/net/staticstudios/data/QueryTest.java similarity index 99% rename from src/test/java/net/staticstudios/data/QueryTest.java rename to core/src/test/java/net/staticstudios/data/QueryTest.java index 7a0df304..aa4248f1 100644 --- a/src/test/java/net/staticstudios/data/QueryTest.java +++ b/core/src/test/java/net/staticstudios/data/QueryTest.java @@ -4,7 +4,6 @@ import net.staticstudios.data.mock.user.MockUser; import net.staticstudios.data.mock.user.MockUserFactory; import net.staticstudios.data.mock.user.MockUserQuery; -import net.staticstudios.data.query.Order; import org.junit.jupiter.api.Test; import java.util.List; diff --git a/src/test/java/net/staticstudios/data/ReferenceTest.java b/core/src/test/java/net/staticstudios/data/ReferenceTest.java similarity index 100% rename from src/test/java/net/staticstudios/data/ReferenceTest.java rename to core/src/test/java/net/staticstudios/data/ReferenceTest.java diff --git a/src/test/java/net/staticstudios/data/SQLParseTest.java b/core/src/test/java/net/staticstudios/data/SQLParseTest.java similarity index 67% rename from src/test/java/net/staticstudios/data/SQLParseTest.java rename to core/src/test/java/net/staticstudios/data/SQLParseTest.java index 1695bb91..f4bf3ca4 100644 --- a/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/core/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -12,9 +12,13 @@ import java.sql.Connection; import java.sql.Statement; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; public class SQLParseTest extends DataTest { @@ -37,6 +41,56 @@ private static String normalize(String str) { return str.replace("\r\n", "\n").trim(); } + private static void assertSqlLinesEqualOrderIndependent(List expectedLines, List actualLines) { + Set expectedSet = new LinkedHashSet<>(expectedLines.stream().map(l -> { + if (l.endsWith(",")) { + l = l.substring(0, l.length() - 1); + } + return l.trim(); + }).toList()); + Set actualSet = new LinkedHashSet<>(actualLines.stream().map(l -> { + if (l.endsWith(",")) { + l = l.substring(0, l.length() - 1); + } + return l.trim(); + }).toList()); + + if (!expectedSet.equals(actualSet)) { + Set missing = new LinkedHashSet<>(expectedSet); + missing.removeAll(actualSet); + Set unexpected = new LinkedHashSet<>(actualSet); + unexpected.removeAll(expectedSet); + + StringBuilder msg = new StringBuilder(); + msg.append(String.format("Schema mismatch: expected %d distinct lines, actual %d distinct lines.%n", expectedSet.size(), actualSet.size())); + if (!missing.isEmpty()) { + msg.append(String.format("Missing (%d):%n", missing.size())); + for (String s : missing) { + msg.append(String.format(" %s%n", s)); + } + } + if (!unexpected.isEmpty()) { + msg.append(String.format("Unexpected (%d):%n", unexpected.size())); + for (String s : unexpected) { + msg.append(String.format(" %s%n", s)); + } + } + + msg.append("Full expected:\n"); + for (String s : expectedLines) { + msg.append(String.format(" %s%n", s)); + } + msg.append("Full actual:\n"); + for (String s : actualLines) { + msg.append(String.format(" %s%n", s)); + } + + fail(msg.toString()); + } + + assertFalse(actualLines.isEmpty(), String.format("No SQL lines were produced by pg_dump after cleaning. Expected %d distinct lines but got %d distinct lines.", expectedSet.size(), actualSet.size())); + } + @Test public void testParse() throws Exception { DataManager dm = getMockEnvironments().getFirst().dataManager(); @@ -113,7 +167,10 @@ public void testParse() throws Exception { ADD CONSTRAINT fk_posts_ref_post_id_to_post_id FOREIGN KEY (posts_ref_post_id) REFERENCES social_media.posts(post_id) ON UPDATE CASCADE ON DELETE CASCADE; """; - assertEquals(normalize(expected), normalize(cleanedDump.toString())); + List expectedLines = Arrays.asList(normalize(expected).split("\n")); + List actualLines = Arrays.asList(normalize(cleanedDump.toString()).split("\n")); + + assertSqlLinesEqualOrderIndependent(expectedLines, actualLines); } //todo: when a delete strategy is set to no action where it was previously set to cascade, the old trigger should be dropped. Add a test for this. moreover, what happens when we change the name of something? will the old trigger stay or what? handle this diff --git a/src/test/java/net/staticstudios/data/ValueParseTest.java b/core/src/test/java/net/staticstudios/data/ValueParseTest.java similarity index 100% rename from src/test/java/net/staticstudios/data/ValueParseTest.java rename to core/src/test/java/net/staticstudios/data/ValueParseTest.java diff --git a/src/test/java/net/staticstudios/data/misc/DataTest.java b/core/src/test/java/net/staticstudios/data/misc/DataTest.java similarity index 100% rename from src/test/java/net/staticstudios/data/misc/DataTest.java rename to core/src/test/java/net/staticstudios/data/misc/DataTest.java diff --git a/oldsrc/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java b/core/src/test/java/net/staticstudios/data/misc/MockEnvironment.java similarity index 100% rename from oldsrc/ogtest/java/net/staticstudios/data/misc/MockEnvironment.java rename to core/src/test/java/net/staticstudios/data/misc/MockEnvironment.java diff --git a/src/test/java/net/staticstudios/data/misc/MockThreadProvider.java b/core/src/test/java/net/staticstudios/data/misc/MockThreadProvider.java similarity index 100% rename from src/test/java/net/staticstudios/data/misc/MockThreadProvider.java rename to core/src/test/java/net/staticstudios/data/misc/MockThreadProvider.java diff --git a/src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java b/core/src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java similarity index 100% rename from src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java rename to core/src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java diff --git a/oldsrc/ogtest/java/net/staticstudios/data/misc/TestUtils.java b/core/src/test/java/net/staticstudios/data/misc/TestUtils.java similarity index 100% rename from oldsrc/ogtest/java/net/staticstudios/data/misc/TestUtils.java rename to core/src/test/java/net/staticstudios/data/misc/TestUtils.java diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountDetails.java b/core/src/test/java/net/staticstudios/data/mock/account/AccountDetails.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/account/AccountDetails.java rename to core/src/test/java/net/staticstudios/data/mock/account/AccountDetails.java diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountSettings.java b/core/src/test/java/net/staticstudios/data/mock/account/AccountSettings.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/account/AccountSettings.java rename to core/src/test/java/net/staticstudios/data/mock/account/AccountSettings.java diff --git a/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/account/MockAccount.java b/core/src/test/java/net/staticstudios/data/mock/account/MockAccount.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/account/MockAccount.java rename to core/src/test/java/net/staticstudios/data/mock/account/MockAccount.java diff --git a/src/test/java/net/staticstudios/data/mock/post/MockPost.java b/core/src/test/java/net/staticstudios/data/mock/post/MockPost.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/post/MockPost.java rename to core/src/test/java/net/staticstudios/data/mock/post/MockPost.java diff --git a/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java b/core/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java rename to core/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/user/MockUser.java rename to core/src/test/java/net/staticstudios/data/mock/user/MockUser.java diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUserSession.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUserSession.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/user/MockUserSession.java rename to core/src/test/java/net/staticstudios/data/mock/user/MockUserSession.java diff --git a/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java rename to core/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java diff --git a/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java similarity index 100% rename from src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java rename to core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java diff --git a/oldsrc/ogtest/resources/log4j.properties b/core/src/test/resources/log4j.properties similarity index 100% rename from oldsrc/ogtest/resources/log4j.properties rename to core/src/test/resources/log4j.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch literal 43453 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vSTxF-Vi3+ZOI=Thq2} zyQgjYY1_7^ZQHh{?P))4+qUiQJLi1&{yE>h?~jU%tjdV0h|FENbM3X(KnJdPKc?~k zh=^Ixv*+smUll!DTWH!jrV*wSh*(mx0o6}1@JExzF(#9FXgmTXVoU+>kDe68N)dkQ zH#_98Zv$}lQwjKL@yBd;U(UD0UCl322=pav<=6g>03{O_3oKTq;9bLFX1ia*lw;#K zOiYDcBJf)82->83N_Y(J7Kr_3lE)hAu;)Q(nUVydv+l+nQ$?|%MWTy`t>{havFSQloHwiIkGK9YZ79^9?AZo0ZyQlVR#}lF%dn5n%xYksXf8gnBm=wO7g_^! zauQ-bH1Dc@3ItZ-9D_*pH}p!IG7j8A_o94#~>$LR|TFq zZ-b00*nuw|-5C2lJDCw&8p5N~Z1J&TrcyErds&!l3$eSz%`(*izc;-?HAFD9AHb-| z>)id`QCrzRws^9(#&=pIx9OEf2rmlob8sK&xPCWS+nD~qzU|qG6KwA{zbikcfQrdH z+ zQg>O<`K4L8rN7`GJB0*3<3`z({lWe#K!4AZLsI{%z#ja^OpfjU{!{)x0ZH~RB0W5X zTwN^w=|nA!4PEU2=LR05x~}|B&ZP?#pNgDMwD*ajI6oJqv!L81gu=KpqH22avXf0w zX3HjbCI!n9>l046)5rr5&v5ja!xkKK42zmqHzPx$9Nn_MZk`gLeSLgC=LFf;H1O#B zn=8|^1iRrujHfbgA+8i<9jaXc;CQBAmQvMGQPhFec2H1knCK2x!T`e6soyrqCamX% zTQ4dX_E*8so)E*TB$*io{$c6X)~{aWfaqdTh=xEeGvOAN9H&-t5tEE-qso<+C!2>+ zskX51H-H}#X{A75wqFe-J{?o8Bx|>fTBtl&tcbdR|132Ztqu5X0i-pisB-z8n71%q%>EF}yy5?z=Ve`}hVh{Drv1YWL zW=%ug_&chF11gDv3D6B)Tz5g54H0mDHNjuKZ+)CKFk4Z|$RD zfRuKLW`1B>B?*RUfVd0+u8h3r-{@fZ{k)c!93t1b0+Q9vOaRnEn1*IL>5Z4E4dZ!7 ztp4GP-^1d>8~LMeb}bW!(aAnB1tM_*la=Xx)q(I0Y@__Zd$!KYb8T2VBRw%e$iSdZ zkwdMwd}eV9q*;YvrBFTv1>1+}{H!JK2M*C|TNe$ZSA>UHKk);wz$(F$rXVc|sI^lD zV^?_J!3cLM;GJuBMbftbaRUs$;F}HDEDtIeHQ)^EJJ1F9FKJTGH<(Jj`phE6OuvE) zqK^K`;3S{Y#1M@8yRQwH`?kHMq4tHX#rJ>5lY3DM#o@or4&^_xtBC(|JpGTfrbGkA z2Tu+AyT^pHannww!4^!$5?@5v`LYy~T`qs7SYt$JgrY(w%C+IWA;ZkwEF)u5sDvOK zGk;G>Mh&elvXDcV69J_h02l&O;!{$({fng9Rlc3ID#tmB^FIG^w{HLUpF+iB`|
NnX)EH+Nua)3Y(c z&{(nX_ht=QbJ%DzAya}!&uNu!4V0xI)QE$SY__m)SAKcN0P(&JcoK*Lxr@P zY&P=}&B3*UWNlc|&$Oh{BEqwK2+N2U$4WB7Fd|aIal`FGANUa9E-O)!gV`((ZGCc$ zBJA|FFrlg~9OBp#f7aHodCe{6= zay$6vN~zj1ddMZ9gQ4p32(7wD?(dE>KA2;SOzXRmPBiBc6g`eOsy+pVcHu=;Yd8@{ zSGgXf@%sKKQz~;!J;|2fC@emm#^_rnO0esEn^QxXgJYd`#FPWOUU5b;9eMAF zZhfiZb|gk8aJIw*YLp4!*(=3l8Cp{(%p?ho22*vN9+5NLV0TTazNY$B5L6UKUrd$n zjbX%#m7&F#U?QNOBXkiiWB*_tk+H?N3`vg;1F-I+83{M2!8<^nydGr5XX}tC!10&e z7D36bLaB56WrjL&HiiMVtpff|K%|*{t*ltt^5ood{FOG0<>k&1h95qPio)2`eL${YAGIx(b4VN*~nKn6E~SIQUuRH zQ+5zP6jfnP$S0iJ@~t!Ai3o`X7biohli;E zT#yXyl{bojG@-TGZzpdVDXhbmF%F9+-^YSIv|MT1l3j zrxOFq>gd2%U}?6}8mIj?M zc077Zc9fq(-)4+gXv?Az26IO6eV`RAJz8e3)SC7~>%rlzDwySVx*q$ygTR5kW2ds- z!HBgcq0KON9*8Ff$X0wOq$`T7ml(@TF)VeoF}x1OttjuVHn3~sHrMB++}f7f9H%@f z=|kP_?#+fve@{0MlbkC9tyvQ_R?lRdRJ@$qcB(8*jyMyeME5ns6ypVI1Xm*Zr{DuS zZ!1)rQfa89c~;l~VkCiHI|PCBd`S*2RLNQM8!g9L6?n`^evQNEwfO@&JJRme+uopQX0%Jo zgd5G&#&{nX{o?TQwQvF1<^Cg3?2co;_06=~Hcb6~4XWpNFL!WU{+CK;>gH%|BLOh7@!hsa(>pNDAmpcuVO-?;Bic17R}^|6@8DahH)G z!EmhsfunLL|3b=M0MeK2vqZ|OqUqS8npxwge$w-4pFVXFq$_EKrZY?BuP@Az@(k`L z`ViQBSk`y+YwRT;&W| z2e3UfkCo^uTA4}Qmmtqs+nk#gNr2W4 zTH%hhErhB)pkXR{B!q5P3-OM+M;qu~f>}IjtF%>w{~K-0*jPVLl?Chz&zIdxp}bjx zStp&Iufr58FTQ36AHU)0+CmvaOpKF;W@sMTFpJ`j;3d)J_$tNQI^c<^1o<49Z(~K> z;EZTBaVT%14(bFw2ob@?JLQ2@(1pCdg3S%E4*dJ}dA*v}_a4_P(a`cHnBFJxNobAv zf&Zl-Yt*lhn-wjZsq<9v-IsXxAxMZ58C@e0!rzhJ+D@9^3~?~yllY^s$?&oNwyH!#~6x4gUrfxplCvK#!f z$viuszW>MFEcFL?>ux*((!L$;R?xc*myjRIjgnQX79@UPD$6Dz0jutM@7h_pq z0Zr)#O<^y_K6jfY^X%A-ip>P%3saX{!v;fxT-*0C_j4=UMH+Xth(XVkVGiiKE#f)q z%Jp=JT)uy{&}Iq2E*xr4YsJ5>w^=#-mRZ4vPXpI6q~1aFwi+lQcimO45V-JXP;>(Q zo={U`{=_JF`EQj87Wf}{Qy35s8r1*9Mxg({CvOt}?Vh9d&(}iI-quvs-rm~P;eRA@ zG5?1HO}puruc@S{YNAF3vmUc2B4!k*yi))<5BQmvd3tr}cIs#9)*AX>t`=~{f#Uz0 z0&Nk!7sSZwJe}=)-R^$0{yeS!V`Dh7w{w5rZ9ir!Z7Cd7dwZcK;BT#V0bzTt>;@Cl z#|#A!-IL6CZ@eHH!CG>OO8!%G8&8t4)Ro@}USB*k>oEUo0LsljsJ-%5Mo^MJF2I8- z#v7a5VdJ-Cd%(a+y6QwTmi+?f8Nxtm{g-+WGL>t;s#epv7ug>inqimZCVm!uT5Pf6 ziEgQt7^%xJf#!aPWbuC_3Nxfb&CFbQy!(8ANpkWLI4oSnH?Q3f?0k1t$3d+lkQs{~(>06l&v|MpcFsyAv zin6N!-;pggosR*vV=DO(#+}4ps|5$`udE%Kdmp?G7B#y%H`R|i8skKOd9Xzx8xgR$>Zo2R2Ytktq^w#ul4uicxW#{ zFjG_RNlBroV_n;a7U(KIpcp*{M~e~@>Q#Av90Jc5v%0c>egEdY4v3%|K1XvB{O_8G zkTWLC>OZKf;XguMH2-Pw{BKbFzaY;4v2seZV0>^7Q~d4O=AwaPhP3h|!hw5aqOtT@ z!SNz}$of**Bl3TK209@F=Tn1+mgZa8yh(Png%Zd6Mt}^NSjy)etQrF zme*llAW=N_8R*O~d2!apJnF%(JcN??=`$qs3Y+~xs>L9x`0^NIn!8mMRFA_tg`etw z3k{9JAjnl@ygIiJcNHTy02GMAvBVqEss&t2<2mnw!; zU`J)0>lWiqVqo|ex7!+@0i>B~BSU1A_0w#Ee+2pJx0BFiZ7RDHEvE*ptc9md(B{&+ zKE>TM)+Pd>HEmdJao7U@S>nL(qq*A)#eLOuIfAS@j`_sK0UEY6OAJJ-kOrHG zjHx`g!9j*_jRcJ%>CE9K2MVf?BUZKFHY?EpV6ai7sET-tqk=nDFh-(65rhjtlKEY% z@G&cQ<5BKatfdA1FKuB=i>CCC5(|9TMW%K~GbA4}80I5%B}(gck#Wlq@$nO3%@QP_ z8nvPkJFa|znk>V92cA!K1rKtr)skHEJD;k8P|R8RkCq1Rh^&}Evwa4BUJz2f!2=MH zo4j8Y$YL2313}H~F7@J7mh>u%556Hw0VUOz-Un@ZASCL)y8}4XXS`t1AC*^>PLwIc zUQok5PFS=*#)Z!3JZN&eZ6ZDP^-c@StY*t20JhCnbMxXf=LK#;`4KHEqMZ-Ly9KsS zI2VUJGY&PmdbM+iT)zek)#Qc#_i4uH43 z@T5SZBrhNCiK~~esjsO9!qBpaWK<`>!-`b71Y5ReXQ4AJU~T2Njri1CEp5oKw;Lnm)-Y@Z3sEY}XIgSy%xo=uek(kAAH5MsV$V3uTUsoTzxp_rF=tx zV07vlJNKtJhCu`b}*#m&5LV4TAE&%KtHViDAdv#c^x`J7bg z&N;#I2GkF@SIGht6p-V}`!F_~lCXjl1BdTLIjD2hH$J^YFN`7f{Q?OHPFEM$65^!u zNwkelo*5+$ZT|oQ%o%;rBX$+?xhvjb)SHgNHE_yP%wYkkvXHS{Bf$OiKJ5d1gI0j< zF6N}Aq=(WDo(J{e-uOecxPD>XZ@|u-tgTR<972`q8;&ZD!cep^@B5CaqFz|oU!iFj zU0;6fQX&~15E53EW&w1s9gQQ~Zk16X%6 zjG`j0yq}4deX2?Tr(03kg>C(!7a|b9qFI?jcE^Y>-VhudI@&LI6Qa}WQ>4H_!UVyF z((cm&!3gmq@;BD#5P~0;_2qgZhtJS|>WdtjY=q zLnHH~Fm!cxw|Z?Vw8*~?I$g#9j&uvgm7vPr#&iZgPP~v~BI4jOv;*OQ?jYJtzO<^y z7-#C={r7CO810!^s(MT!@@Vz_SVU)7VBi(e1%1rvS!?PTa}Uv`J!EP3s6Y!xUgM^8 z4f!fq<3Wer_#;u!5ECZ|^c1{|q_lh3m^9|nsMR1#Qm|?4Yp5~|er2?W^7~cl;_r4WSme_o68J9p03~Hc%X#VcX!xAu%1`R!dfGJCp zV*&m47>s^%Ib0~-2f$6oSgn3jg8m%UA;ArcdcRyM5;}|r;)?a^D*lel5C`V5G=c~k zy*w_&BfySOxE!(~PI$*dwG><+-%KT5p?whOUMA*k<9*gi#T{h3DAxzAPxN&Xws8o9Cp*`PA5>d9*Z-ynV# z9yY*1WR^D8|C%I@vo+d8r^pjJ$>eo|j>XiLWvTWLl(^;JHCsoPgem6PvegHb-OTf| zvTgsHSa;BkbG=(NgPO|CZu9gUCGr$8*EoH2_Z#^BnxF0yM~t`|9ws_xZ8X8iZYqh! zAh;HXJ)3P&)Q0(&F>!LN0g#bdbis-cQxyGn9Qgh`q+~49Fqd2epikEUw9caM%V6WgP)532RMRW}8gNS%V%Hx7apSz}tn@bQy!<=lbhmAH=FsMD?leawbnP5BWM0 z5{)@EEIYMu5;u)!+HQWhQ;D3_Cm_NADNeb-f56}<{41aYq8p4=93d=-=q0Yx#knGYfXVt z+kMxlus}t2T5FEyCN~!}90O_X@@PQpuy;kuGz@bWft%diBTx?d)_xWd_-(!LmVrh**oKg!1CNF&LX4{*j|) zIvjCR0I2UUuuEXh<9}oT_zT#jOrJAHNLFT~Ilh9hGJPI1<5`C-WA{tUYlyMeoy!+U zhA#=p!u1R7DNg9u4|QfED-2TuKI}>p#2P9--z;Bbf4Op*;Q9LCbO&aL2i<0O$ByoI z!9;Ght733FC>Pz>$_mw(F`zU?`m@>gE`9_p*=7o=7av`-&ifU(^)UU`Kg3Kw`h9-1 z6`e6+im=|m2v`pN(2dE%%n8YyQz;#3Q-|x`91z?gj68cMrHl}C25|6(_dIGk*8cA3 zRHB|Nwv{@sP4W+YZM)VKI>RlB`n=Oj~Rzx~M+Khz$N$45rLn6k1nvvD^&HtsMA4`s=MmuOJID@$s8Ph4E zAmSV^+s-z8cfv~Yd(40Sh4JG#F~aB>WFoX7ykaOr3JaJ&Lb49=B8Vk-SQT9%7TYhv z?-Pprt{|=Y5ZQ1?od|A<_IJU93|l4oAfBm?3-wk{O<8ea+`}u%(kub(LFo2zFtd?4 zwpN|2mBNywv+d^y_8#<$r>*5+$wRTCygFLcrwT(qc^n&@9r+}Kd_u@Ithz(6Qb4}A zWo_HdBj#V$VE#l6pD0a=NfB0l^6W^g`vm^sta>Tly?$E&{F?TTX~DsKF~poFfmN%2 z4x`Dc{u{Lkqz&y!33;X}weD}&;7p>xiI&ZUb1H9iD25a(gI|`|;G^NwJPv=1S5e)j z;U;`?n}jnY6rA{V^ zxTd{bK)Gi^odL3l989DQlN+Zs39Xe&otGeY(b5>rlIqfc7Ap4}EC?j<{M=hlH{1+d zw|c}}yx88_xQr`{98Z!d^FNH77=u(p-L{W6RvIn40f-BldeF-YD>p6#)(Qzf)lfZj z?3wAMtPPp>vMehkT`3gToPd%|D8~4`5WK{`#+}{L{jRUMt zrFz+O$C7y8$M&E4@+p+oV5c%uYzbqd2Y%SSgYy#xh4G3hQv>V*BnuKQhBa#=oZB~w{azUB+q%bRe_R^ z>fHBilnRTUfaJ201czL8^~Ix#+qOHSO)A|xWLqOxB$dT2W~)e-r9;bm=;p;RjYahB z*1hegN(VKK+ztr~h1}YP@6cfj{e#|sS`;3tJhIJK=tVJ-*h-5y9n*&cYCSdg#EHE# zSIx=r#qOaLJoVVf6v;(okg6?*L_55atl^W(gm^yjR?$GplNP>BZsBYEf_>wM0Lc;T zhf&gpzOWNxS>m+mN92N0{;4uw`P+9^*|-1~$uXpggj4- z^SFc4`uzj2OwdEVT@}Q`(^EcQ_5(ZtXTql*yGzdS&vrS_w>~~ra|Nb5abwf}Y!uq6R5f&6g2ge~2p(%c< z@O)cz%%rr4*cRJ5f`n@lvHNk@lE1a*96Kw6lJ~B-XfJW%?&-y?;E&?1AacU@`N`!O z6}V>8^%RZ7SQnZ-z$(jsX`amu*5Fj8g!3RTRwK^`2_QHe;_2y_n|6gSaGyPmI#kA0sYV<_qOZc#-2BO%hX)f$s-Z3xlI!ub z^;3ru11DA`4heAu%}HIXo&ctujzE2!6DIGE{?Zs>2}J+p&C$rc7gJC35gxhflorvsb%sGOxpuWhF)dL_&7&Z99=5M0b~Qa;Mo!j&Ti_kXW!86N%n= zSC@6Lw>UQ__F&+&Rzv?gscwAz8IP!n63>SP)^62(HK98nGjLY2*e^OwOq`3O|C92? z;TVhZ2SK%9AGW4ZavTB9?)mUbOoF`V7S=XM;#3EUpR+^oHtdV!GK^nXzCu>tpR|89 zdD{fnvCaN^^LL%amZ^}-E+214g&^56rpdc@yv0b<3}Ys?)f|fXN4oHf$six)-@<;W&&_kj z-B}M5U*1sb4)77aR=@%I?|Wkn-QJVuA96an25;~!gq(g1@O-5VGo7y&E_srxL6ZfS z*R%$gR}dyONgju*D&?geiSj7SZ@ftyA|}(*Y4KbvU!YLsi1EDQQCnb+-cM=K1io78o!v*);o<XwjaQH%)uIP&Zm?)Nfbfn;jIr z)d#!$gOe3QHp}2NBak@yYv3m(CPKkwI|{;d=gi552u?xj9ObCU^DJFQp4t4e1tPzM zvsRIGZ6VF+{6PvqsplMZWhz10YwS={?`~O0Ec$`-!klNUYtzWA^f9m7tkEzCy<_nS z=&<(awFeZvt51>@o_~>PLs05CY)$;}Oo$VDO)?l-{CS1Co=nxjqben*O1BR>#9`0^ zkwk^k-wcLCLGh|XLjdWv0_Hg54B&OzCE^3NCP}~OajK-LuRW53CkV~Su0U>zN%yQP zH8UH#W5P3-!ToO-2k&)}nFe`t+mdqCxxAHgcifup^gKpMObbox9LFK;LP3}0dP-UW z?Zo*^nrQ6*$FtZ(>kLCc2LY*|{!dUn$^RW~m9leoF|@Jy|M5p-G~j%+P0_#orRKf8 zvuu5<*XO!B?1E}-*SY~MOa$6c%2cM+xa8}_8x*aVn~57v&W(0mqN1W`5a7*VN{SUH zXz98DDyCnX2EPl-`Lesf`=AQT%YSDb`$%;(jUTrNen$NPJrlpPDP}prI>Ml!r6bCT;mjsg@X^#&<}CGf0JtR{Ecwd&)2zuhr#nqdgHj+g2n}GK9CHuwO zk>oZxy{vcOL)$8-}L^iVfJHAGfwN$prHjYV0ju}8%jWquw>}_W6j~m<}Jf!G?~r5&Rx)!9JNX!ts#SGe2HzobV5); zpj@&`cNcO&q+%*<%D7za|?m5qlmFK$=MJ_iv{aRs+BGVrs)98BlN^nMr{V_fcl_;jkzRju+c-y?gqBC_@J0dFLq-D9@VN&-`R9U;nv$Hg?>$oe4N&Ht$V_(JR3TG^! zzJsbQbi zFE6-{#9{G{+Z}ww!ycl*7rRdmU#_&|DqPfX3CR1I{Kk;bHwF6jh0opI`UV2W{*|nn zf_Y@%wW6APb&9RrbEN=PQRBEpM(N1w`81s=(xQj6 z-eO0k9=Al|>Ej|Mw&G`%q8e$2xVz1v4DXAi8G};R$y)ww638Y=9y$ZYFDM$}vzusg zUf+~BPX>(SjA|tgaFZr_e0{)+z9i6G#lgt=F_n$d=beAt0Sa0a7>z-?vcjl3e+W}+ z1&9=|vC=$co}-Zh*%3588G?v&U7%N1Qf-wNWJ)(v`iO5KHSkC5&g7CrKu8V}uQGcfcz zmBz#Lbqwqy#Z~UzHgOQ;Q-rPxrRNvl(&u6ts4~0=KkeS;zqURz%!-ERppmd%0v>iRlEf+H$yl{_8TMJzo0 z>n)`On|7=WQdsqhXI?#V{>+~}qt-cQbokEbgwV3QvSP7&hK4R{Z{aGHVS3;+h{|Hz z6$Js}_AJr383c_+6sNR|$qu6dqHXQTc6?(XWPCVZv=)D#6_;D_8P-=zOGEN5&?~8S zl5jQ?NL$c%O)*bOohdNwGIKM#jSAC?BVY={@A#c9GmX0=T(0G}xs`-%f3r=m6-cpK z!%waekyAvm9C3%>sixdZj+I(wQlbB4wv9xKI*T13DYG^T%}zZYJ|0$Oj^YtY+d$V$ zAVudSc-)FMl|54n=N{BnZTM|!>=bhaja?o7s+v1*U$!v!qQ%`T-6fBvmdPbVmro&d zk07TOp*KuxRUSTLRrBj{mjsnF8`d}rMViY8j`jo~Hp$fkv9F_g(jUo#Arp;Xw0M$~ zRIN!B22~$kx;QYmOkos@%|5k)!QypDMVe}1M9tZfkpXKGOxvKXB!=lo`p?|R1l=tA zp(1}c6T3Fwj_CPJwVsYtgeRKg?9?}%oRq0F+r+kdB=bFUdVDRPa;E~~>2$w}>O>v=?|e>#(-Lyx?nbg=ckJ#5U6;RT zNvHhXk$P}m9wSvFyU3}=7!y?Y z=fg$PbV8d7g25&-jOcs{%}wTDKm>!Vk);&rr;O1nvO0VrU&Q?TtYVU=ir`te8SLlS zKSNmV=+vF|ATGg`4$N1uS|n??f}C_4Sz!f|4Ly8#yTW-FBfvS48Tef|-46C(wEO_%pPhUC5$-~Y?!0vFZ^Gu`x=m7X99_?C-`|h zfmMM&Y@zdfitA@KPw4Mc(YHcY1)3*1xvW9V-r4n-9ZuBpFcf{yz+SR{ zo$ZSU_|fgwF~aakGr(9Be`~A|3)B=9`$M-TWKipq-NqRDRQc}ABo*s_5kV%doIX7LRLRau_gd@Rd_aLFXGSU+U?uAqh z8qusWWcvgQ&wu{|sRXmv?sl=xc<$6AR$+cl& zFNh5q1~kffG{3lDUdvEZu5c(aAG~+64FxdlfwY^*;JSS|m~CJusvi-!$XR`6@XtY2 znDHSz7}_Bx7zGq-^5{stTRy|I@N=>*y$zz>m^}^{d&~h;0kYiq8<^Wq7Dz0w31ShO^~LUfW6rfitR0(=3;Uue`Y%y@ex#eKPOW zO~V?)M#AeHB2kovn1v=n^D?2{2jhIQd9t|_Q+c|ZFaWt+r&#yrOu-!4pXAJuxM+Cx z*H&>eZ0v8Y`t}8{TV6smOj=__gFC=eah)mZt9gwz>>W$!>b3O;Rm^Ig*POZP8Rl0f zT~o=Nu1J|lO>}xX&#P58%Yl z83`HRs5#32Qm9mdCrMlV|NKNC+Z~ z9OB8xk5HJ>gBLi+m@(pvpw)1(OaVJKs*$Ou#@Knd#bk+V@y;YXT?)4eP9E5{J%KGtYinNYJUH9PU3A}66c>Xn zZ{Bn0<;8$WCOAL$^NqTjwM?5d=RHgw3!72WRo0c;+houoUA@HWLZM;^U$&sycWrFd zE7ekt9;kb0`lps{>R(}YnXlyGY}5pPd9zBpgXeJTY_jwaJGSJQC#-KJqmh-;ad&F- z-Y)E>!&`Rz!HtCz>%yOJ|v(u7P*I$jqEY3}(Z-orn4 zlI?CYKNl`6I){#2P1h)y(6?i;^z`N3bxTV%wNvQW+eu|x=kbj~s8rhCR*0H=iGkSj zk23lr9kr|p7#qKL=UjgO`@UnvzU)`&fI>1Qs7ubq{@+lK{hH* zvl6eSb9%yngRn^T<;jG1SVa)eA>T^XX=yUS@NCKpk?ovCW1D@!=@kn;l_BrG;hOTC z6K&H{<8K#dI(A+zw-MWxS+~{g$tI7|SfP$EYKxA}LlVO^sT#Oby^grkdZ^^lA}uEF zBSj$weBJG{+Bh@Yffzsw=HyChS(dtLE3i*}Zj@~!_T-Ay7z=B)+*~3|?w`Zd)Co2t zC&4DyB!o&YgSw+fJn6`sn$e)29`kUwAc+1MND7YjV%lO;H2}fNy>hD#=gT ze+-aFNpyKIoXY~Vq-}OWPBe?Rfu^{ps8>Xy%42r@RV#*QV~P83jdlFNgkPN=T|Kt7 zV*M`Rh*30&AWlb$;ae130e@}Tqi3zx2^JQHpM>j$6x`#{mu%tZlwx9Gj@Hc92IuY* zarmT|*d0E~vt6<+r?W^UW0&#U&)8B6+1+;k^2|FWBRP9?C4Rk)HAh&=AS8FS|NQaZ z2j!iZ)nbEyg4ZTp-zHwVlfLC~tXIrv(xrP8PAtR{*c;T24ycA-;auWsya-!kF~CWZ zw_uZ|%urXgUbc@x=L=_g@QJ@m#5beS@6W195Hn7>_}z@Xt{DIEA`A&V82bc^#!q8$ zFh?z_Vn|ozJ;NPd^5uu(9tspo8t%&-U9Ckay-s@DnM*R5rtu|4)~e)`z0P-sy?)kc zs_k&J@0&0!q4~%cKL)2l;N*T&0;mqX5T{Qy60%JtKTQZ-xb%KOcgqwJmb%MOOKk7N zgq})R_6**{8A|6H?fO+2`#QU)p$Ei2&nbj6TpLSIT^D$|`TcSeh+)}VMb}LmvZ{O| ze*1IdCt3+yhdYVxcM)Q_V0bIXLgr6~%JS<<&dxIgfL=Vnx4YHuU@I34JXA|+$_S3~ zy~X#gO_X!cSs^XM{yzDGNM>?v(+sF#<0;AH^YrE8smx<36bUsHbN#y57K8WEu(`qHvQ6cAZPo=J5C(lSmUCZ57Rj6cx!e^rfaI5%w}unz}4 zoX=nt)FVNV%QDJH`o!u9olLD4O5fl)xp+#RloZlaA92o3x4->?rB4`gS$;WO{R;Z3>cG3IgFX2EA?PK^M}@%1%A;?f6}s&CV$cIyEr#q5;yHdNZ9h{| z-=dX+a5elJoDo?Eq&Og!nN6A)5yYpnGEp}?=!C-V)(*~z-+?kY1Q7qs#Rsy%hu_60rdbB+QQNr?S1 z?;xtjUv|*E3}HmuNyB9aFL5H~3Ho0UsmuMZELp1a#CA1g`P{-mT?BchuLEtK}!QZ=3AWakRu~?f9V~3F;TV`5%9Pcs_$gq&CcU}r8gOO zC2&SWPsSG{&o-LIGTBqp6SLQZPvYKp$$7L4WRRZ0BR$Kf0I0SCFkqveCp@f)o8W)! z$%7D1R`&j7W9Q9CGus_)b%+B#J2G;l*FLz#s$hw{BHS~WNLODV#(!u_2Pe&tMsq={ zdm7>_WecWF#D=?eMjLj=-_z`aHMZ=3_-&E8;ibPmM}61i6J3is*=dKf%HC>=xbj4$ zS|Q-hWQ8T5mWde6h@;mS+?k=89?1FU<%qH9B(l&O>k|u_aD|DY*@~(`_pb|B#rJ&g zR0(~(68fpUPz6TdS@4JT5MOPrqDh5_H(eX1$P2SQrkvN8sTxwV>l0)Qq z0pzTuvtEAKRDkKGhhv^jk%|HQ1DdF%5oKq5BS>szk-CIke{%js?~%@$uaN3^Uz6Wf z_iyx{bZ(;9y4X&>LPV=L=d+A}7I4GkK0c1Xts{rrW1Q7apHf-))`BgC^0^F(>At1* za@e7{lq%yAkn*NH8Q1{@{lKhRg*^TfGvv!Sn*ed*x@6>M%aaqySxR|oNadYt1mpUZ z6H(rupHYf&Z z29$5g#|0MX#aR6TZ$@eGxxABRKakDYtD%5BmKp;HbG_ZbT+=81E&=XRk6m_3t9PvD zr5Cqy(v?gHcYvYvXkNH@S#Po~q(_7MOuCAB8G$a9BC##gw^5mW16cML=T=ERL7wsk zzNEayTG?mtB=x*wc@ifBCJ|irFVMOvH)AFRW8WE~U()QT=HBCe@s$dA9O!@`zAAT) zaOZ7l6vyR+Nk_OOF!ZlZmjoImKh)dxFbbR~z(cMhfeX1l7S_`;h|v3gI}n9$sSQ>+3@AFAy9=B_y$)q;Wdl|C-X|VV3w8 z2S#>|5dGA8^9%Bu&fhmVRrTX>Z7{~3V&0UpJNEl0=N32euvDGCJ>#6dUSi&PxFW*s zS`}TB>?}H(T2lxBJ!V#2taV;q%zd6fOr=SGHpoSG*4PDaiG0pdb5`jelVipkEk%FV zThLc@Hc_AL1#D&T4D=w@UezYNJ%0=f3iVRuVL5H?eeZM}4W*bomebEU@e2d`M<~uW zf#Bugwf`VezG|^Qbt6R_=U0}|=k;mIIakz99*>FrsQR{0aQRP6ko?5<7bkDN8evZ& zB@_KqQG?ErKL=1*ZM9_5?Pq%lcS4uLSzN(Mr5=t6xHLS~Ym`UgM@D&VNu8e?_=nSFtF$u@hpPSmI4Vo_t&v?>$~K4y(O~Rb*(MFy_igM7 z*~yYUyR6yQgzWnWMUgDov!!g=lInM+=lOmOk4L`O?{i&qxy&D*_qorRbDwj6?)!ef z#JLd7F6Z2I$S0iYI={rZNk*<{HtIl^mx=h>Cim*04K4+Z4IJtd*-)%6XV2(MCscPiw_a+y*?BKbTS@BZ3AUao^%Zi#PhoY9Vib4N>SE%4>=Jco0v zH_Miey{E;FkdlZSq)e<{`+S3W=*ttvD#hB8w=|2aV*D=yOV}(&p%0LbEWH$&@$X3x~CiF-?ejQ*N+-M zc8zT@3iwkdRT2t(XS`d7`tJQAjRmKAhiw{WOqpuvFp`i@Q@!KMhwKgsA}%@sw8Xo5Y=F zhRJZg)O4uqNWj?V&&vth*H#je6T}}p_<>!Dr#89q@uSjWv~JuW(>FqoJ5^ho0%K?E z9?x_Q;kmcsQ@5=}z@tdljMSt9-Z3xn$k)kEjK|qXS>EfuDmu(Z8|(W?gY6-l z@R_#M8=vxKMAoi&PwnaIYw2COJM@atcgfr=zK1bvjW?9B`-+Voe$Q+H$j!1$Tjn+* z&LY<%)L@;zhnJlB^Og6I&BOR-m?{IW;tyYC%FZ!&Z>kGjHJ6cqM-F z&19n+e1=9AH1VrVeHrIzqlC`w9=*zfmrerF?JMzO&|Mmv;!4DKc(sp+jy^Dx?(8>1 zH&yS_4yL7m&GWX~mdfgH*AB4{CKo;+egw=PrvkTaoBU+P-4u?E|&!c z)DKc;>$$B6u*Zr1SjUh2)FeuWLWHl5TH(UHWkf zLs>7px!c5n;rbe^lO@qlYLzlDVp(z?6rPZel=YB)Uv&n!2{+Mb$-vQl=xKw( zve&>xYx+jW_NJh!FV||r?;hdP*jOXYcLCp>DOtJ?2S^)DkM{{Eb zS$!L$e_o0(^}n3tA1R3-$SNvgBq;DOEo}fNc|tB%%#g4RA3{|euq)p+xd3I8^4E&m zFrD%}nvG^HUAIKe9_{tXB;tl|G<%>yk6R;8L2)KUJw4yHJXUOPM>(-+jxq4R;z8H#>rnJy*)8N+$wA$^F zN+H*3t)eFEgxLw+Nw3};4WV$qj&_D`%ADV2%r zJCPCo%{=z7;`F98(us5JnT(G@sKTZ^;2FVitXyLe-S5(hV&Ium+1pIUB(CZ#h|g)u zSLJJ<@HgrDiA-}V_6B^x1>c9B6%~847JkQ!^KLZ2skm;q*edo;UA)~?SghG8;QbHh z_6M;ouo_1rq9=x$<`Y@EA{C%6-pEV}B(1#sDoe_e1s3^Y>n#1Sw;N|}8D|s|VPd+g z-_$QhCz`vLxxrVMx3ape1xu3*wjx=yKSlM~nFgkNWb4?DDr*!?U)L_VeffF<+!j|b zZ$Wn2$TDv3C3V@BHpSgv3JUif8%hk%OsGZ=OxH@8&4`bbf$`aAMchl^qN>Eyu3JH} z9-S!x8-s4fE=lad%Pkp8hAs~u?|uRnL48O|;*DEU! zuS0{cpk%1E0nc__2%;apFsTm0bKtd&A0~S3Cj^?72-*Owk3V!ZG*PswDfS~}2<8le z5+W^`Y(&R)yVF*tU_s!XMcJS`;(Tr`J0%>p=Z&InR%D3@KEzzI+-2)HK zuoNZ&o=wUC&+*?ofPb0a(E6(<2Amd6%uSu_^-<1?hsxs~0K5^f(LsGqgEF^+0_H=uNk9S0bb!|O8d?m5gQjUKevPaO+*VfSn^2892K~%crWM8+6 z25@V?Y@J<9w%@NXh-2!}SK_(X)O4AM1-WTg>sj1{lj5@=q&dxE^9xng1_z9w9DK>| z6Iybcd0e zyi;Ew!KBRIfGPGytQ6}z}MeXCfLY0?9%RiyagSp_D1?N&c{ zyo>VbJ4Gy`@Fv+5cKgUgs~na$>BV{*em7PU3%lloy_aEovR+J7TfQKh8BJXyL6|P8un-Jnq(ghd!_HEOh$zlv2$~y3krgeH;9zC}V3f`uDtW(%mT#944DQa~^8ZI+zAUu4U(j0YcDfKR$bK#gvn_{JZ>|gZ5+)u?T$w7Q%F^;!Wk?G z(le7r!ufT*cxS}PR6hIVtXa)i`d$-_1KkyBU>qmgz-=T};uxx&sKgv48akIWQ89F{ z0XiY?WM^~;|T8zBOr zs#zuOONzH?svv*jokd5SK8wG>+yMC)LYL|vLqm^PMHcT=`}V$=nIRHe2?h)8WQa6O zPAU}d`1y(>kZiP~Gr=mtJLMu`i<2CspL|q2DqAgAD^7*$xzM`PU4^ga`ilE134XBQ z99P(LhHU@7qvl9Yzg$M`+dlS=x^(m-_3t|h>S}E0bcFMn=C|KamQ)=w2^e)35p`zY zRV8X?d;s^>Cof2SPR&nP3E+-LCkS0J$H!eh8~k0qo$}00b=7!H_I2O+Ro@3O$nPdm ztmbOO^B+IHzQ5w>@@@J4cKw5&^_w6s!s=H%&byAbUtczPQ7}wfTqxxtQNfn*u73Qw zGuWsrky_ajPx-5`R<)6xHf>C(oqGf_Fw|-U*GfS?xLML$kv;h_pZ@Kk$y0X(S+K80 z6^|z)*`5VUkawg}=z`S;VhZhxyDfrE0$(PMurAxl~<>lfZa>JZ288ULK7D` zl9|#L^JL}Y$j*j`0-K6kH#?bRmg#5L3iB4Z)%iF@SqT+Lp|{i`m%R-|ZE94Np7Pa5 zCqC^V3}B(FR340pmF*qaa}M}+h6}mqE~7Sh!9bDv9YRT|>vBNAqv09zXHMlcuhKD| zcjjA(b*XCIwJ33?CB!+;{)vX@9xns_b-VO{i0y?}{!sdXj1GM8+$#v>W7nw;+O_9B z_{4L;C6ol?(?W0<6taGEn1^uG=?Q3i29sE`RfYCaV$3DKc_;?HsL?D_fSYg}SuO5U zOB_f4^vZ_x%o`5|C@9C5+o=mFy@au{s)sKw!UgC&L35aH(sgDxRE2De%(%OT=VUdN ziVLEmdOvJ&5*tCMKRyXctCwQu_RH%;m*$YK&m;jtbdH#Ak~13T1^f89tn`A%QEHWs~jnY~E}p_Z$XC z=?YXLCkzVSK+Id`xZYTegb@W8_baLt-Fq`Tv|=)JPbFsKRm)4UW;yT+J`<)%#ue9DPOkje)YF2fsCilK9MIIK>p*`fkoD5nGfmLwt)!KOT+> zOFq*VZktDDyM3P5UOg`~XL#cbzC}eL%qMB=Q5$d89MKuN#$6|4gx_Jt0Gfn8w&q}%lq4QU%6#jT*MRT% zrLz~C8FYKHawn-EQWN1B75O&quS+Z81(zN)G>~vN8VwC+e+y(`>HcxC{MrJ;H1Z4k zZWuv$w_F0-Ub%MVcpIc){4PGL^I7M{>;hS?;eH!;gmcOE66z3;Z1Phqo(t zVP(Hg6q#0gIKgsg7L7WE!{Y#1nI(45tx2{$34dDd#!Z0NIyrm)HOn5W#7;f4pQci# zDW!FI(g4e668kI9{2+mLwB+=#9bfqgX%!B34V-$wwSN(_cm*^{y0jQtv*4}eO^sOV z*9xoNvX)c9isB}Tgx&ZRjp3kwhTVK?r9;n!x>^XYT z@Q^7zp{rkIs{2mUSE^2!Gf6$6;j~&4=-0cSJJDizZp6LTe8b45;{AKM%v99}{{FfC zz709%u0mC=1KXTo(=TqmZQ;c?$M3z(!xah>aywrj40sc2y3rKFw4jCq+Y+u=CH@_V zxz|qeTwa>+<|H%8Dz5u>ZI5MmjTFwXS-Fv!TDd*`>3{krWoNVx$<133`(ftS?ZPyY z&4@ah^3^i`vL$BZa>O|Nt?ucewzsF)0zX3qmM^|waXr=T0pfIb0*$AwU=?Ipl|1Y; z*Pk6{C-p4MY;j@IJ|DW>QHZQJcp;Z~?8(Q+Kk3^0qJ}SCk^*n4W zu9ZFwLHUx-$6xvaQ)SUQcYd6fF8&x)V`1bIuX@>{mE$b|Yd(qomn3;bPwnDUc0F=; zh*6_((%bqAYQWQ~odER?h>1mkL4kpb3s7`0m@rDKGU*oyF)$j~Ffd4fXV$?`f~rHf zB%Y)@5SXZvfwm10RY5X?TEo)PK_`L6qgBp=#>fO49$D zDq8Ozj0q6213tV5Qq=;fZ0$|KroY{Dz=l@lU^J)?Ko@ti20TRplXzphBi>XGx4bou zEWrkNjz0t5j!_ke{g5I#PUlEU$Km8g8TE|XK=MkU@PT4T><2OVamoK;wJ}3X0L$vX zgd7gNa359*nc)R-0!`2X@FOTB`+oETOPc=ubp5R)VQgY+5BTZZJ2?9QwnO=dnulIUF3gFn;BODC2)65)HeVd%t86sL7Rv^Y+nbn+&l z6BAJY(ETvwI)Ts$aiE8rht4KD*qNyE{8{x6R|%akbTBzw;2+6Echkt+W+`u^XX z_z&x%n@00IL3?^hro$gg*4VI_WAaTyVM5Foj~O|-84 z$;06hMwt*rV;^8iB z1~&0XWpYJmG?Ts^K9PC62H*`G}xom%S%yq|xvG~FIfP=9*f zZoDRJBm*Y0aId=qJ?7dyb)6)JGWGwe)MHeNSzhi)Ko6J<-m@v=a%NsP537lHe0R* z`If4$aaBA#S=w!2z&m>{lpTy^Lm^mg*3?M&7HFv}7K6x*cukLIGX;bQG|QWdn{%_6 zHnwBKr84#B7Z+AnBXa16a?or^R?+>$4`}{*a_>IhbjvyTtWkHw)|ay)ahWUd-qq$~ zMbh6roVsj;_qnC-R{G+Cy6bApVOinSU-;(DxUEl!i2)1EeQ9`hrfqj(nKI7?Z>Xur zoJz-a`PxkYit1HEbv|jy%~DO^13J-ut986EEG=66S}D3!L}Efp;Bez~7tNq{QsUMm zh9~(HYg1pA*=37C0}n4g&bFbQ+?-h-W}onYeE{q;cIy%eZK9wZjSwGvT+&Cgv z?~{9p(;bY_1+k|wkt_|N!@J~aoY@|U_RGoWX<;p{Nu*D*&_phw`8jYkMNpRTWx1H* z>J-Mi_!`M468#5Aix$$u1M@rJEIOc?k^QBc?T(#=n&*5eS#u*Y)?L8Ha$9wRWdH^3D4|Ps)Y?m0q~SiKiSfEkJ!=^`lJ(%W3o|CZ zSrZL-Xxc{OrmsQD&s~zPfNJOpSZUl%V8tdG%ei}lQkM+z@-4etFPR>GOH9+Y_F<3=~SXln9Kb-o~f>2a6Xz@AS3cn^;c_>lUwlK(n>z?A>NbC z`Ud8^aQy>wy=$)w;JZzA)_*Y$Z5hU=KAG&htLw1Uh00yE!|Nu{EZkch zY9O6x7Y??>!7pUNME*d!=R#s)ghr|R#41l!c?~=3CS8&zr6*aA7n9*)*PWBV2w+&I zpW1-9fr3j{VTcls1>ua}F*bbju_Xq%^v;-W~paSqlf zolj*dt`BBjHI)H9{zrkBo=B%>8}4jeBO~kWqO!~Thi!I1H(in=n^fS%nuL=X2+s!p}HfTU#NBGiwEBF^^tKU zbhhv+0dE-sbK$>J#t-J!B$TMgN@Wh5wTtK2BG}4BGfsZOoRUS#G8Cxv|6EI*n&Xxq zt{&OxCC+BNqz$9b0WM7_PyBJEVObHFh%%`~!@MNZlo*oXDCwDcFwT~Rls!aApL<)^ zbBftGKKBRhB!{?fX@l2_y~%ygNFfF(XJzHh#?`WlSL{1lKT*gJM zs>bd^H9NCxqxn(IOky5k-wALFowQr(gw%|`0991u#9jXQh?4l|l>pd6a&rx|v=fPJ z1mutj{YzpJ_gsClbWFk(G}bSlFi-6@mwoQh-XeD*j@~huW4(8ub%^I|azA)h2t#yG z7e_V_<4jlM3D(I+qX}yEtqj)cpzN*oCdYHa!nm%0t^wHm)EmFP*|FMw!tb@&`G-u~ zK)=Sf6z+BiTAI}}i{*_Ac$ffr*Wrv$F7_0gJkjx;@)XjYSh`RjAgrCck`x!zP>Ifu z&%he4P|S)H*(9oB4uvH67^0}I-_ye_!w)u3v2+EY>eD3#8QR24<;7?*hj8k~rS)~7 zSXs5ww)T(0eHSp$hEIBnW|Iun<_i`}VE0Nc$|-R}wlSIs5pV{g_Dar(Zz<4X3`W?K z6&CAIl4U(Qk-tTcK{|zYF6QG5ArrEB!;5s?tW7 zrE3hcFY&k)+)e{+YOJ0X2uDE_hd2{|m_dC}kgEKqiE9Q^A-+>2UonB+L@v3$9?AYw zVQv?X*pK;X4Ovc6Ev5Gbg{{Eu*7{N3#0@9oMI~}KnObQE#Y{&3mM4`w%wN+xrKYgD zB-ay0Q}m{QI;iY`s1Z^NqIkjrTlf`B)B#MajZ#9u41oRBC1oM1vq0i|F59> z#StM@bHt|#`2)cpl_rWB($DNJ3Lap}QM-+A$3pe}NyP(@+i1>o^fe-oxX#Bt`mcQc zb?pD4W%#ep|3%CHAYnr*^M6Czg>~L4?l16H1OozM{P*en298b+`i4$|w$|4AHbzqB zHpYUsHZET$Z0ztC;U+0*+amF!@PI%^oUIZy{`L{%O^i{Xk}X0&nl)n~tVEpcAJSJ} zverw15zP1P-O8h9nd!&hj$zuwjg?DoxYIw{jWM zW5_pj+wFy8Tsa9g<7Qa21WaV&;ejoYflRKcz?#fSH_)@*QVlN2l4(QNk| z4aPnv&mrS&0|6NHq05XQw$J^RR9T{3SOcMKCXIR1iSf+xJ0E_Wv?jEc*I#ZPzyJN2 zUG0UOXHl+PikM*&g$U@g+KbG-RY>uaIl&DEtw_Q=FYq?etc!;hEC_}UX{eyh%dw2V zTTSlap&5>PY{6I#(6`j-9`D&I#|YPP8a;(sOzgeKDWsLa!i-$frD>zr-oid!Hf&yS z!i^cr&7tN}OOGmX2)`8k?Tn!!4=tz~3hCTq_9CdiV!NIblUDxHh(FJ$zs)B2(t5@u z-`^RA1ShrLCkg0)OhfoM;4Z{&oZmAec$qV@ zGQ(7(!CBk<5;Ar%DLJ0p0!ResC#U<+3i<|vib1?{5gCebG7$F7URKZXuX-2WgF>YJ^i zMhHDBsh9PDU8dlZ$yJKtc6JA#y!y$57%sE>4Nt+wF1lfNIWyA`=hF=9Gj%sRwi@vd z%2eVV3y&dvAgyuJ=eNJR+*080dbO_t@BFJO<@&#yqTK&+xc|FRR;p;KVk@J3$S{p` zGaMj6isho#%m)?pOG^G0mzOAw0z?!AEMsv=0T>WWcE>??WS=fII$t$(^PDPMU(P>o z_*0s^W#|x)%tx8jIgZY~A2yG;US0m2ZOQt6yJqW@XNY_>_R7(Nxb8Ged6BdYW6{prd!|zuX$@Q2o6Ona8zzYC1u!+2!Y$Jc9a;wy+pXt}o6~Bu1oF1c zp7Y|SBTNi@=I(K%A60PMjM#sfH$y*c{xUgeSpi#HB`?|`!Tb&-qJ3;vxS!TIzuTZs-&%#bAkAyw9m4PJgvey zM5?up*b}eDEY+#@tKec)-c(#QF0P?MRlD1+7%Yk*jW;)`f;0a-ZJ6CQA?E%>i2Dt7T9?s|9ZF|KP4;CNWvaVKZ+Qeut;Jith_y{v*Ny6Co6!8MZx;Wgo z=qAi%&S;8J{iyD&>3CLCQdTX*$+Rx1AwA*D_J^0>suTgBMBb=*hefV+Ars#mmr+YsI3#!F@Xc1t4F-gB@6aoyT+5O(qMz*zG<9Qq*f0w^V!03rpr*-WLH}; zfM{xSPJeu6D(%8HU%0GEa%waFHE$G?FH^kMS-&I3)ycx|iv{T6Wx}9$$D&6{%1N_8 z_CLw)_9+O4&u94##vI9b-HHm_95m)fa??q07`DniVjAy`t7;)4NpeyAY(aAk(+T_O z1om+b5K2g_B&b2DCTK<>SE$Ode1DopAi)xaJjU>**AJK3hZrnhEQ9E`2=|HHe<^tv z63e(bn#fMWuz>4erc47}!J>U58%<&N<6AOAewyzNTqi7hJc|X{782&cM zHZYclNbBwU6673=!ClmxMfkC$(CykGR@10F!zN1Se83LR&a~$Ht&>~43OX22mt7tcZUpa;9@q}KDX3O&Ugp6< zLZLfIMO5;pTee1vNyVC$FGxzK2f>0Z-6hM82zKg44nWo|n}$Zk6&;5ry3`(JFEX$q zK&KivAe${e^5ZGc3a9hOt|!UOE&OocpVryE$Y4sPcs4rJ>>Kbi2_subQ9($2VN(3o zb~tEzMsHaBmBtaHAyES+d3A(qURgiskSSwUc9CfJ@99&MKp2sooSYZu+-0t0+L*!I zYagjOlPgx|lep9tiU%ts&McF6b0VE57%E0Ho%2oi?=Ks+5%aj#au^OBwNwhec zta6QAeQI^V!dF1C)>RHAmB`HnxyqWx?td@4sd15zPd*Fc9hpDXP23kbBenBxGeD$k z;%0VBQEJ-C)&dTAw_yW@k0u?IUk*NrkJ)(XEeI z9Y>6Vel>#s_v@=@0<{4A{pl=9cQ&Iah0iD0H`q)7NeCIRz8zx;! z^OO;1+IqoQNak&pV`qKW+K0^Hqp!~gSohcyS)?^P`JNZXw@gc6{A3OLZ?@1Uc^I2v z+X!^R*HCm3{7JPq{8*Tn>5;B|X7n4QQ0Bs79uTU%nbqOJh`nX(BVj!#f;#J+WZxx4 z_yM&1Y`2XzhfqkIMO7tB3raJKQS+H5F%o83bM+hxbQ zeeJm=Dvix$2j|b4?mDacb67v-1^lTp${z=jc1=j~QD>7c*@+1?py>%Kj%Ejp7Y-!? z8iYRUlGVrQPandAaxFfks53@2EC#0)%mrnmGRn&>=$H$S8q|kE_iWko4`^vCS2aWg z#!`RHUGyOt*k?bBYu3*j3u0gB#v(3tsije zgIuNNWNtrOkx@Pzs;A9un+2LX!zw+p3_NX^Sh09HZAf>m8l@O*rXy_82aWT$Q>iyy zqO7Of)D=wcSn!0+467&!Hl))eff=$aneB?R!YykdKW@k^_uR!+Q1tR)+IJb`-6=jj zymzA>Sv4>Z&g&WWu#|~GcP7qP&m*w-S$)7Xr;(duqCTe7p8H3k5>Y-n8438+%^9~K z3r^LIT_K{i7DgEJjIocw_6d0!<;wKT`X;&vv+&msmhAAnIe!OTdybPctzcEzBy88_ zWO{6i4YT%e4^WQZB)KHCvA(0tS zHu_Bg+6Ko%a9~$EjRB90`P(2~6uI@SFibxct{H#o&y40MdiXblu@VFXbhz>Nko;7R z70Ntmm-FePqhb%9gL+7U8@(ch|JfH5Fm)5${8|`Lef>LttM_iww6LW2X61ldBmG0z zax3y)njFe>j*T{i0s8D4=L>X^j0)({R5lMGVS#7(2C9@AxL&C-lZQx~czI7Iv+{%1 z2hEG>RzX4S8x3v#9sgGAnPzptM)g&LB}@%E>fy0vGSa(&q0ch|=ncKjNrK z`jA~jObJhrJ^ri|-)J^HUyeZXz~XkBp$VhcTEcTdc#a2EUOGVX?@mYx#Vy*!qO$Jv zQ4rgOJ~M*o-_Wptam=~krnmG*p^j!JAqoQ%+YsDFW7Cc9M%YPiBOrVcD^RY>m9Pd< zu}#9M?K{+;UIO!D9qOpq9yxUquQRmQNMo0pT`@$pVt=rMvyX)ph(-CCJLvUJy71DI zBk7oc7)-%ngdj~s@76Yse3L^gV0 z2==qfp&Q~L(+%RHP0n}+xH#k(hPRx(!AdBM$JCfJ5*C=K3ts>P?@@SZ_+{U2qFZb>4kZ{Go37{# zSQc+-dq*a-Vy4?taS&{Ht|MLRiS)Sn14JOONyXqPNnpq&2y~)6wEG0oNy>qvod$FF z`9o&?&6uZjhZ4_*5qWVrEfu(>_n2Xi2{@Gz9MZ8!YmjYvIMasE9yVQL10NBrTCczq zcTY1q^PF2l!Eraguf{+PtHV3=2A?Cu&NN&a8V(y;q(^_mFc6)%Yfn&X&~Pq zU1?qCj^LF(EQB1F`8NxNjyV%fde}dEa(Hx=r7$~ts2dzDwyi6ByBAIx$NllB4%K=O z$AHz1<2bTUb>(MCVPpK(E9wlLElo(aSd(Os)^Raum`d(g9Vd_+Bf&V;l=@mM=cC>) z)9b0enb)u_7V!!E_bl>u5nf&Rl|2r=2F3rHMdb7y9E}}F82^$Rf+P8%dKnOeKh1vs zhH^P*4Ydr^$)$h@4KVzxrHyy#cKmWEa9P5DJ|- zG;!Qi35Tp7XNj60=$!S6U#!(${6hyh7d4q=pF{`0t|N^|L^d8pD{O9@tF~W;#Je*P z&ah%W!KOIN;SyAEhAeTafJ4uEL`(RtnovM+cb(O#>xQnk?dzAjG^~4$dFn^<@-Na3 z395;wBnS{t*H;Jef2eE!2}u5Ns{AHj>WYZDgQJt8v%x?9{MXqJsGP|l%OiZqQ1aB! z%E=*Ig`(!tHh>}4_z5IMpg{49UvD*Pp9!pxt_gdAW%sIf3k6CTycOT1McPl=_#0?8 zVjz8Hj*Vy9c5-krd-{BQ{6Xy|P$6LJvMuX$* zA+@I_66_ET5l2&gk9n4$1M3LN8(yEViRx&mtd#LD}AqEs?RW=xKC(OCWH;~>(X6h!uDxXIPH06xh z*`F4cVlbDP`A)-fzf>MuScYsmq&1LUMGaQ3bRm6i7OsJ|%uhTDT zlvZA1M}nz*SalJWNT|`dBm1$xlaA>CCiQ zK`xD-RuEn>-`Z?M{1%@wewf#8?F|(@1e0+T4>nmlSRrNK5f)BJ2H*$q(H>zGD0>eL zQ!tl_Wk)k*e6v^m*{~A;@6+JGeWU-q9>?+L_#UNT%G?4&BnOgvm9@o7l?ov~XL+et zbGT)|G7)KAeqb=wHSPk+J1bdg7N3$vp(ekjI1D9V$G5Cj!=R2w=3*4!z*J-r-cyeb zd(i2KmX!|Lhey!snRw z?#$Gu%S^SQEKt&kep)up#j&9}e+3=JJBS(s>MH+|=R(`8xK{mmndWo_r`-w1#SeRD&YtAJ#GiVI*TkQZ}&aq<+bU2+coU3!jCI6E+Ad_xFW*ghnZ$q zAoF*i&3n1j#?B8x;kjSJD${1jdRB;)R*)Ao!9bd|C7{;iqDo|T&>KSh6*hCD!rwv= zyK#F@2+cv3=|S1Kef(E6Niv8kyLVLX&e=U;{0x{$tDfShqkjUME>f8d(5nzSkY6@! z^-0>DM)wa&%m#UF1F?zR`8Y3X#tA!*7Q$P3lZJ%*KNlrk_uaPkxw~ zxZ1qlE;Zo;nb@!SMazSjM>;34ROOoygo%SF);LL>rRonWwR>bmSd1XD^~sGSu$Gg# zFZ`|yKU0%!v07dz^v(tY%;So(e`o{ZYTX`hm;@b0%8|H>VW`*cr8R%3n|ehw2`(9B+V72`>SY}9^8oh$En80mZK9T4abVG*to;E z1_S6bgDOW?!Oy1LwYy=w3q~KKdbNtyH#d24PFjX)KYMY93{3-mPP-H>@M-_>N~DDu zENh~reh?JBAK=TFN-SfDfT^=+{w4ea2KNWXq2Y<;?(gf(FgVp8Zp-oEjKzB%2Iqj;48GmY3h=bcdYJ}~&4tS`Q1sb=^emaW$IC$|R+r-8V- zf0$gGE(CS_n4s>oicVk)MfvVg#I>iDvf~Ov8bk}sSxluG!6#^Z_zhB&U^`eIi1@j( z^CK$z^stBHtaDDHxn+R;3u+>Lil^}fj?7eaGB z&5nl^STqcaBxI@v>%zG|j))G(rVa4aY=B@^2{TFkW~YP!8!9TG#(-nOf^^X-%m9{Z zCC?iC`G-^RcBSCuk=Z`(FaUUe?hf3{0C>>$?Vs z`2Uud9M+T&KB6o4o9kvdi^Q=Bw!asPdxbe#W-Oaa#_NP(qpyF@bVxv5D5))srkU#m zj_KA+#7sqDn*Ipf!F5Byco4HOSd!Ui$l94|IbW%Ny(s1>f4|Mv^#NfB31N~kya9!k zWCGL-$0ZQztBate^fd>R!hXY_N9ZjYp3V~4_V z#eB)Kjr8yW=+oG)BuNdZG?jaZlw+l_ma8aET(s+-x+=F-t#Qoiuu1i`^x8Sj>b^U} zs^z<()YMFP7CmjUC@M=&lA5W7t&cxTlzJAts*%PBDAPuqcV5o7HEnqjif_7xGt)F% zGx2b4w{@!tE)$p=l3&?Bf#`+!-RLOleeRk3 z7#pF|w@6_sBmn1nECqdunmG^}pr5(ZJQVvAt$6p3H(16~;vO>?sTE`Y+mq5YP&PBo zvq!7#W$Gewy`;%6o^!Dtjz~x)T}Bdk*BS#=EY=ODD&B=V6TD2z^hj1m5^d6s)D*wk zu$z~D7QuZ2b?5`p)E8e2_L38v3WE{V`bVk;6fl#o2`) z99JsWhh?$oVRn@$S#)uK&8DL8>An0&S<%V8hnGD7Z^;Y(%6;^9!7kDQ5bjR_V+~wp zfx4m3z6CWmmZ<8gDGUyg3>t8wgJ5NkkiEm^(sedCicP^&3D%}6LtIUq>mXCAt{9eF zNXL$kGcoUTf_Lhm`t;hD-SE)m=iBnxRU(NyL}f6~1uH)`K!hmYZjLI%H}AmEF5RZt z06$wn63GHnApHXZZJ}s^s)j9(BM6e*7IBK6Bq(!)d~zR#rbxK9NVIlgquoMq z=eGZ9NR!SEqP6=9UQg#@!rtbbSBUM#ynF);zKX+|!Zm}*{H z+j=d?aZ2!?@EL7C~%B?6ouCKLnO$uWn;Y6Xz zX8dSwj732u(o*U3F$F=7xwxm>E-B+SVZH;O-4XPuPkLSt_?S0)lb7EEg)Mglk0#eS z9@jl(OnH4juMxY+*r03VDfPx_IM!Lmc(5hOI;`?d37f>jPP$?9jQQIQU@i4vuG6MagEoJrQ=RD7xt@8E;c zeGV*+Pt+t$@pt!|McETOE$9k=_C!70uhwRS9X#b%ZK z%q(TIUXSS^F0`4Cx?Rk07C6wI4!UVPeI~-fxY6`YH$kABdOuiRtl73MqG|~AzZ@iL&^s?24iS;RK_pdlWkhcF z@Wv-Om(Aealfg)D^adlXh9Nvf~Uf@y;g3Y)i(YP zEXDnb1V}1pJT5ZWyw=1i+0fni9yINurD=EqH^ciOwLUGi)C%Da)tyt=zq2P7pV5-G zR7!oq28-Fgn5pW|nlu^b!S1Z#r7!Wtr{5J5PQ>pd+2P7RSD?>(U7-|Y z7ZQ5lhYIl_IF<9?T9^IPK<(Hp;l5bl5tF9>X-zG14_7PfsA>6<$~A338iYRT{a@r_ zuXBaT=`T5x3=s&3=RYx6NgG>No4?5KFBVjE(swfcivcIpPQFx5l+O;fiGsOrl5teR z_Cm+;PW}O0Dwe_(4Z@XZ)O0W-v2X><&L*<~*q3dg;bQW3g7)a#3KiQP>+qj|qo*Hk z?57>f2?f@`=Fj^nkDKeRkN2d$Z@2eNKpHo}ksj-$`QKb6n?*$^*%Fb3_Kbf1(*W9K>{L$mud2WHJ=j0^=g30Xhg8$#g^?36`p1fm;;1@0Lrx+8t`?vN0ZorM zSW?rhjCE8$C|@p^sXdx z|NOHHg+fL;HIlqyLp~SSdIF`TnSHehNCU9t89yr@)FY<~hu+X`tjg(aSVae$wDG*C zq$nY(Y494R)hD!i1|IIyP*&PD_c2FPgeY)&mX1qujB1VHPG9`yFQpLFVQ0>EKS@Bp zAfP5`C(sWGLI?AC{XEjLKR4FVNw(4+9b?kba95ukgR1H?w<8F7)G+6&(zUhIE5Ef% z=fFkL3QKA~M@h{nzjRq!Y_t!%U66#L8!(2-GgFxkD1=JRRqk=n%G(yHKn%^&$dW>; zSjAcjETMz1%205se$iH_)ZCpfg_LwvnsZQAUCS#^FExp8O4CrJb6>JquNV@qPq~3A zZ<6dOU#6|8+fcgiA#~MDmcpIEaUO02L5#T$HV0$EMD94HT_eXLZ2Zi&(! z&5E>%&|FZ`)CN10tM%tLSPD*~r#--K(H-CZqIOb99_;m|D5wdgJ<1iOJz@h2Zkq?} z%8_KXb&hf=2Wza(Wgc;3v3TN*;HTU*q2?#z&tLn_U0Nt!y>Oo>+2T)He6%XuP;fgn z-G!#h$Y2`9>Jtf}hbVrm6D70|ERzLAU>3zoWhJmjWfgM^))T+2u$~5>HF9jQDkrXR z=IzX36)V75PrFjkQ%TO+iqKGCQ-DDXbaE;C#}!-CoWQx&v*vHfyI>$HNRbpvm<`O( zlx9NBWD6_e&J%Ous4yp~s6)Ghni!I6)0W;9(9$y1wWu`$gs<$9Mcf$L*piP zPR0Av*2%ul`W;?-1_-5Zy0~}?`e@Y5A&0H!^ApyVTT}BiOm4GeFo$_oPlDEyeGBbh z1h3q&Dx~GmUS|3@4V36&$2uO8!Yp&^pD7J5&TN{?xphf*-js1fP?B|`>p_K>lh{ij zP(?H%e}AIP?_i^f&Li=FDSQ`2_NWxL+BB=nQr=$ zHojMlXNGauvvwPU>ZLq!`bX-5F4jBJ&So{kE5+ms9UEYD{66!|k~3vsP+mE}x!>%P za98bAU0!h0&ka4EoiDvBM#CP#dRNdXJcb*(%=<(g+M@<)DZ!@v1V>;54En?igcHR2 zhubQMq}VSOK)onqHfczM7YA@s=9*ow;k;8)&?J3@0JiGcP! zP#00KZ1t)GyZeRJ=f0^gc+58lc4Qh*S7RqPIC6GugG1gXe$LIQMRCo8cHf^qXgAa2 z`}t>u2Cq1CbSEpLr~E=c7~=Qkc9-vLE%(v9N*&HF`(d~(0`iukl5aQ9u4rUvc8%m) zr2GwZN4!s;{SB87lJB;veebPmqE}tSpT>+`t?<457Q9iV$th%i__Z1kOMAswFldD6 ztbOvO337S5o#ZZgN2G99_AVqPv!?Gmt3pzgD+Hp3QPQ`9qJ(g=kjvD+fUSS3upJn! zqoG7acIKEFRX~S}3|{EWT$kdz#zrDlJU(rPkxjws_iyLKU8+v|*oS_W*-guAb&Pj1 z35Z`3z<&Jb@2Mwz=KXucNYdY#SNO$tcVFr9KdKm|%^e-TXzs6M`PBper%ajkrIyUe zp$vVxVs9*>Vp4_1NC~Zg)WOCPmOxI1V34QlG4!aSFOH{QqSVq1^1)- z0P!Z?tT&E-ll(pwf0?=F=yOzik=@nh1Clxr9}Vij89z)ePDSCYAqw?lVI?v?+&*zH z)p$CScFI8rrwId~`}9YWPFu0cW1Sf@vRELs&cbntRU6QfPK-SO*mqu|u~}8AJ!Q$z znzu}50O=YbjwKCuSVBs6&CZR#0FTu)3{}qJJYX(>QPr4$RqWiwX3NT~;>cLn*_&1H zaKpIW)JVJ>b{uo2oq>oQt3y=zJjb%fU@wLqM{SyaC6x2snMx-}ivfU<1- znu1Lh;i$3Tf$Kh5Uk))G!D1UhE8pvx&nO~w^fG)BC&L!_hQk%^p`Kp@F{cz>80W&T ziOK=Sq3fdRu*V0=S53rcIfWFazI}Twj63CG(jOB;$*b`*#B9uEnBM`hDk*EwSRdwP8?5T?xGUKs=5N83XsR*)a4|ijz|c{4tIU+4j^A5C<#5 z*$c_d=5ml~%pGxw#?*q9N7aRwPux5EyqHVkdJO=5J>84!X6P>DS8PTTz>7C#FO?k#edkntG+fJk8ZMn?pmJSO@`x-QHq;7^h6GEXLXo1TCNhH z8ZDH{*NLAjo3WM`xeb=X{((uv3H(8&r8fJJg_uSs_%hOH%JDD?hu*2NvWGYD+j)&` zz#_1%O1wF^o5ryt?O0n;`lHbzp0wQ?rcbW(F1+h7_EZZ9{>rePvLAPVZ_R|n@;b$;UchU=0j<6k8G9QuQf@76oiE*4 zXOLQ&n3$NR#p4<5NJMVC*S);5x2)eRbaAM%VxWu9ohlT;pGEk7;002enCbQ>2r-us z3#bpXP9g|mE`65VrN`+3mC)M(eMj~~eOf)do<@l+fMiTR)XO}422*1SL{wyY(%oMpBgJagtiDf zz>O6(m;};>Hi=t8o{DVC@YigqS(Qh+ix3Rwa9aliH}a}IlOCW1@?%h_bRbq-W{KHF z%Vo?-j@{Xi@=~Lz5uZP27==UGE15|g^0gzD|3x)SCEXrx`*MP^FDLl%pOi~~Il;dc z^hrwp9sYeT7iZ)-ajKy@{a`kr0-5*_!XfBpXwEcFGJ;%kV$0Nx;apKrur zJN2J~CAv{Zjj%FolyurtW8RaFmpn&zKJWL>(0;;+q(%(Hx!GMW4AcfP0YJ*Vz!F4g z!ZhMyj$BdXL@MlF%KeInmPCt~9&A!;cRw)W!Hi@0DY(GD_f?jeV{=s=cJ6e}JktJw zQORnxxj3mBxfrH=x{`_^Z1ddDh}L#V7i}$njUFRVwOX?qOTKjfPMBO4y(WiU<)epb zvB9L=%jW#*SL|Nd_G?E*_h1^M-$PG6Pc_&QqF0O-FIOpa4)PAEPsyvB)GKasmBoEt z?_Q2~QCYGH+hW31x-B=@5_AN870vY#KB~3a*&{I=f);3Kv7q4Q7s)0)gVYx2#Iz9g(F2;=+Iy4 z6KI^8GJ6D@%tpS^8boU}zpi=+(5GfIR)35PzrbuXeL1Y1N%JK7PG|^2k3qIqHfX;G zQ}~JZ-UWx|60P5?d1e;AHx!_;#PG%d=^X(AR%i`l0jSpYOpXoKFW~7ip7|xvN;2^? zsYC9fanpO7rO=V7+KXqVc;Q5z%Bj})xHVrgoR04sA2 zl~DAwv=!(()DvH*=lyhIlU^hBkA0$e*7&fJpB0|oB7)rqGK#5##2T`@_I^|O2x4GO z;xh6ROcV<9>?e0)MI(y++$-ksV;G;Xe`lh76T#Htuia+(UrIXrf9?

L(tZ$0BqX1>24?V$S+&kLZ`AodQ4_)P#Q3*4xg8}lMV-FLwC*cN$< zt65Rf%7z41u^i=P*qO8>JqXPrinQFapR7qHAtp~&RZ85$>ob|Js;GS^y;S{XnGiBc zGa4IGvDl?x%gY`vNhv8wgZnP#UYI-w*^4YCZnxkF85@ldepk$&$#3EAhrJY0U)lR{F6sM3SONV^+$;Zx8BD&Eku3K zKNLZyBni3)pGzU0;n(X@1fX8wYGKYMpLmCu{N5-}epPDxClPFK#A@02WM3!myN%bkF z|GJ4GZ}3sL{3{qXemy+#Uk{4>Kf8v11;f8I&c76+B&AQ8udd<8gU7+BeWC`akUU~U zgXoxie>MS@rBoyY8O8Tc&8id!w+_ooxcr!1?#rc$-|SBBtH6S?)1e#P#S?jFZ8u-Bs&k`yLqW|{j+%c#A4AQ>+tj$Y z^CZajspu$F%73E68Lw5q7IVREED9r1Ijsg#@DzH>wKseye>hjsk^{n0g?3+gs@7`i zHx+-!sjLx^fS;fY!ERBU+Q zVJ!e0hJH%P)z!y%1^ZyG0>PN@5W~SV%f>}c?$H8r;Sy-ui>aruVTY=bHe}$e zi&Q4&XK!qT7-XjCrDaufT@>ieQ&4G(SShUob0Q>Gznep9fR783jGuUynAqc6$pYX; z7*O@@JW>O6lKIk0G00xsm|=*UVTQBB`u1f=6wGAj%nHK_;Aqmfa!eAykDmi-@u%6~ z;*c!pS1@V8r@IX9j&rW&d*}wpNs96O2Ute>%yt{yv>k!6zfT6pru{F1M3P z2WN1JDYqoTB#(`kE{H676QOoX`cnqHl1Yaru)>8Ky~VU{)r#{&s86Vz5X)v15ULHA zAZDb{99+s~qI6;-dQ5DBjHJP@GYTwn;Dv&9kE<0R!d z8tf1oq$kO`_sV(NHOSbMwr=To4r^X$`sBW4$gWUov|WY?xccQJN}1DOL|GEaD_!@& z15p?Pj+>7d`@LvNIu9*^hPN)pwcv|akvYYq)ks%`G>!+!pW{-iXPZsRp8 z35LR;DhseQKWYSD`%gO&k$Dj6_6q#vjWA}rZcWtQr=Xn*)kJ9kacA=esi*I<)1>w^ zO_+E>QvjP)qiSZg9M|GNeLtO2D7xT6vsj`88sd!94j^AqxFLi}@w9!Y*?nwWARE0P znuI_7A-saQ+%?MFA$gttMV-NAR^#tjl_e{R$N8t2NbOlX373>e7Ox=l=;y#;M7asp zRCz*CLnrm$esvSb5{T<$6CjY zmZ(i{Rs_<#pWW>(HPaaYj`%YqBra=Ey3R21O7vUbzOkJJO?V`4-D*u4$Me0Bx$K(lYo`JO}gnC zx`V}a7m-hLU9Xvb@K2ymioF)vj12<*^oAqRuG_4u%(ah?+go%$kOpfb`T96P+L$4> zQ#S+sA%VbH&mD1k5Ak7^^dZoC>`1L%i>ZXmooA!%GI)b+$D&ziKrb)a=-ds9xk#~& z7)3iem6I|r5+ZrTRe_W861x8JpD`DDIYZNm{$baw+$)X^Jtjnl0xlBgdnNY}x%5za zkQ8E6T<^$sKBPtL4(1zi_Rd(tVth*3Xs!ulflX+70?gb&jRTnI8l+*Aj9{|d%qLZ+ z>~V9Z;)`8-lds*Zgs~z1?Fg?Po7|FDl(Ce<*c^2=lFQ~ahwh6rqSjtM5+$GT>3WZW zj;u~w9xwAhOc<kF}~`CJ68 z?(S5vNJa;kriPlim33{N5`C{9?NWhzsna_~^|K2k4xz1`xcui*LXL-1#Y}Hi9`Oo!zQ>x-kgAX4LrPz63uZ+?uG*84@PKq-KgQlMNRwz=6Yes) zY}>YN+qP}nwr$(CZQFjUOI=-6J$2^XGvC~EZ+vrqWaOXB$k?%Suf5k=4>AveC1aJ! ziaW4IS%F$_Babi)kA8Y&u4F7E%99OPtm=vzw$$ zEz#9rvn`Iot_z-r3MtV>k)YvErZ<^Oa${`2>MYYODSr6?QZu+be-~MBjwPGdMvGd!b!elsdi4% z`37W*8+OGulab8YM?`KjJ8e+jM(tqLKSS@=jimq3)Ea2EB%88L8CaM+aG7;27b?5` z4zuUWBr)f)k2o&xg{iZ$IQkJ+SK>lpq4GEacu~eOW4yNFLU!Kgc{w4&D$4ecm0f}~ zTTzquRW@`f0}|IILl`!1P+;69g^upiPA6F{)U8)muWHzexRenBU$E^9X-uIY2%&1w z_=#5*(nmxJ9zF%styBwivi)?#KMG96-H@hD-H_&EZiRNsfk7mjBq{L%!E;Sqn!mVX*}kXhwH6eh;b42eD!*~upVG@ z#smUqz$ICm!Y8wY53gJeS|Iuard0=;k5i5Z_hSIs6tr)R4n*r*rE`>38Pw&lkv{_r!jNN=;#?WbMj|l>cU(9trCq; z%nN~r^y7!kH^GPOf3R}?dDhO=v^3BeP5hF|%4GNQYBSwz;x({21i4OQY->1G=KFyu z&6d`f2tT9Yl_Z8YACZaJ#v#-(gcyeqXMhYGXb=t>)M@fFa8tHp2x;ODX=Ap@a5I=U z0G80^$N0G4=U(>W%mrrThl0DjyQ-_I>+1Tdd_AuB3qpYAqY54upwa3}owa|x5iQ^1 zEf|iTZxKNGRpI>34EwkIQ2zHDEZ=(J@lRaOH>F|2Z%V_t56Km$PUYu^xA5#5Uj4I4RGqHD56xT%H{+P8Ag>e_3pN$4m8n>i%OyJFPNWaEnJ4McUZPa1QmOh?t8~n& z&RulPCors8wUaqMHECG=IhB(-tU2XvHP6#NrLVyKG%Ee*mQ5Ps%wW?mcnriTVRc4J`2YVM>$ixSF2Xi+Wn(RUZnV?mJ?GRdw%lhZ+t&3s7g!~g{%m&i<6 z5{ib-<==DYG93I(yhyv4jp*y3#*WNuDUf6`vTM%c&hiayf(%=x@4$kJ!W4MtYcE#1 zHM?3xw63;L%x3drtd?jot!8u3qeqctceX3m;tWetK+>~q7Be$h>n6riK(5@ujLgRS zvOym)k+VAtyV^mF)$29Y`nw&ijdg~jYpkx%*^ z8dz`C*g=I?;clyi5|!27e2AuSa$&%UyR(J3W!A=ZgHF9OuKA34I-1U~pyD!KuRkjA zbkN!?MfQOeN>DUPBxoy5IX}@vw`EEB->q!)8fRl_mqUVuRu|C@KD-;yl=yKc=ZT0% zB$fMwcC|HE*0f8+PVlWHi>M`zfsA(NQFET?LrM^pPcw`cK+Mo0%8*x8@65=CS_^$cG{GZQ#xv($7J z??R$P)nPLodI;P!IC3eEYEHh7TV@opr#*)6A-;EU2XuogHvC;;k1aI8asq7ovoP!* z?x%UoPrZjj<&&aWpsbr>J$Er-7!E(BmOyEv!-mbGQGeJm-U2J>74>o5x`1l;)+P&~ z>}f^=Rx(ZQ2bm+YE0u=ZYrAV@apyt=v1wb?R@`i_g64YyAwcOUl=C!i>=Lzb$`tjv zOO-P#A+)t-JbbotGMT}arNhJmmGl-lyUpMn=2UacVZxmiG!s!6H39@~&uVokS zG=5qWhfW-WOI9g4!R$n7!|ViL!|v3G?GN6HR0Pt_L5*>D#FEj5wM1DScz4Jv@Sxnl zB@MPPmdI{(2D?;*wd>3#tjAirmUnQoZrVv`xM3hARuJksF(Q)wd4P$88fGYOT1p6U z`AHSN!`St}}UMBT9o7i|G`r$ zrB=s$qV3d6$W9@?L!pl0lf%)xs%1ko^=QY$ty-57=55PvP(^6E7cc zGJ*>m2=;fOj?F~yBf@K@9qwX0hA803Xw+b0m}+#a(>RyR8}*Y<4b+kpp|OS+!whP( zH`v{%s>jsQI9rd$*vm)EkwOm#W_-rLTHcZRek)>AtF+~<(did)*oR1|&~1|e36d-d zgtm5cv1O0oqgWC%Et@P4Vhm}Ndl(Y#C^MD03g#PH-TFy+7!Osv1z^UWS9@%JhswEq~6kSr2DITo59+; ze=ZC}i2Q?CJ~Iyu?vn|=9iKV>4j8KbxhE4&!@SQ^dVa-gK@YfS9xT(0kpW*EDjYUkoj! zE49{7H&E}k%5(>sM4uGY)Q*&3>{aitqdNnRJkbOmD5Mp5rv-hxzOn80QsG=HJ_atI-EaP69cacR)Uvh{G5dTpYG7d zbtmRMq@Sexey)||UpnZ?;g_KMZq4IDCy5}@u!5&B^-=6yyY{}e4Hh3ee!ZWtL*s?G zxG(A!<9o!CL+q?u_utltPMk+hn?N2@?}xU0KlYg?Jco{Yf@|mSGC<(Zj^yHCvhmyx z?OxOYoxbptDK()tsJ42VzXdINAMWL$0Gcw?G(g8TMB)Khw_|v9`_ql#pRd2i*?CZl z7k1b!jQB=9-V@h%;Cnl7EKi;Y^&NhU0mWEcj8B|3L30Ku#-9389Q+(Yet0r$F=+3p z6AKOMAIi|OHyzlHZtOm73}|ntKtFaXF2Fy|M!gOh^L4^62kGUoWS1i{9gsds_GWBc zLw|TaLP64z3z9?=R2|T6Xh2W4_F*$cq>MtXMOy&=IPIJ`;!Tw?PqvI2b*U1)25^<2 zU_ZPoxg_V0tngA0J+mm?3;OYw{i2Zb4x}NedZug!>EoN3DC{1i)Z{Z4m*(y{ov2%- zk(w>+scOO}MN!exSc`TN)!B=NUX`zThWO~M*ohqq;J2hx9h9}|s#?@eR!=F{QTrq~ zTcY|>azkCe$|Q0XFUdpFT=lTcyW##i;-e{}ORB4D?t@SfqGo_cS z->?^rh$<&n9DL!CF+h?LMZRi)qju!meugvxX*&jfD!^1XB3?E?HnwHP8$;uX{Rvp# zh|)hM>XDv$ZGg=$1{+_bA~u-vXqlw6NH=nkpyWE0u}LQjF-3NhATL@9rRxMnpO%f7 z)EhZf{PF|mKIMFxnC?*78(}{Y)}iztV12}_OXffJ;ta!fcFIVjdchyHxH=t%ci`Xd zX2AUB?%?poD6Zv*&BA!6c5S#|xn~DK01#XvjT!w!;&`lDXSJT4_j$}!qSPrb37vc{ z9^NfC%QvPu@vlxaZ;mIbn-VHA6miwi8qJ~V;pTZkKqqOii<1Cs}0i?uUIss;hM4dKq^1O35y?Yp=l4i zf{M!@QHH~rJ&X~8uATV><23zZUbs-J^3}$IvV_ANLS08>k`Td7aU_S1sLsfi*C-m1 z-e#S%UGs4E!;CeBT@9}aaI)qR-6NU@kvS#0r`g&UWg?fC7|b^_HyCE!8}nyh^~o@< zpm7PDFs9yxp+byMS(JWm$NeL?DNrMCNE!I^ko-*csB+dsf4GAq{=6sfyf4wb>?v1v zmb`F*bN1KUx-`ra1+TJ37bXNP%`-Fd`vVQFTwWpX@;s(%nDQa#oWhgk#mYlY*!d>( zE&!|ySF!mIyfING+#%RDY3IBH_fW$}6~1%!G`suHub1kP@&DoAd5~7J55;5_noPI6eLf{t;@9Kf<{aO0`1WNKd?<)C-|?C?)3s z>wEq@8=I$Wc~Mt$o;g++5qR+(6wt9GI~pyrDJ%c?gPZe)owvy^J2S=+M^ z&WhIE`g;;J^xQLVeCtf7b%Dg#Z2gq9hp_%g)-%_`y*zb; zn9`f`mUPN-Ts&fFo(aNTsXPA|J!TJ{0hZp0^;MYHLOcD=r_~~^ymS8KLCSeU3;^QzJNqS z5{5rEAv#l(X?bvwxpU;2%pQftF`YFgrD1jt2^~Mt^~G>T*}A$yZc@(k9orlCGv&|1 zWWvVgiJsCAtamuAYT~nzs?TQFt<1LSEx!@e0~@yd6$b5!Zm(FpBl;(Cn>2vF?k zOm#TTjFwd2D-CyA!mqR^?#Uwm{NBemP>(pHmM}9;;8`c&+_o3#E5m)JzfwN?(f-a4 zyd%xZc^oQx3XT?vcCqCX&Qrk~nu;fxs@JUoyVoi5fqpi&bUhQ2y!Ok2pzsFR(M(|U zw3E+kH_zmTRQ9dUMZWRE%Zakiwc+lgv7Z%|YO9YxAy`y28`Aw;WU6HXBgU7fl@dnt z-fFBV)}H-gqP!1;V@Je$WcbYre|dRdp{xt!7sL3Eoa%IA`5CAA%;Wq8PktwPdULo! z8!sB}Qt8#jH9Sh}QiUtEPZ6H0b*7qEKGJ%ITZ|vH)5Q^2m<7o3#Z>AKc%z7_u`rXA zqrCy{-{8;9>dfllLu$^M5L z-hXs))h*qz%~ActwkIA(qOVBZl2v4lwbM>9l70Y`+T*elINFqt#>OaVWoja8RMsep z6Or3f=oBnA3vDbn*+HNZP?8LsH2MY)x%c13@(XfuGR}R?Nu<|07{$+Lc3$Uv^I!MQ z>6qWgd-=aG2Y^24g4{Bw9ueOR)(9h`scImD=86dD+MnSN4$6 z^U*o_mE-6Rk~Dp!ANp#5RE9n*LG(Vg`1)g6!(XtDzsov$Dvz|Gv1WU68J$CkshQhS zCrc|cdkW~UK}5NeaWj^F4MSgFM+@fJd{|LLM)}_O<{rj z+?*Lm?owq?IzC%U%9EBga~h-cJbIu=#C}XuWN>OLrc%M@Gu~kFEYUi4EC6l#PR2JS zQUkGKrrS#6H7}2l0F@S11DP`@pih0WRkRJl#F;u{c&ZC{^$Z+_*lB)r)-bPgRFE;* zl)@hK4`tEP=P=il02x7-C7p%l=B`vkYjw?YhdJU9!P!jcmY$OtC^12w?vy3<<=tlY zUwHJ_0lgWN9vf>1%WACBD{UT)1qHQSE2%z|JHvP{#INr13jM}oYv_5#xsnv9`)UAO zuwgyV4YZ;O)eSc3(mka6=aRohi!HH@I#xq7kng?Acdg7S4vDJb6cI5fw?2z%3yR+| zU5v@Hm}vy;${cBp&@D=HQ9j7NcFaOYL zj-wV=eYF{|XTkFNM2uz&T8uH~;)^Zo!=KP)EVyH6s9l1~4m}N%XzPpduPg|h-&lL` zAXspR0YMOKd2yO)eMFFJ4?sQ&!`dF&!|niH*!^*Ml##o0M(0*uK9&yzekFi$+mP9s z>W9d%Jb)PtVi&-Ha!o~Iyh@KRuKpQ@)I~L*d`{O8!kRObjO7=n+Gp36fe!66neh+7 zW*l^0tTKjLLzr`x4`_8&on?mjW-PzheTNox8Hg7Nt@*SbE-%kP2hWYmHu#Fn@Q^J(SsPUz*|EgOoZ6byg3ew88UGdZ>9B2Tq=jF72ZaR=4u%1A6Vm{O#?@dD!(#tmR;eP(Fu z{$0O%=Vmua7=Gjr8nY%>ul?w=FJ76O2js&17W_iq2*tb!i{pt#`qZB#im9Rl>?t?0c zicIC}et_4d+CpVPx)i4~$u6N-QX3H77ez z?ZdvXifFk|*F8~L(W$OWM~r`pSk5}#F?j_5u$Obu9lDWIknO^AGu+Blk7!9Sb;NjS zncZA?qtASdNtzQ>z7N871IsPAk^CC?iIL}+{K|F@BuG2>qQ;_RUYV#>hHO(HUPpk@ z(bn~4|F_jiZi}Sad;_7`#4}EmD<1EiIxa48QjUuR?rC}^HRocq`OQPM@aHVKP9E#q zy%6bmHygCpIddPjE}q_DPC`VH_2m;Eey&ZH)E6xGeStOK7H)#+9y!%-Hm|QF6w#A( zIC0Yw%9j$s-#odxG~C*^MZ?M<+&WJ+@?B_QPUyTg9DJGtQN#NIC&-XddRsf3n^AL6 zT@P|H;PvN;ZpL0iv$bRb7|J{0o!Hq+S>_NrH4@coZtBJu#g8#CbR7|#?6uxi8d+$g z87apN>EciJZ`%Zv2**_uiET9Vk{pny&My;+WfGDw4EVL#B!Wiw&M|A8f1A@ z(yFQS6jfbH{b8Z-S7D2?Ixl`j0{+ZnpT=;KzVMLW{B$`N?Gw^Fl0H6lT61%T2AU**!sX0u?|I(yoy&Xveg7XBL&+>n6jd1##6d>TxE*Vj=8lWiG$4=u{1UbAa5QD>5_ z;Te^42v7K6Mmu4IWT6Rnm>oxrl~b<~^e3vbj-GCdHLIB_>59}Ya+~OF68NiH=?}2o zP(X7EN=quQn&)fK>M&kqF|<_*H`}c zk=+x)GU>{Af#vx&s?`UKUsz})g^Pc&?Ka@t5$n$bqf6{r1>#mWx6Ep>9|A}VmWRnowVo`OyCr^fHsf# zQjQ3Ttp7y#iQY8l`zEUW)(@gGQdt(~rkxlkefskT(t%@i8=|p1Y9Dc5bc+z#n$s13 zGJk|V0+&Ekh(F};PJzQKKo+FG@KV8a<$gmNSD;7rd_nRdc%?9)p!|B-@P~kxQG}~B zi|{0}@}zKC(rlFUYp*dO1RuvPC^DQOkX4<+EwvBAC{IZQdYxoq1Za!MW7%p7gGr=j zzWnAq%)^O2$eItftC#TTSArUyL$U54-O7e|)4_7%Q^2tZ^0-d&3J1}qCzR4dWX!)4 zzIEKjgnYgMus^>6uw4Jm8ga6>GBtMjpNRJ6CP~W=37~||gMo_p@GA@#-3)+cVYnU> zE5=Y4kzl+EbEh%dhQokB{gqNDqx%5*qBusWV%!iprn$S!;oN_6E3?0+umADVs4ako z?P+t?m?};gev9JXQ#Q&KBpzkHPde_CGu-y z<{}RRAx=xlv#mVi+Ibrgx~ujW$h{?zPfhz)Kp7kmYS&_|97b&H&1;J-mzrBWAvY} zh8-I8hl_RK2+nnf&}!W0P+>5?#?7>npshe<1~&l_xqKd0_>dl_^RMRq@-Myz&|TKZBj1=Q()) zF{dBjv5)h=&Z)Aevx}+i|7=R9rG^Di!sa)sZCl&ctX4&LScQ-kMncgO(9o6W6)yd< z@Rk!vkja*X_N3H=BavGoR0@u0<}m-7|2v!0+2h~S2Q&a=lTH91OJsvms2MT~ zY=c@LO5i`mLpBd(vh|)I&^A3TQLtr>w=zoyzTd=^f@TPu&+*2MtqE$Avf>l>}V|3-8Fp2hzo3y<)hr_|NO(&oSD z!vEjTWBxbKTiShVl-U{n*B3#)3a8$`{~Pk}J@elZ=>Pqp|MQ}jrGv7KrNcjW%TN_< zZz8kG{#}XoeWf7qY?D)L)8?Q-b@Na&>i=)(@uNo zr;cH98T3$Iau8Hn*@vXi{A@YehxDE2zX~o+RY`)6-X{8~hMpc#C`|8y> zU8Mnv5A0dNCf{Ims*|l-^ z(MRp{qoGohB34|ggDI*p!Aw|MFyJ|v+<+E3brfrI)|+l3W~CQLPbnF@G0)P~Ly!1TJLp}xh8uW`Q+RB-v`MRYZ9Gam3cM%{ zb4Cb*f)0deR~wtNb*8w-LlIF>kc7DAv>T0D(a3@l`k4TFnrO+g9XH7;nYOHxjc4lq zMmaW6qpgAgy)MckYMhl?>sq;-1E)-1llUneeA!ya9KM$)DaNGu57Z5aE>=VST$#vb zFo=uRHr$0M{-ha>h(D_boS4zId;3B|Tpqo|?B?Z@I?G(?&Iei+-{9L_A9=h=Qfn-U z1wIUnQe9!z%_j$F_{rf&`ZFSott09gY~qrf@g3O=Y>vzAnXCyL!@(BqWa)Zqt!#_k zfZHuwS52|&&)aK;CHq9V-t9qt0au{$#6c*R#e5n3rje0hic7c7m{kW$p(_`wB=Gw7 z4k`1Hi;Mc@yA7dp@r~?@rfw)TkjAW++|pkfOG}0N|2guek}j8Zen(!+@7?qt_7ndX zB=BG6WJ31#F3#Vk3=aQr8T)3`{=p9nBHlKzE0I@v`{vJ}h8pd6vby&VgFhzH|q;=aonunAXL6G2y(X^CtAhWr*jI zGjpY@raZDQkg*aMq}Ni6cRF z{oWv}5`nhSAv>usX}m^GHt`f(t8@zHc?K|y5Zi=4G*UG1Sza{$Dpj%X8 zzEXaKT5N6F5j4J|w#qlZP!zS7BT)9b+!ZSJdToqJts1c!)fwih4d31vfb{}W)EgcA zH2pZ^8_k$9+WD2n`6q5XbOy8>3pcYH9 z07eUB+p}YD@AH!}p!iKv><2QF-Y^&xx^PAc1F13A{nUeCDg&{hnix#FiO!fe(^&%Qcux!h znu*S!s$&nnkeotYsDthh1dq(iQrE|#f_=xVgfiiL&-5eAcC-> z5L0l|DVEM$#ulf{bj+Y~7iD)j<~O8CYM8GW)dQGq)!mck)FqoL^X zwNdZb3->hFrbHFm?hLvut-*uK?zXn3q1z|UX{RZ;-WiLoOjnle!xs+W0-8D)kjU#R z+S|A^HkRg$Ij%N4v~k`jyHffKaC~=wg=9)V5h=|kLQ@;^W!o2^K+xG&2n`XCd>OY5Ydi= zgHH=lgy++erK8&+YeTl7VNyVm9-GfONlSlVb3)V9NW5tT!cJ8d7X)!b-$fb!s76{t z@d=Vg-5K_sqHA@Zx-L_}wVnc@L@GL9_K~Zl(h5@AR#FAiKad8~KeWCo@mgXIQ#~u{ zgYFwNz}2b6Vu@CP0XoqJ+dm8px(5W5-Jpis97F`+KM)TuP*X8H@zwiVKDKGVp59pI zifNHZr|B+PG|7|Y<*tqap0CvG7tbR1R>jn70t1X`XJixiMVcHf%Ez*=xm1(CrTSDt z0cle!+{8*Ja&EOZ4@$qhBuKQ$U95Q%rc7tg$VRhk?3=pE&n+T3upZg^ZJc9~c2es% zh7>+|mrmA-p&v}|OtxqmHIBgUxL~^0+cpfkSK2mhh+4b=^F1Xgd2)}U*Yp+H?ls#z zrLxWg_hm}AfK2XYWr!rzW4g;+^^&bW%LmbtRai9f3PjU${r@n`JThy-cphbcwn)rq9{A$Ht`lmYKxOacy z6v2R(?gHhD5@&kB-Eg?4!hAoD7~(h>(R!s1c1Hx#s9vGPePUR|of32bS`J5U5w{F) z>0<^ktO2UHg<0{oxkdOQ;}coZDQph8p6ruj*_?uqURCMTac;>T#v+l1Tc~%^k-Vd@ zkc5y35jVNc49vZpZx;gG$h{%yslDI%Lqga1&&;mN{Ush1c7p>7e-(zp}6E7f-XmJb4nhk zb8zS+{IVbL$QVF8pf8}~kQ|dHJAEATmmnrb_wLG}-yHe>W|A&Y|;muy-d^t^<&)g5SJfaTH@P1%euONny=mxo+C z4N&w#biWY41r8k~468tvuYVh&XN&d#%QtIf9;iVXfWY)#j=l`&B~lqDT@28+Y!0E+MkfC}}H*#(WKKdJJq=O$vNYCb(ZG@p{fJgu;h z21oHQ(14?LeT>n5)s;uD@5&ohU!@wX8w*lB6i@GEH0pM>YTG+RAIWZD;4#F1&F%Jp zXZUml2sH0!lYJT?&sA!qwez6cXzJEd(1ZC~kT5kZSp7(@=H2$Azb_*W&6aA|9iwCL zdX7Q=42;@dspHDwYE?miGX#L^3xD&%BI&fN9^;`v4OjQXPBaBmOF1;#C)8XA(WFlH zycro;DS2?(G&6wkr6rqC>rqDv3nfGw3hmN_9Al>TgvmGsL8_hXx09};l9Ow@)F5@y z#VH5WigLDwZE4nh^7&@g{1FV^UZ%_LJ-s<{HN*2R$OPg@R~Z`c-ET*2}XB@9xvAjrK&hS=f|R8Gr9 zr|0TGOsI7RD+4+2{ZiwdVD@2zmg~g@^D--YL;6UYGSM8i$NbQr4!c7T9rg!8;TM0E zT#@?&S=t>GQm)*ua|?TLT2ktj#`|R<_*FAkOu2Pz$wEc%-=Y9V*$&dg+wIei3b*O8 z2|m$!jJG!J!ZGbbIa!(Af~oSyZV+~M1qGvelMzPNE_%5?c2>;MeeG2^N?JDKjFYCy z7SbPWH-$cWF9~fX%9~v99L!G(wi!PFp>rB!9xj7=Cv|F+7CsGNwY0Q_J%FID%C^CBZQfJ9K(HK%k31j~e#&?hQ zNuD6gRkVckU)v+53-fc} z7ZCzYN-5RG4H7;>>Hg?LU9&5_aua?A0)0dpew1#MMlu)LHe(M;OHjHIUl7|%%)YPo z0cBk;AOY00%Fe6heoN*$(b<)Cd#^8Iu;-2v@>cE-OB$icUF9EEoaC&q8z9}jMTT2I z8`9;jT%z0;dy4!8U;GW{i`)3!c6&oWY`J3669C!tM<5nQFFrFRglU8f)5Op$GtR-3 zn!+SPCw|04sv?%YZ(a7#L?vsdr7ss@WKAw&A*}-1S|9~cL%uA+E~>N6QklFE>8W|% zyX-qAUGTY1hQ-+um`2|&ji0cY*(qN!zp{YpDO-r>jPk*yuVSay<)cUt`t@&FPF_&$ zcHwu1(SQ`I-l8~vYyUxm@D1UEdFJ$f5Sw^HPH7b!9 zzYT3gKMF((N(v0#4f_jPfVZ=ApN^jQJe-X$`A?X+vWjLn_%31KXE*}5_}d8 zw_B1+a#6T1?>M{ronLbHIlEsMf93muJ7AH5h%;i99<~JX^;EAgEB1uHralD*!aJ@F zV2ruuFe9i2Q1C?^^kmVy921eb=tLDD43@-AgL^rQ3IO9%+vi_&R2^dpr}x{bCVPej z7G0-0o64uyWNtr*loIvslyo0%)KSDDKjfThe0hcqs)(C-MH1>bNGBDRTW~scy_{w} zp^aq8Qb!h9Lwielq%C1b8=?Z=&U)ST&PHbS)8Xzjh2DF?d{iAv)Eh)wsUnf>UtXN( zL7=$%YrZ#|^c{MYmhn!zV#t*(jdmYdCpwqpZ{v&L8KIuKn`@IIZfp!uo}c;7J57N` zAxyZ-uA4=Gzl~Ovycz%MW9ZL7N+nRo&1cfNn9(1H5eM;V_4Z_qVann7F>5f>%{rf= zPBZFaV@_Sobl?Fy&KXyzFDV*FIdhS5`Uc~S^Gjo)aiTHgn#<0C=9o-a-}@}xDor;D zZyZ|fvf;+=3MZd>SR1F^F`RJEZo+|MdyJYQAEauKu%WDol~ayrGU3zzbHKsnHKZ*z zFiwUkL@DZ>!*x05ql&EBq@_Vqv83&?@~q5?lVmffQZ+V-=qL+!u4Xs2Z2zdCQ3U7B&QR9_Iggy} z(om{Y9eU;IPe`+p1ifLx-XWh?wI)xU9ik+m#g&pGdB5Bi<`PR*?92lE0+TkRuXI)z z5LP!N2+tTc%cB6B1F-!fj#}>S!vnpgVU~3!*U1ej^)vjUH4s-bd^%B=ItQqDCGbrEzNQi(dJ`J}-U=2{7-d zK8k^Rlq2N#0G?9&1?HSle2vlkj^KWSBYTwx`2?9TU_DX#J+f+qLiZCqY1TXHFxXZqYMuD@RU$TgcnCC{_(vwZ-*uX)~go#%PK z@}2Km_5aQ~(<3cXeJN6|F8X_1@L%@xTzs}$_*E|a^_URF_qcF;Pfhoe?FTFwvjm1o z8onf@OY@jC2tVcMaZS;|T!Ks(wOgPpRzRnFS-^RZ4E!9dsnj9sFt609a|jJbb1Dt@ z<=Gal2jDEupxUSwWu6zp<<&RnAA;d&4gKVG0iu6g(DsST(4)z6R)zDpfaQ}v{5ARt zyhwvMtF%b-YazR5XLz+oh=mn;y-Mf2a8>7?2v8qX;19y?b>Z5laGHvzH;Nu9S`B8} zI)qN$GbXIQ1VL3lnof^6TS~rvPVg4V?Dl2Bb*K2z4E{5vy<(@@K_cN@U>R!>aUIRnb zL*)=787*cs#zb31zBC49x$`=fkQbMAef)L2$dR{)6BAz!t5U_B#1zZG`^neKSS22oJ#5B=gl%U=WeqL9REF2g zZnfCb0?quf?Ztj$VXvDSWoK`0L=Zxem2q}!XWLoT-kYMOx)!7fcgT35uC~0pySEme z`{wGWTkGr7>+Kb^n;W?BZH6ZP(9tQX%-7zF>vc2}LuWDI(9kh1G#7B99r4x6;_-V+k&c{nPUrR zAXJGRiMe~aup{0qzmLNjS_BC4cB#sXjckx{%_c&^xy{M61xEb>KW_AG5VFXUOjAG4 z^>Qlm9A#1N{4snY=(AmWzatb!ngqiqPbBZ7>Uhb3)dTkSGcL#&SH>iMO-IJBPua`u zo)LWZ>=NZLr758j{%(|uQuZ)pXq_4c!!>s|aDM9#`~1bzK3J1^^D#<2bNCccH7~-X}Ggi!pIIF>uFx%aPARGQsnC8ZQc8lrQ5o~smqOg>Ti^GNme94*w z)JZy{_{#$jxGQ&`M z!OMvZMHR>8*^>eS%o*6hJwn!l8VOOjZQJvh)@tnHVW&*GYPuxqXw}%M!(f-SQf`=L z5;=5w2;%82VMH6Xi&-K3W)o&K^+vJCepWZ-rW%+Dc6X3(){z$@4zjYxQ|}8UIojeC zYZpQ1dU{fy=oTr<4VX?$q)LP}IUmpiez^O&N3E_qPpchGTi5ZM6-2ScWlQq%V&R2Euz zO|Q0Hx>lY1Q1cW5xHv5!0OGU~PVEqSuy#fD72d#O`N!C;o=m+YioGu-wH2k6!t<~K zSr`E=W9)!g==~x9VV~-8{4ZN9{~-A9zJpRe%NGg$+MDuI-dH|b@BD)~>pPCGUNNzY zMDg||0@XGQgw`YCt5C&A{_+J}mvV9Wg{6V%2n#YSRN{AP#PY?1FF1#|vO_%e+#`|2*~wGAJaeRX6=IzFNeWhz6gJc8+(03Ph4y6ELAm=AkN7TOgMUEw*N{= z_)EIDQx5q22oUR+_b*tazu9+pX|n1c*IB-}{DqIj z-?E|ks{o3AGRNb;+iKcHkZvYJvFsW&83RAPs1Oh@IWy%l#5x2oUP6ZCtv+b|q>jsf zZ_9XO;V!>n`UxH1LvH8)L4?8raIvasEhkpQoJ`%!5rBs!0Tu(s_D{`4opB;57)pkX z4$A^8CsD3U5*!|bHIEqsn~{q+Ddj$ME@Gq4JXtgVz&7l{Ok!@?EA{B3P~NAqb9)4? zkQo30A^EbHfQ@87G5&EQTd`frrwL)&Yw?%-W@uy^Gn23%j?Y!Iea2xw<-f;esq zf%w5WN@E1}zyXtYv}}`U^B>W`>XPmdLj%4{P298|SisrE;7HvXX;A}Ffi8B#3Lr;1 zHt6zVb`8{#+e$*k?w8|O{Uh|&AG}|DG1PFo1i?Y*cQm$ZwtGcVgMwtBUDa{~L1KT-{jET4w60>{KZ27vXrHJ;fW{6| z=|Y4!&UX020wU1>1iRgB@Q#m~1^Z^9CG1LqDhYBrnx%IEdIty z!46iOoKlKs)c}newDG)rWUikD%j`)p z_w9Ph&e40=(2eBy;T!}*1p1f1SAUDP9iWy^u^Ubdj21Kn{46;GR+hwLO=4D11@c~V zI8x&(D({K~Df2E)Nx_yQvYfh4;MbMJ@Z}=Dt3_>iim~QZ*hZIlEs0mEb z_54+&*?wMD`2#vsQRN3KvoT>hWofI_Vf(^C1ff-Ike@h@saEf7g}<9T`W;HAne-Nd z>RR+&SP35w)xKn8^U$7))PsM!jKwYZ*RzEcG-OlTrX3}9a{q%#Un5E5W{{hp>w~;` zGky+3(vJvQyGwBo`tCpmo0mo((?nM8vf9aXrrY1Ve}~TuVkB(zeds^jEfI}xGBCM2 zL1|#tycSaWCurP+0MiActG3LCas@_@tao@(R1ANlwB$4K53egNE_;!&(%@Qo$>h`^1S_!hN6 z)vZtG$8fN!|BXBJ=SI>e(LAU(y(i*PHvgQ2llulxS8>qsimv7yL}0q_E5WiAz7)(f zC(ahFvG8&HN9+6^jGyLHM~$)7auppeWh_^zKk&C_MQ~8;N??OlyH~azgz5fe^>~7F zl3HnPN3z-kN)I$4@`CLCMQx3sG~V8hPS^}XDXZrQA>}mQPw%7&!sd(Pp^P=tgp-s^ zjl}1-KRPNWXgV_K^HkP__SR`S-|OF0bR-N5>I%ODj&1JUeAQ3$9i;B~$S6}*^tK?= z**%aCiH7y?xdY?{LgVP}S0HOh%0%LI$wRx;$T|~Y8R)Vdwa}kGWv8?SJVm^>r6+%I z#lj1aR94{@MP;t-scEYQWc#xFA30^}?|BeX*W#9OL;Q9#WqaaM546j5j29((^_8Nu z4uq}ESLr~r*O7E7$D{!k9W>`!SLoyA53i9QwRB{!pHe8um|aDE`Cg0O*{jmor)^t)3`>V>SWN-2VJcFmj^1?~tT=JrP`fVh*t zXHarp=8HEcR#vFe+1a%XXuK+)oFs`GDD}#Z+TJ}Ri`FvKO@ek2ayn}yaOi%(8p%2$ zpEu)v0Jym@f}U|-;}CbR=9{#<^z28PzkkTNvyKvJDZe+^VS2bES3N@Jq!-*}{oQlz z@8bgC_KnDnT4}d#&Cpr!%Yb?E!brx0!eVOw~;lLwUoz#Np%d$o%9scc3&zPm`%G((Le|6o1 zM(VhOw)!f84zG^)tZ1?Egv)d8cdNi+T${=5kV+j;Wf%2{3g@FHp^Gf*qO0q!u$=m9 zCaY`4mRqJ;FTH5`a$affE5dJrk~k`HTP_7nGTY@B9o9vvnbytaID;^b=Tzp7Q#DmD zC(XEN)Ktn39z5|G!wsVNnHi) z%^q94!lL|hF`IijA^9NR0F$@h7k5R^ljOW(;Td9grRN0Mb)l_l7##{2nPQ@?;VjXv zaLZG}yuf$r$<79rVPpXg?6iiieX|r#&`p#Con2i%S8*8F}(E) zI5E6c3tG*<;m~6>!&H!GJ6zEuhH7mkAzovdhLy;)q z{H2*8I^Pb}xC4s^6Y}6bJvMu=8>g&I)7!N!5QG$xseeU#CC?ZM-TbjsHwHgDGrsD= z{%f;@Sod+Ch66Ko2WF~;Ty)v>&x^aovCbCbD7>qF*!?BXmOV3(s|nxsb*Lx_2lpB7 zokUnzrk;P=T-&kUHO}td+Zdj!3n&NR?K~cRU zAXU!DCp?51{J4w^`cV#ye}(`SQhGQkkMu}O3M*BWt4UsC^jCFUy;wTINYmhD$AT;4 z?Xd{HaJjP`raZ39qAm;%beDbrLpbRf(mkKbANan7XsL>_pE2oo^$TgdidjRP!5-`% zv0d!|iKN$c0(T|L0C~XD0aS8t{*&#LnhE;1Kb<9&=c2B+9JeLvJr*AyyRh%@jHej=AetOMSlz^=!kxX>>B{2B1uIrQyfd8KjJ+DBy!h)~*(!|&L4^Q_07SQ~E zcemVP`{9CwFvPFu7pyVGCLhH?LhEVb2{7U+Z_>o25#+3<|8%1T^5dh}*4(kfJGry} zm%r#hU+__Z;;*4fMrX=Bkc@7|v^*B;HAl0((IBPPii%X9+u3DDF6%bI&6?Eu$8&aWVqHIM7mK6?Uvq$1|(-T|)IV<>e?!(rY zqkmO1MRaLeTR=)io(0GVtQT@s6rN%C6;nS3@eu;P#ry4q;^O@1ZKCJyp_Jo)Ty^QW z+vweTx_DLm{P-XSBj~Sl<%_b^$=}odJ!S2wAcxenmzFGX1t&Qp8Vxz2VT`uQsQYtdn&_0xVivIcxZ_hnrRtwq4cZSj1c-SG9 z7vHBCA=fd0O1<4*=lu$6pn~_pVKyL@ztw1swbZi0B?spLo56ZKu5;7ZeUml1Ws1?u zqMf1p{5myAzeX$lAi{jIUqo1g4!zWLMm9cfWcnw`k6*BR^?$2(&yW?>w;G$EmTA@a z6?y#K$C~ZT8+v{87n5Dm&H6Pb_EQ@V0IWmG9cG=O;(;5aMWWrIPzz4Q`mhK;qQp~a z+BbQrEQ+w{SeiuG-~Po5f=^EvlouB@_|4xQXH@A~KgpFHrwu%dwuCR)=B&C(y6J4J zvoGk9;lLs9%iA-IJGU#RgnZZR+@{5lYl8(e1h6&>Vc_mvg0d@);X zji4T|n#lB!>pfL|8tQYkw?U2bD`W{na&;*|znjmalA&f;*U++_aBYerq;&C8Kw7mI z7tsG*?7*5j&dU)Lje;^{D_h`%(dK|pB*A*1(Jj)w^mZ9HB|vGLkF1GEFhu&rH=r=8 zMxO42e{Si6$m+Zj`_mXb&w5Q(i|Yxyg?juUrY}78uo@~3v84|8dfgbPd0iQJRdMj< zncCNGdMEcsxu#o#B5+XD{tsg*;j-eF8`mp~K8O1J!Z0+>0=7O=4M}E?)H)ENE;P*F z$Ox?ril_^p0g7xhDUf(q652l|562VFlC8^r8?lQv;TMvn+*8I}&+hIQYh2 z1}uQQaag&!-+DZ@|C+C$bN6W;S-Z@)d1|en+XGvjbOxCa-qAF*LA=6s(Jg+g;82f$ z(Vb)8I)AH@cdjGFAR5Rqd0wiNCu!xtqWbcTx&5kslzTb^7A78~Xzw1($UV6S^VWiP zFd{Rimd-0CZC_Bu(WxBFW7+k{cOW7DxBBkJdJ;VsJ4Z@lERQr%3eVv&$%)b%<~ zCl^Y4NgO}js@u{|o~KTgH}>!* z_iDNqX2(As7T0xivMH|3SC1ivm8Q}6Ffcd7owUKN5lHAtzMM4<0v+ykUT!QiowO;`@%JGv+K$bBx@*S7C8GJVqQ_K>12}M`f_Ys=S zKFh}HM9#6Izb$Y{wYzItTy+l5U2oL%boCJn?R3?jP@n$zSIwlmyGq30Cw4QBO|14` zW5c);AN*J3&eMFAk$SR~2k|&+&Bc$e>s%c{`?d~85S-UWjA>DS5+;UKZ}5oVa5O(N zqqc@>)nee)+4MUjH?FGv%hm2{IlIF-QX}ym-7ok4Z9{V+ZHVZQl$A*x!(q%<2~iVv znUa+BX35&lCb#9VE-~Y^W_f;Xhl%vgjwdjzMy$FsSIj&ok}L+X`4>J=9BkN&nu^E*gbhj3(+D>C4E z@Fwq_=N)^bKFSHTzZk?-gNU$@l}r}dwGyh_fNi=9b|n}J>&;G!lzilbWF4B}BBq4f zYIOl?b)PSh#XTPp4IS5ZR_2C!E)Z`zH0OW%4;&~z7UAyA-X|sh9@~>cQW^COA9hV4 zXcA6qUo9P{bW1_2`eo6%hgbN%(G-F1xTvq!sc?4wN6Q4`e9Hku zFwvlAcRY?6h^Fj$R8zCNEDq8`=uZB8D-xn)tA<^bFFy}4$vA}Xq0jAsv1&5!h!yRA zU()KLJya5MQ`q&LKdH#fwq&(bNFS{sKlEh_{N%{XCGO+po#(+WCLmKW6&5iOHny>g z3*VFN?mx!16V5{zyuMWDVP8U*|BGT$(%IO|)?EF|OI*sq&RovH!N%=>i_c?K*A>>k zyg1+~++zY4Q)J;VWN0axhoIKx;l&G$gvj(#go^pZskEVj8^}is3Jw26LzYYVos0HX zRPvmK$dVxM8(Tc?pHFe0Z3uq){{#OK3i-ra#@+;*=ui8)y6hsRv z4Fxx1c1+fr!VI{L3DFMwXKrfl#Q8hfP@ajgEau&QMCxd{g#!T^;ATXW)nUg&$-n25 zruy3V!!;{?OTobo|0GAxe`Acn3GV@W=&n;~&9 zQM>NWW~R@OYORkJAo+eq1!4vzmf9K%plR4(tB@TR&FSbDoRgJ8qVcH#;7lQub*nq&?Z>7WM=oeEVjkaG zT#f)=o!M2DO5hLR+op>t0CixJCIeXH*+z{-XS|%jx)y(j&}Wo|3!l7{o)HU3m7LYyhv*xF&tq z%IN7N;D4raue&&hm0xM=`qv`+TK@;_xAcGKuK(2|75~ar2Yw)geNLSmVxV@x89bQu zpViVKKnlkwjS&&c|-X6`~xdnh}Ps)Hs z4VbUL^{XNLf7_|Oi>tA%?SG5zax}esF*FH3d(JH^Gvr7Rp*n=t7frH!U;!y1gJB^i zY_M$KL_}mW&XKaDEi9K-wZR|q*L32&m+2n_8lq$xRznJ7p8}V>w+d@?uB!eS3#u<} zIaqi!b!w}a2;_BfUUhGMy#4dPx>)_>yZ`ai?Rk`}d0>~ce-PfY-b?Csd(28yX22L% zI7XI>OjIHYTk_@Xk;Gu^F52^Gn6E1&+?4MxDS2G_#PQ&yXPXP^<-p|2nLTb@AAQEY zI*UQ9Pmm{Kat}wuazpjSyXCdnrD&|C1c5DIb1TnzF}f4KIV6D)CJ!?&l&{T)e4U%3HTSYqsQ zo@zWB1o}ceQSV)<4G<)jM|@@YpL+XHuWsr5AYh^Q{K=wSV99D~4RRU52FufmMBMmd z_H}L#qe(}|I9ZyPRD6kT>Ivj&2Y?qVZq<4bG_co_DP`sE*_Xw8D;+7QR$Uq(rr+u> z8bHUWbV19i#)@@G4bCco@Xb<8u~wVDz9S`#k@ciJtlu@uP1U0X?yov8v9U3VOig2t zL9?n$P3=1U_Emi$#slR>N5wH-=J&T=EdUHA}_Z zZIl3nvMP*AZS9{cDqFanrA~S5BqxtNm9tlu;^`)3X&V4tMAkJ4gEIPl= zoV!Gyx0N{3DpD@)pv^iS*dl2FwANu;1;%EDl}JQ7MbxLMAp>)UwNwe{=V}O-5C*>F zu?Ny+F64jZn<+fKjF01}8h5H_3pey|;%bI;SFg$w8;IC<8l|3#Lz2;mNNik6sVTG3 z+Su^rIE#40C4a-587$U~%KedEEw1%r6wdvoMwpmlXH$xPnNQN#f%Z7|p)nC>WsuO= z4zyqapLS<8(UJ~Qi9d|dQijb_xhA2)v>la)<1md5s^R1N&PiuA$^k|A<+2C?OiHbj z>Bn$~t)>Y(Zb`8hW7q9xQ=s>Rv81V+UiuZJc<23HplI88isqRCId89fb`Kt|CxVIg znWcwprwXnotO>3s&Oypkte^9yJjlUVVxSe%_xlzmje|mYOVPH^vjA=?6xd0vaj0Oz zwJ4OJNiFdnHJX3rw&inskjryukl`*fRQ#SMod5J|KroJRsVXa5_$q7whSQ{gOi*s0 z1LeCy|JBWRsDPn7jCb4s(p|JZiZ8+*ExC@Vj)MF|*Vp{B(ziccSn`G1Br9bV(v!C2 z6#?eqpJBc9o@lJ#^p-`-=`4i&wFe>2)nlPK1p9yPFzJCzBQbpkcR>={YtamIw)3nt z(QEF;+)4`>8^_LU)_Q3 zC5_7lgi_6y>U%m)m@}Ku4C}=l^J=<<7c;99ec3p{aR+v=diuJR7uZi%aQv$oP?dn?@6Yu_+*^>T0ptf(oobdL;6)N-I!TO`zg^Xbv3#L0I~sn@WGk-^SmPh5>W+LB<+1PU}AKa?FCWF|qMNELOgdxR{ zbqE7@jVe+FklzdcD$!(A$&}}H*HQFTJ+AOrJYnhh}Yvta(B zQ_bW4Rr;R~&6PAKwgLWXS{Bnln(vUI+~g#kl{r+_zbngT`Y3`^Qf=!PxN4IYX#iW4 zucW7@LLJA9Zh3(rj~&SyN_pjO8H&)|(v%!BnMWySBJV=eSkB3YSTCyIeJ{i;(oc%_hk{$_l;v>nWSB)oVeg+blh=HB5JSlG_r7@P z3q;aFoZjD_qS@zygYqCn=;Zxjo!?NK!%J$ z52lOP`8G3feEj+HTp@Tnn9X~nG=;tS+z}u{mQX_J0kxtr)O30YD%oo)L@wy`jpQYM z@M>Me=95k1p*FW~rHiV1CIfVc{K8r|#Kt(ApkXKsDG$_>76UGNhHExFCw#Ky9*B-z zNq2ga*xax!HMf_|Vp-86r{;~YgQKqu7%szk8$hpvi_2I`OVbG1doP(`gn}=W<8%Gn z%81#&WjkH4GV;4u43EtSW>K_Ta3Zj!XF?;SO3V#q=<=>Tc^@?A`i;&`-cYj|;^ zEo#Jl5zSr~_V-4}y8pnufXLa80vZY4z2ko7fj>DR)#z=wWuS1$$W!L?(y}YC+yQ|G z@L&`2upy3f>~*IquAjkVNU>}c10(fq#HdbK$~Q3l6|=@-eBbo>B9(6xV`*)sae58*f zym~RRVx;xoCG3`JV`xo z!lFw)=t2Hy)e!IFs?0~7osWk(d%^wxq&>_XD4+U#y&-VF%4z?XH^i4w`TxpF{`XhZ z%G}iEzf!T(l>g;W9<~K+)$g!{UvhW{E0Lis(S^%I8OF&%kr!gJ&fMOpM=&=Aj@wuL zBX?*6i51Qb$uhkwkFYkaD_UDE+)rh1c;(&Y=B$3)J&iJfQSx!1NGgPtK!$c9OtJuu zX(pV$bfuJpRR|K(dp@^j}i&HeJOh@|7lWo8^$*o~Xqo z5Sb+!EtJ&e@6F+h&+_1ETbg7LfP5GZjvIUIN3ibCOldAv z)>YdO|NH$x7AC8dr=<2ekiY1%fN*r~e5h6Yaw<{XIErujKV~tiyrvV_DV0AzEknC- zR^xKM3i<1UkvqBj3C{wDvytOd+YtDSGu!gEMg+!&|8BQrT*|p)(dwQLEy+ zMtMzij3zo40)CA!BKZF~yWg?#lWhqD3@qR)gh~D{uZaJO;{OWV8XZ_)J@r3=)T|kt zUS1pXr6-`!Z}w2QR7nP%d?ecf90;K_7C3d!UZ`N(TZoWNN^Q~RjVhQG{Y<%E1PpV^4 z-m-K+$A~-+VDABs^Q@U*)YvhY4Znn2^w>732H?NRK(5QSS$V@D7yz2BVX4)f5A04~$WbxGOam22>t&uD)JB8-~yiQW6ik;FGblY_I>SvB_z2?PS z*Qm&qbKI{H1V@YGWzpx`!v)WeLT02};JJo*#f$a*FH?IIad-^(;9XC#YTWN6;Z6+S zm4O1KH=#V@FJw7Pha0!9Vb%ZIM$)a`VRMoiN&C|$YA3~ZC*8ayZRY^fyuP6$n%2IU z$#XceYZeqLTXw(m$_z|33I$B4k~NZO>pP6)H_}R{E$i%USGy{l{-jOE;%CloYPEU+ zRFxOn4;7lIOh!7abb23YKD+_-?O z0FP9otcAh+oSj;=f#$&*ExUHpd&e#bSF%#8*&ItcL2H$Sa)?pt0Xtf+t)z$_u^wZi z44oE}r4kIZGy3!Mc8q$B&6JqtnHZ>Znn!Zh@6rgIu|yU+zG8q`q9%B18|T|oN3zMq z`l&D;U!OL~%>vo&q0>Y==~zLiCZk4v%s_7!9DxQ~id1LLE93gf*gg&2$|hB#j8;?3 z5v4S;oM6rT{Y;I+#FdmNw z){d%tNM<<#GN%n9ox7B=3#;u7unZ~tLB_vRZ52a&2=IM)2VkXm=L+Iqq~uk#Dug|x z>S84e+A7EiOY5lj*!q?6HDkNh~0g;0Jy(al!ZHHDtur9T$y-~)94HelX1NHjXWIM7UAe}$?jiz z9?P4`I0JM=G5K{3_%2jPLC^_Mlw?-kYYgb7`qGa3@dn|^1fRMwiyM@Ch z;CB&o7&&?c5e>h`IM;Wnha0QKnEp=$hA8TJgR-07N~U5(>9vJzeoFsSRBkDq=x(YgEMpb=l4TDD`2 zwVJpWGTA_u7}?ecW7s6%rUs&NXD3+n;jB86`X?8(l3MBo6)PdakI6V6a}22{)8ilT zM~T*mU}__xSy|6XSrJ^%lDAR3Lft%+yxC|ZUvSO_nqMX!_ul3;R#*{~4DA=h$bP)%8Yv9X zyp><|e8=_ttI}ZAwOd#dlnSjck#6%273{E$kJuCGu=I@O)&6ID{nWF5@gLb16sj|&Sb~+du4e4O_%_o`Ix4NRrAsyr1_}MuP94s>de8cH-OUkVPk3+K z&jW)It9QiU-ti~AuJkL`XMca8Oh4$SyJ=`-5WU<{cIh+XVH#e4d&zive_UHC!pN>W z3TB;Mn5i)9Qn)#6@lo4QpI3jFYc0~+jS)4AFz8fVC;lD^+idw^S~Qhq>Tg(!3$yLD zzktzoFrU@6s4wwCMz}edpF5i5Q1IMmEJQHzp(LAt)pgN3&O!&d?3W@6U4)I^2V{;- z6A(?zd93hS*uQmnh4T)nHnE{wVhh(=MMD(h(P4+^p83Om6t<*cUW>l(qJzr%5vp@K zN27ka(L{JX=1~e2^)F^i=TYj&;<7jyUUR2Bek^A8+3Up*&Xwc{)1nRR5CT8vG>ExV zHnF3UqXJOAno_?bnhCX-&kwI~Ti8t4`n0%Up>!U`ZvK^w2+0Cs-b9%w%4`$+To|k= zKtgc&l}P`*8IS>8DOe?EB84^kx4BQp3<7P{Pq}&p%xF_81pg!l2|u=&I{AuUgmF5n zJQCTLv}%}xbFGYtKfbba{CBo)lWW%Z>i(_NvLhoQZ*5-@2l&x>e+I~0Nld3UI9tdL zRzu8}i;X!h8LHVvN?C+|M81e>Jr38%&*9LYQec9Ax>?NN+9(_>XSRv&6hlCYB`>Qm z1&ygi{Y()OU4@D_jd_-7vDILR{>o|7-k)Sjdxkjgvi{@S>6GqiF|o`*Otr;P)kLHN zZkpts;0zw_6;?f(@4S1FN=m!4^mv~W+lJA`&7RH%2$)49z0A+8@0BCHtj|yH--AEL z0tW6G%X-+J+5a{5*WKaM0QDznf;V?L5&uQw+yegDNDP`hA;0XPYc6e0;Xv6|i|^F2WB)Z$LR|HR4 zTQsRAby9(^Z@yATyOgcfQw7cKyr^3Tz7lc7+JEwwzA7)|2x+PtEb>nD(tpxJQm)Kn zW9K_*r!L%~N*vS8<5T=iv|o!zTe9k_2jC_j*7ik^M_ zaf%k{WX{-;0*`t`G!&`eW;gChVXnJ-Rn)To8vW-?>>a%QU1v`ZC=U)f8iA@%JG0mZ zDqH;~mgBnrCP~1II<=V9;EBL)J+xzCoiRBaeH&J6rL!{4zIY8tZka?_FBeQeNO3q6 zyG_alW54Ba&wQf{&F1v-r1R6ID)PTsqjIBc+5MHkcW5Fnvi~{-FjKe)t1bl}Y;z@< z=!%zvpRua>>t_x}^}z0<7MI!H2v6|XAyR9!t50q-A)xk0nflgF4*OQlCGK==4S|wc zRMsSscNhRzHMBU8TdcHN!q^I}x0iXJ%uehac|Zs_B$p@CnF)HeXPpB_Za}F{<@6-4 zl%kml@}kHQ(ypD8FsPJ2=14xXJE|b20RUIgs!2|R3>LUMGF6X*B_I|$`Qg=;zm7C z{mEDy9dTmPbued7mlO@phdmAmJ7p@GR1bjCkMw6*G7#4+`k>fk1czdJUB!e@Q(~6# zwo%@p@V5RL0ABU2LH7Asq^quDUho@H>eTZH9f*no9fY0T zD_-9px3e}A!>>kv5wk91%C9R1J_Nh!*&Kk$J3KNxC}c_@zlgpJZ+5L)Nw|^p=2ue}CJtm;uj*Iqr)K})kA$xtNUEvX;4!Px*^&9T_`IN{D z{6~QY=Nau6EzpvufB^hflc#XIsSq0Y9(nf$d~6ZwK}fal92)fr%T3=q{0mP-EyP_G z)UR5h@IX}3Qll2b0oCAcBF>b*@Etu*aTLPU<%C>KoOrk=x?pN!#f_Og-w+;xbFgjQ zXp`et%lDBBh~OcFnMKMUoox0YwBNy`N0q~bSPh@+enQ=4RUw1) zpovN`QoV>vZ#5LvC;cl|6jPr}O5tu!Ipoyib8iXqy}TeJ;4+_7r<1kV0v5?Kv>fYp zg>9L`;XwXa&W7-jf|9~uP2iyF5`5AJ`Q~p4eBU$MCC00`rcSF>`&0fbd^_eqR+}mK z4n*PMMa&FOcc)vTUR zlDUAn-mh`ahi_`f`=39JYTNVjsTa_Y3b1GOIi)6dY)D}xeshB0T8Eov5%UhWd1)u}kjEQ|LDo{tqKKrYIfVz~@dp!! zMOnah@vp)%_-jDTUG09l+;{CkDCH|Q{NqX*uHa1YxFShy*1+;J`gywKaz|2Q{lG8x zP?KBur`}r`!WLKXY_K;C8$EWG>jY3UIh{+BLv0=2)KH%P}6xE2kg)%(-uA6lC?u8}{K(#P*c zE9C8t*u%j2r_{;Rpe1A{9nNXU;b_N0vNgyK!EZVut~}+R2rcbsHilqsOviYh-pYX= zHw@53nlmwYI5W5KP>&`dBZe0Jn?nAdC^HY1wlR6$u^PbpB#AS&5L6zqrXN&7*N2Q` z+Rae1EwS)H=aVSIkr8Ek^1jy2iS2o7mqm~Mr&g5=jjt7VxwglQ^`h#Mx+x2v|9ZAwE$i_9918MjJxTMr?n!bZ6n$}y11u8I9COTU`Z$Fi z!AeAQLMw^gp_{+0QTEJrhL424pVDp%wpku~XRlD3iv{vQ!lAf!_jyqd_h}+Tr1XG| z`*FT*NbPqvHCUsYAkFnM`@l4u_QH&bszpUK#M~XLJt{%?00GXY?u_{gj3Hvs!=N(I z(=AuWPijyoU!r?aFTsa8pLB&cx}$*%;K$e*XqF{~*rA-qn)h^!(-;e}O#B$|S~c+U zN4vyOK0vmtx$5K!?g*+J@G1NmlEI=pyZXZ69tAv=@`t%ag_Hk{LP~OH9iE)I= zaJ69b4kuCkV0V zo(M0#>phpQ_)@j;h%m{-a*LGi(72TP)ws2w*@4|C-3+;=5DmC4s7Lp95%n%@Ko zfdr3-a7m*dys9iIci$A=4NPJ`HfJ;hujLgU)ZRuJI`n;Pw|yksu!#LQnJ#dJysgNb z@@qwR^wrk(jbq4H?d!lNyy72~Dnn87KxsgQ!)|*m(DRM+eC$wh7KnS-mho3|KE)7h zK3k;qZ;K1Lj6uEXLYUYi)1FN}F@-xJ z@@3Hb84sl|j{4$3J}aTY@cbX@pzB_qM~APljrjju6P0tY{C@ zpUCOz_NFmALMv1*blCcwUD3?U6tYs+N%cmJ98D%3)%)Xu^uvzF zS5O!sc#X6?EwsYkvPo6A%O8&y8sCCQH<%f2togVwW&{M;PR!a(ZT_A+jVAbf{@5kL zB@Z(hb$3U{T_}SKA_CoQVU-;j>2J=L#lZ~aQCFg-d<9rzs$_gO&d5N6eFSc z1ml8)P*FSi+k@!^M9nDWR5e@ATD8oxtDu=36Iv2!;dZzidIS(PCtEuXAtlBb1;H%Z zwnC^Ek*D)EX4#Q>R$$WA2sxC_t(!!6Tr?C#@{3}n{<^o;9id1RA&-Pig1e-2B1XpG zliNjgmd3c&%A}s>qf{_j#!Z`fu0xIwm4L0)OF=u(OEmp;bLCIaZX$&J_^Z%4Sq4GZ zPn6sV_#+6pJmDN_lx@1;Zw6Md_p0w9h6mHtzpuIEwNn>OnuRSC2=>fP^Hqgc)xu^4 z<3!s`cORHJh#?!nKI`Et7{3C27+EuH)Gw1f)aoP|B3y?fuVfvpYYmmukx0ya-)TQX zR{ggy5cNf4X|g)nl#jC9p>7|09_S7>1D2GTRBUTW zAkQ=JMRogZqG#v;^=11O6@rPPwvJkr{bW-Qg8`q8GoD#K`&Y+S#%&B>SGRL>;ZunM@49!}Uy zN|bBCJ%sO;@3wl0>0gbl3L@1^O60ONObz8ZI7nder>(udj-jt`;yj^nTQ$L9`OU9W zX4alF#$|GiR47%x@s&LV>2Sz2R6?;2R~5k6V>)nz!o_*1Y!$p>BC5&?hJg_MiE6UBy>RkVZj`9UWbRkN-Hk!S`=BS3t3uyX6)7SF#)71*}`~Ogz z1rap5H6~dhBJ83;q-Y<5V35C2&F^JI-it(=5D#v!fAi9p#UwV~2tZQI+W(Dv?1t9? zfh*xpxxO{-(VGB>!Q&0%^YW_F!@aZS#ucP|YaD#>wd1Fv&Z*SR&mc;asi}1G) z_H>`!akh-Zxq9#io(7%;a$)w+{QH)Y$?UK1Dt^4)up!Szcxnu}kn$0afcfJL#IL+S z5gF_Y30j;{lNrG6m~$Ay?)*V9fZuU@3=kd40=LhazjFrau>(Y>SJNtOz>8x_X-BlA zIpl{i>OarVGj1v(4?^1`R}aQB&WCRQzS~;7R{tDZG=HhgrW@B`W|#cdyj%YBky)P= zpxuOZkW>S6%q7U{VsB#G(^FMsH5QuGXhb(sY+!-R8Bmv6Sx3WzSW<1MPPN1!&PurYky(@`bP9tz z52}LH9Q?+FF5jR6-;|+GVdRA!qtd;}*-h&iIw3Tq3qF9sDIb1FFxGbo&fbG5n8$3F zyY&PWL{ys^dTO}oZ#@sIX^BKW*bon=;te9j5k+T%wJ zNJtoN1~YVj4~YRrlZl)b&kJqp+Z`DqT!la$x&&IxgOQw#yZd-nBP3!7FijBXD|IsU8Zl^ zc6?MKpJQ+7ka|tZQLfchD$PD|;K(9FiLE|eUZX#EZxhG!S-63C$jWX1Yd!6-Yxi-u zjULIr|0-Q%D9jz}IF~S%>0(jOqZ(Ln<$9PxiySr&2Oic7vb<8q=46)Ln%Z|<*z5&> z3f~Zw@m;vR(bESB<=Jqkxn(=#hQw42l(7)h`vMQQTttz9XW6^|^8EK7qhju4r_c*b zJIi`)MB$w@9epwdIfnEBR+?~);yd6C(LeMC& zn&&N*?-g&BBJcV;8&UoZi4Lmxcj16ojlxR~zMrf=O_^i1wGb9X-0@6_rpjPYemIin zmJb+;lHe;Yp=8G)Q(L1bzH*}I>}uAqhj4;g)PlvD9_e_ScR{Ipq|$8NvAvLD8MYr}xl=bU~)f%B3E>r3Bu9_t|ThF3C5~BdOve zEbk^r&r#PT&?^V1cb{72yEWH}TXEE}w>t!cY~rA+hNOTK8FAtIEoszp!qqptS&;r$ zaYV-NX96-h$6aR@1xz6_E0^N49mU)-v#bwtGJm)ibygzJ8!7|WIrcb`$XH~^!a#s& z{Db-0IOTFq#9!^j!n_F}#Z_nX{YzBK8XLPVmc&X`fT7!@$U-@2KM9soGbmOSAmqV z{nr$L^MBo_u^Joyf0E^=eo{Rt0{{e$IFA(#*kP@SQd6lWT2-#>` zP1)7_@IO!9lk>Zt?#CU?cuhiLF&)+XEM9B)cS(gvQT!X3`wL*{fArTS;Ak`J<84du zALKPz4}3nlG8Fo^MH0L|oK2-4xIY!~Oux~1sw!+It)&D3p;+N8AgqKI`ld6v71wy8I!eP0o~=RVcFQR2Gr(eP_JbSytoQ$Yt}l*4r@A8Me94y z8cTDWhqlq^qoAhbOzGBXv^Wa4vUz$(7B!mX`T=x_ueKRRDfg&Uc-e1+z4x$jyW_Pm zp?U;-R#xt^Z8Ev~`m`iL4*c#65Nn)q#=Y0l1AuD&+{|8-Gsij3LUZXpM0Bx0u7WWm zH|%yE@-#XEph2}-$-thl+S;__ciBxSSzHveP%~v}5I%u!z_l_KoW{KRx2=eB33umE zIYFtu^5=wGU`Jab8#}cnYry@9p5UE#U|VVvx_4l49JQ;jQdp(uw=$^A$EA$LM%vmE zvdEOaIcp5qX8wX{mYf0;#51~imYYPn4=k&#DsKTxo{_Mg*;S495?OBY?#gv=edYC* z^O@-sd-qa+U24xvcbL0@C7_6o!$`)sVr-jSJE4XQUQ$?L7}2(}Eixqv;L8AdJAVqc zq}RPgpnDb@E_;?6K58r3h4-!4rT4Ab#rLHLX?eMOfluJk=3i1@Gt1i#iA=O`M0@x! z(HtJP9BMHXEzuD93m|B&woj0g6T?f#^)>J>|I4C5?Gam>n9!8CT%~aT;=oco5d6U8 zMXl(=W;$ND_8+DD*?|5bJ!;8ebESXMUKBAf7YBwNVJibGaJ*(2G`F%wx)grqVPjudiaq^Kl&g$8A2 zWMxMr@_$c}d+;_B`#kUX-t|4VKH&_f^^EP0&=DPLW)H)UzBG%%Tra*5 z%$kyZe3I&S#gfie^z5)!twG={3Cuh)FdeA!Kj<-9** zvT*5%Tb`|QbE!iW-XcOuy39>D3oe6x{>&<#E$o8Ac|j)wq#kQzz|ATd=Z0K!p2$QE zPu?jL8Lb^y3_CQE{*}sTDe!2!dtlFjq&YLY@2#4>XS`}v#PLrpvc4*@q^O{mmnr5D zmyJq~t?8>FWU5vZdE(%4cuZuao0GNjp3~Dt*SLaxI#g_u>hu@k&9Ho*#CZP~lFJHj z(e!SYlLigyc?&5-YxlE{uuk$9b&l6d`uIlpg_z15dPo*iU&|Khx2*A5Fp;8iK_bdP z?T6|^7@lcx2j0T@x>X7|kuuBSB7<^zeY~R~4McconTxA2flHC0_jFxmSTv-~?zVT| zG_|yDqa9lkF*B6_{j=T>=M8r<0s;@z#h)3BQ4NLl@`Xr__o7;~M&dL3J8fP&zLfDfy z);ckcTev{@OUlZ`bCo(-3? z1u1xD`PKgSg?RqeVVsF<1SLF;XYA@Bsa&cY!I48ZJn1V<3d!?s=St?TLo zC0cNr`qD*M#s6f~X>SCNVkva^9A2ZP>CoJ9bvgXe_c}WdX-)pHM5m7O zrHt#g$F0AO+nGA;7dSJ?)|Mo~cf{z2L)Rz!`fpi73Zv)H=a5K)*$5sf_IZypi($P5 zsPwUc4~P-J1@^3C6-r9{V-u0Z&Sl7vNfmuMY4yy*cL>_)BmQF!8Om9Dej%cHxbIzA zhtV0d{=%cr?;bpBPjt@4w=#<>k5ee=TiWAXM2~tUGfm z$s&!Dm0R^V$}fOR*B^kGaipi~rx~A2cS0;t&khV1a4u38*XRUP~f za!rZMtay8bsLt6yFYl@>-y^31(*P!L^^s@mslZy(SMsv9bVoX`O#yBgEcjCmGpyc* zeH$Dw6vB5P*;jor+JOX@;6K#+xc)Z9B8M=x2a@Wx-{snPGpRmOC$zpsqW*JCh@M2Y z#K+M(>=#d^>Of9C`))h<=Bsy)6zaMJ&x-t%&+UcpLjV`jo4R2025 zXaG8EA!0lQa)|dx-@{O)qP6`$rhCkoQqZ`^SW8g-kOwrwsK8 z3ms*AIcyj}-1x&A&vSq{r=QMyp3CHdWH35!sad#!Sm>^|-|afB+Q;|Iq@LFgqIp#Z zD1%H+3I?6RGnk&IFo|u+E0dCxXz4yI^1i!QTu7uvIEH>i3rR{srcST`LIRwdV1P;W z+%AN1NIf@xxvVLiSX`8ILA8MzNqE&7>%jMzGt9wm78bo9<;h*W84i29^w!>V>{N+S zd`5Zmz^G;f=icvoOZfK5#1ctx*~UwD=ab4DGQXehQ!XYnak*dee%YN$_ZPL%KZuz$ zD;$PpT;HM^$KwtQm@7uvT`i6>Hae1CoRVM2)NL<2-k2PiX=eAx+-6j#JI?M}(tuBW zkF%jjLR)O`gI2fcPBxF^HeI|DWwQWHVR!;;{BXXHskxh8F@BMDn`oEi-NHt;CLymW z=KSv5)3dyzec0T5B*`g-MQ<;gz=nIWKUi9ko<|4I(-E0k$QncH>E4l z**1w&#={&zv4Tvhgz#c29`m|;lU-jmaXFMC11 z*dlXDMEOG>VoLMc>!rApwOu2prKSi*!w%`yzGmS+k(zm*CsLK*wv{S_0WX^8A-rKy zbk^Gf_92^7iB_uUF)EE+ET4d|X|>d&mdN?x@vxKAQk`O+r4Qdu>XGy(a(19g;=jU} zFX{O*_NG>!$@jh!U369Lnc+D~qch3uT+_Amyi}*k#LAAwh}k8IPK5a-WZ81ufD>l> z$4cF}GSz>ce`3FAic}6W4Z7m9KGO?(eWqi@L|5Hq0@L|&2flN1PVl}XgQ2q*_n2s3 zt5KtowNkTYB5b;SVuoXA@i5irXO)A&%7?V`1@HGCB&)Wgk+l|^XXChq;u(nyPB}b3 zY>m5jkxpZgi)zfbgv&ec4Zqdvm+D<?Im*mXweS9H+V>)zF#Zp3)bhl$PbISY{5=_z!8&*Jv~NYtI-g!>fDs zmvL5O^U%!^VaKA9gvKw|5?-jk>~%CVGvctKmP$kpnpfN{D8@X*Aazi$txfa%vd-|E z>kYmV66W!lNekJPom29LdZ%(I+ZLZYTXzTg*to~m?7vp%{V<~>H+2}PQ?PPAq`36R z<%wR8v6UkS>Wt#hzGk#44W<%9S=nBfB);6clKwnxY}T*w21Qc3_?IJ@4gYzC7s;WP zVQNI(M=S=JT#xsZy7G`cR(BP9*je0bfeN8JN5~zY(DDs0t{LpHOIbN);?T-69Pf3R zSNe*&p2%AwXHL>__g+xd4Hlc_vu<25H?(`nafS%)3UPP7_4;gk-9ckt8SJRTv5v0M z_Hww`qPudL?ajIR&X*;$y-`<)6dxx1U~5eGS13CB!lX;3w7n&lDDiArbAhSycd}+b zya_3p@A`$kQy;|NJZ~s44Hqo7Hwt}X86NK=(ey>lgWTtGL6k@Gy;PbO!M%1~Wcn2k zUFP|*5d>t-X*RU8g%>|(wwj*~#l4z^Aatf^DWd1Wj#Q*AY0D^V@sC`M zjJc6qXu0I7Y*2;;gGu!plAFzG=J;1%eIOdn zQA>J&e05UN*7I5@yRhK|lbBSfJ+5Uq;!&HV@xfPZrgD}kE*1DSq^=%{o%|LChhl#0 zlMb<^a6ixzpd{kNZr|3jTGeEzuo}-eLT-)Q$#b{!vKx8Tg}swCni>{#%vDY$Ww$84 zew3c9BBovqb}_&BRo#^!G(1Eg((BScRZ}C)Oz?y`T5wOrv);)b^4XR8 zhJo7+<^7)qB>I;46!GySzdneZ>n_E1oWZY;kf94#)s)kWjuJN1c+wbVoNQcmnv}{> zN0pF+Sl3E}UQ$}slSZeLJrwT>Sr}#V(dVaezCQl2|4LN`7L7v&siYR|r7M(*JYfR$ zst3=YaDw$FSc{g}KHO&QiKxuhEzF{f%RJLKe3p*7=oo`WNP)M(9X1zIQPP0XHhY3c znrP{$4#Ol$A0s|4S7Gx2L23dv*Gv2o;h((XVn+9+$qvm}s%zi6nI-_s6?mG! zj{DV;qesJb&owKeEK?=J>UcAlYckA7Sl+I&IN=yasrZOkejir*kE@SN`fk<8Fgx*$ zy&fE6?}G)d_N`){P~U@1jRVA|2*69)KSe_}!~?+`Yb{Y=O~_+@!j<&oVQQMnhoIRU zA0CyF1OFfkK44n*JD~!2!SCPM;PRSk%1XL=0&rz00wxPs&-_eapJy#$h!eqY%nS0{ z!aGg58JIJPF3_ci%n)QSVpa2H`vIe$RD43;#IRfDV&Ibit z+?>HW4{2wOfC6Fw)}4x}i1maDxcE1qi@BS*qcxD2gE@h3#4cgU*D-&3z7D|tVZWt= z-Cy2+*Cm@P4GN_TPUtaVyVesbVDazF@)j8VJ4>XZv!f%}&eO1SvIgr}4`A*3#vat< z_MoByL(qW6L7SFZ#|Gc1fFN)L2PxY+{B8tJp+pxRyz*87)vXR}*=&ahXjBlQKguuf zX6x<<6fQulE^C*KH8~W%ptpaC0l?b=_{~*U4?5Vt;dgM4t_{&UZ1C2j?b>b+5}{IF_CUyvz-@QZPMlJ)r_tS$9kH%RPv#2_nMb zRLj5;chJ72*U`Z@Dqt4$@_+k$%|8m(HqLG!qT4P^DdfvGf&){gKnGCX#H0!;W=AGP zbA&Z`-__a)VTS}kKFjWGk z%|>yE?t*EJ!qeQ%dPk$;xIQ+P0;()PCBDgjJm6Buj{f^awNoVx+9<|lg3%-$G(*f) zll6oOkN|yamn1uyl2*N-lnqRI1cvs_JxLTeahEK=THV$Sz*gQhKNb*p0fNoda#-&F zB-qJgW^g}!TtM|0bS2QZekW7_tKu%GcJ!4?lObt0z_$mZ4rbQ0o=^curCs3bJK6sq z9fu-aW-l#>z~ca(B;4yv;2RZ?tGYAU)^)Kz{L|4oPj zdOf_?de|#yS)p2v8-N||+XL=O*%3+y)oI(HbM)Ds?q8~HPzIP(vs*G`iddbWq}! z(2!VjP&{Z1w+%eUq^ /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 06032b26..25da30db 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo homeLocation of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo homeLocation of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/intellij-plugin/build.gradle b/intellij-plugin/build.gradle new file mode 100644 index 00000000..fd196e89 --- /dev/null +++ b/intellij-plugin/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'java' + id 'org.jetbrains.intellij.platform' version '2.10.1' +} + +repositories { + mavenCentral() + + intellijPlatform { + defaultRepositories() + } +} + +dependencies { + intellijPlatform { + intellijIdeaCommunity('2023.3') + bundledPlugin("com.intellij.java") + } +} + + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Constants.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Constants.java new file mode 100644 index 00000000..db65881e --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Constants.java @@ -0,0 +1,16 @@ +package net.staticstudios.data.ide.intellij; + +public class Constants { + public static final String DATA_ANNOTATION_FQN = "net.staticstudios.data.Data"; + public static final String COLUMN_ANNOTATION_FQN = "net.staticstudios.data.Column"; + public static final String FOREIGN_COLUMN_ANNOTATION_FQN = "net.staticstudios.data.ForeignColumn"; + public static final String ID_COLUMN_ANNOTATION_FQN = "net.staticstudios.data.IdColumn"; + 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 PERSISTENT_VALUE_FQN = "net.staticstudios.data.PersistentValue"; + 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"; + public static final String ORDER_FQN = "net.staticstudios.data.Order"; +} 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 new file mode 100644 index 00000000..9926fd37 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/DataPsiAugmentProvider.java @@ -0,0 +1,273 @@ +package net.staticstudios.data.ide.intellij; + +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.augment.PsiAugmentProvider; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.CachedValue; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; +import net.staticstudios.data.ide.intellij.query.QueryBuilderUtils; +import net.staticstudios.data.ide.intellij.query.QueryClause; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +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. + //TODO: Add javadocs to generated methods and classes. + //TODO: Make the IDE give inline warnings/errors if static-data is used wrong. I.e. define a non-abstract class that extends UniqueData without the @Data annotation. + + 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> 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_WHERE_CLASS_KEY = Key.create("synthetic.class.query.where"); + + + @Override + protected @NotNull List getAugments(@NotNull PsiElement element, @NotNull Class type, @Nullable String nameHint) { + if (!(element instanceof PsiClass psiClass)) { + return Collections.emptyList(); + } + + if (!Utils.extendsClass(psiClass, Constants.UNIQUE_DATA_FQN)) { + return Collections.emptyList(); + } + + if (!Utils.hasAnnotation(psiClass, Constants.DATA_ANNOTATION_FQN)) { + return Collections.emptyList(); + } + + if (type.isAssignableFrom(PsiClass.class)) { + return List.of(type.cast(getBuilderClass(psiClass)), type.cast(getQueryClass(psiClass))); + } + + if (type.isAssignableFrom(PsiMethod.class)) { + return List.of(type.cast(getBuilderMethod(psiClass)), type.cast(getQueryMethod(psiClass))); + } + + return Collections.emptyList(); + } + + private PsiClass getBuilderClass(PsiClass parent) { + return CachedValuesManager.getCachedValue(parent, BUILDER_CLASS_KEY, () -> { + PsiClass builderClass = createBuilderBuilderClass(parent); + return CachedValueProvider.Result.create(builderClass, parent); + }); + } + + private PsiClass getQueryClass(PsiClass parent) { + return CachedValuesManager.getCachedValue(parent, QUERY_CLASS_KEY, () -> { + PsiClass queryClass = createQueryBuilderClass(parent); + return CachedValueProvider.Result.create(queryClass, parent); + }); + } + + private PsiClass getQueryWhereClass(PsiClass parent) { + return CachedValuesManager.getCachedValue(parent, QUERY_WHERE_CLASS_KEY, () -> { + PsiClass queryWhereClass = createQueryWhereBuilderClass(parent); + return CachedValueProvider.Result.create(queryWhereClass, 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; + } + + 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; + } + + private SyntheticBuilderClass createBuilderBuilderClass(PsiClass parentClass) { + SyntheticBuilderClass builderClass = new SyntheticBuilderClass(parentClass, "Builder"); + PsiType builderType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createType(builderClass, PsiSubstitutor.EMPTY); + for (PsiField psiField : parentClass.getAllFields()) { + PsiType type = psiField.getType(); + PsiType innerType = Utils.getGenericParameter((PsiClassType) type, parentClass.getManager()); + if (Utils.isValidPersistentValue(psiField)) { + SyntheticMethod setterMethod = new SyntheticMethod(parentClass, builderClass, psiField.getName(), builderType); + setterMethod.addParameter(psiField.getName(), innerType); + setterMethod.addModifier(PsiModifier.PUBLIC); + setterMethod.addModifier(PsiModifier.FINAL); + + builderClass.addMethod(setterMethod); + } else if (Utils.isValidReference(psiField)) { + SyntheticMethod setterMethod = new SyntheticMethod(parentClass, builderClass, psiField.getName(), builderType); + setterMethod.addParameter(psiField.getName(), innerType); + setterMethod.addModifier(PsiModifier.PUBLIC); + setterMethod.addModifier(PsiModifier.FINAL); + + builderClass.addMethod(setterMethod); + } + //todo: support CachedValues, similar to PVs + } + + return builderClass; + } + + private SyntheticBuilderClass createQueryBuilderClass(PsiClass parentClass) { + SyntheticBuilderClass queryClass = new SyntheticBuilderClass(parentClass, "Query"); + PsiClass whereClass = getQueryWhereClass(parentClass); + PsiType whereType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createType(whereClass, PsiSubstitutor.EMPTY); + PsiType queryType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createType(queryClass, PsiSubstitutor.EMPTY); + PsiType parentType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createType(parentClass, PsiSubstitutor.EMPTY); + + PsiType intType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createTypeFromText("int", parentClass); + + PsiType orderType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createTypeFromText(Constants.ORDER_FQN, parentClass); + + + PsiClass listClass = JavaPsiFacade.getInstance(parentClass.getProject()) + .findClass(List.class.getName(), GlobalSearchScope.allScope(parentClass.getProject())); + assert listClass != null; + + PsiClass functionClass = JavaPsiFacade.getInstance(parentClass.getProject()) + .findClass(Function.class.getName(), GlobalSearchScope.allScope(parentClass.getProject())); + assert functionClass != null; + PsiSubstitutor substitutor = PsiSubstitutor.EMPTY + .put(functionClass.getTypeParameters()[0], whereType) + .put(functionClass.getTypeParameters()[1], whereType); + PsiType whereFunctionType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createType(functionClass, substitutor); + + substitutor = PsiSubstitutor.EMPTY + .put(listClass.getTypeParameters()[0], parentType); + PsiType listOfParentType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createType(listClass, substitutor); + + for (PsiField psiField : parentClass.getAllFields()) { + if (Utils.isValidPersistentValue(psiField)) { + SyntheticMethod orderByMethod = new SyntheticMethod(parentClass, queryClass, "orderBy" + StringUtil.capitalize(psiField.getName()), queryType); + orderByMethod.addParameter("order", orderType); + orderByMethod.addModifier(PsiModifier.PUBLIC); + orderByMethod.addModifier(PsiModifier.FINAL); + queryClass.addMethod(orderByMethod); + } + } + + SyntheticMethod whereMethod = new SyntheticMethod(parentClass, queryClass, "where", queryType); + whereMethod.addModifier(PsiModifier.PUBLIC); + whereMethod.addModifier(PsiModifier.FINAL); + whereMethod.addParameter("where", whereFunctionType); + queryClass.addMethod(whereMethod); + SyntheticMethod limitMethod = new SyntheticMethod(parentClass, queryClass, "limit", queryType); + limitMethod.addParameter("limit", intType); + limitMethod.addModifier(PsiModifier.PUBLIC); + limitMethod.addModifier(PsiModifier.FINAL); + queryClass.addMethod(limitMethod); + SyntheticMethod offsetMethod = new SyntheticMethod(parentClass, queryClass, "offset", queryType); + offsetMethod.addParameter("offset", intType); + offsetMethod.addModifier(PsiModifier.PUBLIC); + offsetMethod.addModifier(PsiModifier.FINAL); + queryClass.addMethod(offsetMethod); + + SyntheticMethod findAllMethod = new SyntheticMethod(parentClass, queryClass, "findAll", listOfParentType); + findAllMethod.addModifier(PsiModifier.PUBLIC); + findAllMethod.addModifier(PsiModifier.FINAL); + queryClass.addMethod(findAllMethod); + SyntheticMethod findOneMethod = new SyntheticMethod(parentClass, queryClass, "findOne", parentType); + findOneMethod.addModifier(PsiModifier.PUBLIC); + findOneMethod.addModifier(PsiModifier.FINAL); + queryClass.addMethod(findOneMethod); + + return queryClass; + } + + private SyntheticBuilderClass createQueryWhereBuilderClass(PsiClass parentClass) { + SyntheticBuilderClass whereClass = new SyntheticBuilderClass(parentClass, "QueryWhere"); + PsiType whereType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createType(whereClass, PsiSubstitutor.EMPTY); + + PsiClass functionClass = JavaPsiFacade.getInstance(parentClass.getProject()) + .findClass(Function.class.getName(), GlobalSearchScope.allScope(parentClass.getProject())); + assert functionClass != null; + PsiSubstitutor substitutor = PsiSubstitutor.EMPTY + .put(functionClass.getTypeParameters()[0], whereType) + .put(functionClass.getTypeParameters()[1], whereType); + PsiType functionParenType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createType(functionClass, substitutor); + + + SyntheticMethod andMethod = new SyntheticMethod(parentClass, whereClass, "and", whereType); + andMethod.addModifier(PsiModifier.PUBLIC); + andMethod.addModifier(PsiModifier.FINAL); + whereClass.addMethod(andMethod); + SyntheticMethod orMethod = new SyntheticMethod(parentClass, whereClass, "or", whereType); + orMethod.addModifier(PsiModifier.PUBLIC); + orMethod.addModifier(PsiModifier.FINAL); + whereClass.addMethod(orMethod); + + // ( [clause] ) + SyntheticMethod groupMethod = new SyntheticMethod(parentClass, whereClass, "group", whereType); + groupMethod.addParameter("clause", functionParenType); + groupMethod.addModifier(PsiModifier.PUBLIC); + groupMethod.addModifier(PsiModifier.FINAL); + whereClass.addMethod(groupMethod); + for (PsiField psiField : parentClass.getAllFields()) { + PsiType type = psiField.getType(); + boolean isValidReference = false; + if (!Utils.isValidPersistentValue(psiField) && !(isValidReference = Utils.isValidReference(psiField))) { + continue; //non-supported field type + } + PsiType innerType = Utils.getGenericParameter((PsiClassType) type, parentClass.getManager()); + + List clauses = QueryBuilderUtils.getClausesForType(psiField, isValidReference || Utils.isNullable(psiField, type)); + for (QueryClause clause : clauses) { + String methodName = clause.getMethodName(psiField.getName()); + List parameterTypes = clause.getMethodParamTypes(parentClass.getManager(), innerType); + SyntheticMethod queryMethod = new SyntheticMethod(parentClass, whereClass, methodName, whereType); + for (int i = 0; i < parameterTypes.size(); i++) { + queryMethod.addParameter(psiField.getName() + i, parameterTypes.get(i)); + } + queryMethod.addModifier(PsiModifier.PUBLIC); + queryMethod.addModifier(PsiModifier.FINAL); + whereClass.addMethod(queryMethod); + } + } + + return whereClass; + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..0e17e391 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticBuilderClass.java @@ -0,0 +1,138 @@ +package net.staticstudios.data.ide.intellij; + +import com.intellij.lang.java.JavaLanguage; +import com.intellij.psi.*; +import com.intellij.psi.impl.PsiClassImplUtil; +import com.intellij.psi.impl.light.LightModifierList; +import com.intellij.psi.impl.light.LightPsiClassBase; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.SearchScope; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * A synthetic builder class generated for data classes. + * "Builder" refers to the builder pattern, not to be confused with other uses of the term "builder". + */ +public class SyntheticBuilderClass extends LightPsiClassBase { + + private final WeakReference parentClass; + private final List methods = new ArrayList<>(); + private final LightModifierList modifierList; + + public SyntheticBuilderClass(@NotNull PsiClass parentClass, String suffix) { + super(parentClass, parentClass.getName() + suffix); + this.parentClass = new WeakReference<>(parentClass); + this.modifierList = new LightModifierList(getManager(), JavaLanguage.INSTANCE, PsiModifier.PUBLIC, PsiModifier.STATIC, PsiModifier.FINAL); + } + + public void addMethod(PsiMethod method) { + methods.add(method); + } + + @Override + public @NotNull GlobalSearchScope getResolveScope() { + PsiClass parentClass = this.parentClass.get(); + if (parentClass != null) { + return parentClass.getResolveScope(); + } + return super.getResolveScope(); + } + + @Override + public @NotNull SearchScope getUseScope() { + PsiClass parentClass = this.parentClass.get(); + if (parentClass != null) { + return parentClass.getUseScope(); + } + return super.getUseScope(); + } + + @Override + public @NotNull PsiModifierList getModifierList() { + return modifierList; + } + + @Override + public @Nullable PsiReferenceList getExtendsList() { + return null; + } + + @Override + public @Nullable PsiReferenceList getImplementsList() { + return null; + } + + @Override + public PsiField @NotNull [] getFields() { + return PsiField.EMPTY_ARRAY; + } + + @Override + public PsiMethod @NotNull [] getMethods() { + return methods.toArray(PsiMethod.EMPTY_ARRAY); + } + + @Override + public PsiClass @NotNull [] getInnerClasses() { + return PsiClass.EMPTY_ARRAY; + } + + @Override + public PsiClassInitializer @NotNull [] getInitializers() { + return PsiClassInitializer.EMPTY_ARRAY; + } + + @Override + public PsiElement getScope() { + return null; + } + + @Override + public @Nullable PsiClass getContainingClass() { + return parentClass.get(); + } + + @Override + public @Nullable PsiTypeParameterList getTypeParameterList() { + return null; + } + + @Override + public boolean isValid() { + PsiClass parentClass = this.parentClass.get(); + return parentClass != null && parentClass.isValid(); + } + + @Override + public PsiFile getContainingFile() { + PsiClass parentClass = this.parentClass.get(); + if (parentClass != null) { + return parentClass.getContainingFile(); + } + return null; + } + + @Override + public @NotNull PsiElement getNavigationElement() { + PsiClass parentClass = this.parentClass.get(); + if (parentClass != null) { + return parentClass.getNavigationElement(); + } + return this; + } + + @Override + public PsiElement getParent() { + return null; // Note: when returning parentClass.get(), there are issues finding the class, for some reason. So don't return it. + } + + @Override + public boolean isEquivalentTo(PsiElement another) { + return PsiClassImplUtil.isClassEquivalentTo(this, another); + } +} \ 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 new file mode 100644 index 00000000..fba250ea --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticMethod.java @@ -0,0 +1,111 @@ +package net.staticstudios.data.ide.intellij; + +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightMethodBuilder; +import com.intellij.psi.javadoc.PsiDocComment; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.WeakReference; + +/** + * A synthetic method generated for data classes. + */ +public class SyntheticMethod extends LightMethodBuilder implements SyntheticElement { + + private final WeakReference parentClass; + private final PsiClass containingClass; + private final PsiType returnType; + private final String name; + + public SyntheticMethod(@NotNull PsiClass parentClass, @NotNull PsiClass containingClass, @NotNull String name, PsiType returnType) { + super(parentClass, parentClass.getLanguage()); + this.name = name; + this.returnType = returnType; + this.parentClass = new WeakReference<>(parentClass); + this.containingClass = containingClass; + setContainingClass(containingClass); + } + + @Override + public @Nullable PsiType getReturnType() { + return returnType; + } + + @Override + public boolean isConstructor() { + return false; + } + + @Override + public boolean isVarArgs() { + return false; + } + + @Override + public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException { + return this; + } + + @Override + public @Nullable PsiClass getContainingClass() { + return containingClass; + } + + @Override + public boolean hasTypeParameters() { + return false; + } + + @Override + public @Nullable PsiTypeParameterList getTypeParameterList() { + return null; + } + + @Override + public PsiTypeParameter @NotNull [] getTypeParameters() { + return new PsiTypeParameter[0]; + } + + @Override + public boolean isValid() { + PsiClass cls = parentClass.get(); + return cls != null && cls.isValid(); + } + + @Override + public String toString() { + return "SyntheticMethod:" + getName(); + } + + @Override + public @NotNull String getName() { + return name; + } + + @Override + public @NotNull PsiElement getNavigationElement() { + PsiClass cls = parentClass.get(); + if (cls != null) { + return cls.getNavigationElement(); + } + return this; + } + + @Override + public PsiElement getParent() { + return parentClass.get(); + } + + @Override + public boolean isDeprecated() { + return false; + } + + @Override + public @Nullable PsiDocComment getDocComment() { + return null; + } +} \ No newline at end of file diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Utils.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Utils.java new file mode 100644 index 00000000..ea702a44 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Utils.java @@ -0,0 +1,81 @@ +package net.staticstudios.data.ide.intellij; + +import com.intellij.psi.*; + +import java.util.Objects; + +public class Utils { + 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()); + } + + 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; + } + } + + return extendsClass; + } + + public static boolean hasAnnotation(PsiModifierListOwner element, String annotationFqn) { + if (element.getModifierList() == null) { + return false; + } + return element.getModifierList().findAnnotation(annotationFqn) != null; + } + + public static PsiType getGenericParameter(PsiClassType type, PsiManager manager) { + PsiType[] params = type.getParameters(); + return (params.length > 0) ? params[0] : PsiType.getJavaLangObject(manager, type.getResolveScope()); + } + + public static boolean isNullable(PsiField psiField, PsiType fieldType) { + PsiModifierList modifierList = psiField.getModifierList(); + if (modifierList == null) return false; + if (is(fieldType, Constants.PERSISTENT_VALUE_FQN)) { + PsiAnnotation annotation; + annotation = modifierList.findAnnotation(Constants.COLUMN_ANNOTATION_FQN); + if (annotation == null) { + annotation = modifierList.findAnnotation(Constants.ID_COLUMN_ANNOTATION_FQN); + } + if (annotation == null) { + annotation = modifierList.findAnnotation(Constants.FOREIGN_COLUMN_ANNOTATION_FQN); + } + if (annotation == null) return false; + PsiAnnotationMemberValue memberValue = annotation.findAttributeValue("nullable"); + if (memberValue instanceof PsiLiteralExpression) { + Object value = ((PsiLiteralExpression) memberValue).getValue(); + if (value instanceof Boolean) { + return (Boolean) value; + } + } + return false; + } + + return true; // assume null for other fields like Reference etc... + } + + public static boolean isValidPersistentValue(PsiField psiField) { + return Utils.is(psiField.getType(), Constants.PERSISTENT_VALUE_FQN) && ( + Utils.hasAnnotation(psiField, Constants.COLUMN_ANNOTATION_FQN) || + Utils.hasAnnotation(psiField, Constants.FOREIGN_COLUMN_ANNOTATION_FQN) || + Utils.hasAnnotation(psiField, Constants.ID_COLUMN_ANNOTATION_FQN)); + } + + public static boolean isValidReference(PsiField psiField) { + return Utils.is(psiField.getType(), Constants.REFERENCE_FQN) && + Utils.hasAnnotation(psiField, Constants.ONE_TO_ONE_ANNOTATION_FQN); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/NumericClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/NumericClause.java new file mode 100644 index 00000000..b41543f3 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/NumericClause.java @@ -0,0 +1,14 @@ +package net.staticstudios.data.ide.intellij.query; + +import com.intellij.psi.PsiClassType; +import com.intellij.psi.PsiField; +import net.staticstudios.data.ide.intellij.Utils; + +public interface NumericClause extends QueryClause { + + @Override + default boolean matches(PsiField psiField, boolean nullable) { + if (!(psiField.getType() instanceof PsiClassType psiClassType)) return false; + return QueryBuilderUtils.isNumeric(Utils.getGenericParameter(psiClassType, psiField.getManager())); + } +} 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 new file mode 100644 index 00000000..451b5442 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryBuilderUtils.java @@ -0,0 +1,77 @@ +package net.staticstudios.data.ide.intellij.query; + +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.Utils; +import net.staticstudios.data.ide.intellij.query.clause.*; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +public class QueryBuilderUtils { + private static final List pvClauses; + private static final List referenceClauses; + + static { + pvClauses = new ArrayList<>(); + pvClauses.add(new IsClause()); + pvClauses.add(new IsNotClause()); + + pvClauses.add(new IsNullClause()); + pvClauses.add(new IsNotNullClause()); + + pvClauses.add(new IsLikeClause()); + pvClauses.add(new IsNotLikeClause()); + + pvClauses.add(new IsGreaterThanClause()); + pvClauses.add(new IsLessThanClause()); + pvClauses.add(new IsGreaterThanOrEqualToClause()); + pvClauses.add(new IsLessThanOrEqualToClause()); + pvClauses.add(new IsBetweenClause()); + pvClauses.add(new IsNotBetweenClause()); + + referenceClauses = new ArrayList<>(); + referenceClauses.add(new IsClause()); + referenceClauses.add(new IsNotClause()); + referenceClauses.add(new IsNullClause()); + referenceClauses.add(new IsNotNullClause()); + } + + public static List getClausesForType(PsiField psiField, boolean nullable) { + if (Utils.isValidPersistentValue(psiField)) { + List applicableClauses = new ArrayList<>(); + for (QueryClause clause : pvClauses) { + if (clause.matches(psiField, nullable)) { + applicableClauses.add(clause); + } + } + return applicableClauses; + } + if (Utils.isValidReference(psiField)) { + List applicableClauses = new ArrayList<>(); + for (QueryClause clause : referenceClauses) { + if (clause.matches(psiField, nullable)) { + applicableClauses.add(clause); + } + } + return applicableClauses; + } + return List.of(); + } + + public static boolean isNumeric(PsiType psiType) { + String typeName = psiType.getCanonicalText(); + return typeName.equals(Integer.class.getName()) || + typeName.equals(Long.class.getName()) || + typeName.equals(Float.class.getName()) || + typeName.equals(Double.class.getName()) || + typeName.equals(Short.class.getName()) || + typeName.equals(Timestamp.class.getName()) || + typeName.equals("int") || + typeName.equals("long") || + typeName.equals("float") || + typeName.equals("short") || + typeName.equals("double"); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryClause.java new file mode 100644 index 00000000..990656da --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryClause.java @@ -0,0 +1,16 @@ +package net.staticstudios.data.ide.intellij.query; + +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; + +import java.util.List; + +public interface QueryClause { + + boolean matches(PsiField psiField, boolean nullable); + + String getMethodName(String fieldName); + + List getMethodParamTypes(PsiManager manager, PsiType fieldType); +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsBetweenClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsBetweenClause.java new file mode 100644 index 00000000..67729733 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsBetweenClause.java @@ -0,0 +1,20 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.NumericClause; + +import java.util.List; + +public class IsBetweenClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsBetween"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return List.of(fieldType, fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsClause.java new file mode 100644 index 00000000..445c7728 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsClause.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class IsClause 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) { + return List.of(fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanClause.java new file mode 100644 index 00000000..8c94257f --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanClause.java @@ -0,0 +1,20 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.NumericClause; + +import java.util.List; + +public class IsGreaterThanClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsGreaterThan"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return List.of(fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanOrEqualToClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanOrEqualToClause.java new file mode 100644 index 00000000..ef603763 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanOrEqualToClause.java @@ -0,0 +1,20 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.NumericClause; + +import java.util.List; + +public class IsGreaterThanOrEqualToClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsGreaterThanOrEqualTo"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return List.of(fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanClause.java new file mode 100644 index 00000000..08d24cad --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanClause.java @@ -0,0 +1,20 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.NumericClause; + +import java.util.List; + +public class IsLessThanClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsLessThan"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return List.of(fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanOrEqualToClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanOrEqualToClause.java new file mode 100644 index 00000000..a54e61d9 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanOrEqualToClause.java @@ -0,0 +1,20 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.NumericClause; + +import java.util.List; + +public class IsLessThanOrEqualToClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsLessThanOrEqualTo"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return List.of(fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java new file mode 100644 index 00000000..1cada5ff --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.Utils; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class IsLikeClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return Utils.is(psiField.getType(), String.class.getName()); + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsLike"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return List.of(fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotBetweenClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotBetweenClause.java new file mode 100644 index 00000000..7ec55c33 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotBetweenClause.java @@ -0,0 +1,20 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.NumericClause; + +import java.util.List; + +public class IsNotBetweenClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNotBetween"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return List.of(fieldType, fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotClause.java new file mode 100644 index 00000000..807eb766 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotClause.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class IsNotClause 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) { + return List.of(fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java new file mode 100644 index 00000000..7e489366 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.Utils; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class IsNotLikeClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return Utils.is(psiField.getType(), String.class.getName()); + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNotLike"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return List.of(fieldType); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotNullClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotNullClause.java new file mode 100644 index 00000000..0736068a --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotNullClause.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.Collections; +import java.util.List; + +public class IsNotNullClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return nullable; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNotNull"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return Collections.emptyList(); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNullClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNullClause.java new file mode 100644 index 00000000..3dfe904f --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNullClause.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.Collections; +import java.util.List; + +public class IsNullClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return nullable; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNull"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { + return Collections.emptyList(); + } +} diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml new file mode 100644 index 00000000..3bd0b421 --- /dev/null +++ b/intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,15 @@ + + net.staticstudios.data.ide.intellij + static-data + Static Studios + 1.0.0 + + Provides support for static-data. static-data generates builders and other utilities at compile-time in order to + provide a better developer experience, with strict type safety. This plugin makes IntelliJ aware of these + generated classes and/or methods. + + + + + + diff --git a/javac-plugin/build.gradle b/javac-plugin/build.gradle new file mode 100644 index 00000000..c1ee0d4e --- /dev/null +++ b/javac-plugin/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":annotations")) +} + +tasks.withType(JavaCompile).configureEach { + options.compilerArgs += [ + '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' + ] +} + + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java new file mode 100644 index 00000000..4eeaa51b --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java @@ -0,0 +1,102 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.*; +import com.sun.tools.javac.api.BasicJavacTask; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.Names; +import net.staticstudios.data.Data; + + +public class StaticDataJavacPlugin implements Plugin { + //TODO: properly implement this and match the IntelliJ plugin's behavior + // note: delegate a lot of behavior to utility classes to avoid generated unnecessary (and less reliable/more complex) code. + // i.e. AbstractQueryBuilder or something + + @Override + public String getName() { + return "StaticDataJavacPlugin"; + } + + @Override + public void init(JavacTask task, String... args) { + Context context = ((BasicJavacTask) task).getContext(); + TreeMaker treeMaker = TreeMaker.instance(context); + Names names = Names.instance(context); + Trees trees = Trees.instance(task); + + task.addTaskListener(new TaskListener() { + @Override + public void finished(TaskEvent e) { + if (e.getKind() != TaskEvent.Kind.ENTER) return; + + e.getCompilationUnit().accept(new TreeScanner() { + @Override + public Void visitClass(ClassTree node, Void unused) { + boolean hasDataAnnotation = node.getModifiers().getAnnotations().stream() + .anyMatch(a -> { + Tree type = a.getAnnotationType(); + if (type instanceof IdentifierTree) { //todo: handle qualified names + return type.toString().equals(Data.class.getSimpleName()); + } + return false; + }); + + if (!hasDataAnnotation) return super.visitClass(node, unused); + + JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) node; + + boolean hasBuilderClass = classDecl.defs.stream() + .anyMatch(def -> def instanceof JCTree.JCClassDecl && + ((JCTree.JCClassDecl) def).name.toString().equals("Builder")); + boolean hasBuilderMethod = classDecl.defs.stream() + .anyMatch(def -> def instanceof JCTree.JCMethodDecl && + ((JCTree.JCMethodDecl) def).name.toString().equals("builder") && + (((JCTree.JCMethodDecl) def).mods.flags & Flags.STATIC) != 0); + + if (!hasBuilderClass) { + System.err.println("Adding Builder class to " + classDecl.name); + JCTree.JCClassDecl builderClass = treeMaker.ClassDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString("Builder"), + List.nil(), + null, + List.nil(), + List.nil() + ); + classDecl.defs = classDecl.defs.append(builderClass); + } + if (!hasBuilderMethod) { + System.err.println("Adding builder() method to " + classDecl.name); + JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString("builder"), + treeMaker.Ident(names.fromString("Builder")), + List.nil(), + List.nil(), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Return( + treeMaker.NewClass(null, List.nil(), + treeMaker.Ident(names.fromString("Builder")), + List.nil(), null) + ) + )), + null + ); + classDecl.defs = classDecl.defs.append(builderMethod); + } + return super.visitClass(node, unused); + } + }, null); + } + }); + + } +} diff --git a/javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin b/javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin new file mode 100644 index 00000000..daff7920 --- /dev/null +++ b/javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin @@ -0,0 +1 @@ +net.staticstudios.data.compiler.javac.StaticDataJavacPlugin \ No newline at end of file diff --git a/oldsrc/og/java/net/staticstudios/data/CachedValue.java b/oldsrc/og/java/net/staticstudios/data/CachedValue.java deleted file mode 100644 index eb72eb8f..00000000 --- a/oldsrc/og/java/net/staticstudios/data/CachedValue.java +++ /dev/null @@ -1,285 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.data.value.InitialCachedValue; -import net.staticstudios.data.data.value.Value; -import net.staticstudios.data.impl.CachedValueManager; -import net.staticstudios.data.key.RedisKey; -import net.staticstudios.data.primative.Primitive; -import net.staticstudios.data.primative.Primitives; -import net.staticstudios.data.util.DataDoesNotExistException; -import net.staticstudios.data.util.DeletionStrategy; -import net.staticstudios.data.util.ValueUpdate; -import net.staticstudios.data.util.ValueUpdateHandler; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.Blocking; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.time.Instant; -import java.util.List; -import java.util.function.Supplier; - -/** - * Represents a value that lives in Redis. - * Data stored in {@link CachedValue} objects should be considered volatile and may be deleted at any time. - * - * @param the type of data stored in this value - */ -public class CachedValue implements Value { - private final String identifyingKey; - private final Class dataType; - private final DataHolder holder; - private final DataManager dataManager; - private final String schema; - private final String table; - private final String idColumn; - private int expirySeconds = -1; - private Supplier fallbackValue; - private DeletionStrategy deletionStrategy; - - private CachedValue(String key, String schema, String table, String idColumn, Class dataType, DataHolder holder, DataManager dataManager) { - if (!holder.getDataManager().isSupportedType(dataType)) { - throw new IllegalArgumentException("Unsupported data type: " + dataType); - } - - this.identifyingKey = key; - this.schema = schema; - this.table = table; - this.idColumn = idColumn; - this.dataType = dataType; - this.holder = holder; - this.dataManager = dataManager; - } - - /** - * Create a new {@link CachedValue} object. - * - * @param holder the holder of this value - * @param dataType the type of data stored in this value - * @param key the identifying key for this value (note that this is only part of what is used to construct the Redis key, see {@link RedisKey#toString()}) - * @param the type of data stored in this value - * @return the new {@link CachedValue} object - */ - public static CachedValue of(UniqueData holder, Class dataType, String key) { - CachedValue cv = new CachedValue<>(key, holder.getSchema(), holder.getTable(), holder.getIdentifier().getColumn(), dataType, holder, holder.getDataManager()); - cv.deletionStrategy = DeletionStrategy.CASCADE; - return cv; - } - - /** - * Create a new {@link CachedValue} object. - * - * @param holder the holder of this value - * @param dataType the type of data stored in this value - * @param schema the schema of the table that the holder is stored in - * @param table the table that the holder is stored in - * @param idColumn the column in the table that holds the id of the holder - * @param key the identifying key for this value (note that this is only part of what is used to construct the Redis key, see {@link RedisKey#toString()}) - * @param the type of data stored in this value - * @return the new {@link CachedValue} object - */ - public static CachedValue of(UniqueData holder, Class dataType, String schema, String table, String idColumn, String key) { - CachedValue cv = new CachedValue<>(key, schema, table, idColumn, dataType, holder, holder.getDataManager()); - cv.deletionStrategy = DeletionStrategy.CASCADE; - return cv; - } - - /** - * Set the initial value for this object - * - * @param value the initial value - * @return the initial value object - */ - public InitialCachedValue initial(@Nullable T value) { - return new InitialCachedValue(this, value); - } - - /** - * Add an update handler to this value. - * Note that update handlers are run asynchronously. - * - * @param updateHandler the update handler - * @return this - */ - @SuppressWarnings("unchecked") - public CachedValue onUpdate(ValueUpdateHandler updateHandler) { - dataManager.registerValueUpdateHandler(this.getKey(), update -> ThreadUtils.submit(() -> updateHandler.handle((ValueUpdate) update))); - return this; - } - - /** - * Set the expiry time for this value. - * After the expiry time has passed, the value will be deleted from Redis, and this {@link CachedValue} object will return the fallback value. - * - * @param seconds the number of seconds before the value expires, or -1 to disable expiry - * @return this - */ - public CachedValue withExpiry(int seconds) { - this.expirySeconds = seconds; - return this; - } - - /** - * Fallback values are similar to default values, however they are not stored. - * The fallback value is returned by {@link #get()} if the value does not exist, or it is null. - * Unlike default values, fallback values are never stored. - * - * @param fallbackValue the value to fall back on - * @return this - */ - public CachedValue withFallback(Supplier fallbackValue) { - this.fallbackValue = fallbackValue; - return this; - } - - /** - * Fallback values are similar to default values, however they are not stored. - * The fallback value is returned by {@link #get()} if the value does not exist, or it is null. - * Unlike default values, fallback values are never stored. - * - * @param fallbackValue the value to fall back on - * @return this - */ - public CachedValue withFallback(T fallbackValue) { - this.fallbackValue = () -> fallbackValue; - return this; - } - - @SuppressWarnings("unchecked") - private T getFallbackValue() { - T fallbackValue = this.fallbackValue == null ? null : this.fallbackValue.get(); - if (fallbackValue == null) { - if (!Primitives.isPrimitive(dataType)) { - return null; - } - - Primitive primitive = Primitives.getPrimitive(dataType); - - if (primitive.isNullable()) { - return null; - } - - return (T) primitive.getDefaultValue(); - } - - return fallbackValue; - } - - /** - * Get the expiry time for this value. - * - * @return the expiry time in seconds, or -1 if expiry is disabled - */ - public int getExpirySeconds() { - return expirySeconds; - } - - @Override - public RedisKey getKey() { - return new RedisKey(this); - } - - public T get() { - T value; - try { - value = getDataManager().get(this); - } catch (DataDoesNotExistException e) { - value = null; - } - - return value == null ? getFallbackValue() : value; - } - - public void set(T value) { - CachedValueManager manager = dataManager.getCachedValueManager(); - dataManager.cache(this.getKey(), dataType, value, Instant.now(), true); - getDataManager().submitAsyncTask((connection, jedis) -> manager.setInRedis(jedis, List.of(initial(value)))); - } - - /** - * Set the value of this object. - * This method blocks until the value has been set in Redis. - * - * @param value the value to set - */ - @Blocking - public void setNow(T value) { - CachedValueManager manager = dataManager.getCachedValueManager(); - dataManager.cache(this.getKey(), dataType, value, Instant.now(), true); - getDataManager().submitBlockingTask((connection, jedis) -> manager.setInRedis(jedis, List.of(initial(value)))); - } - - /** - * Get the identifying key for this value. - * Note that this is only part of what is used to construct the Redis key, see {@link RedisKey#toString()} - * - * @return the identifying key - */ - public String getIdentifyingKey() { - return identifyingKey; - } - - @Override - public Class getDataType() { - return dataType; - } - - @Override - public DataManager getDataManager() { - return dataManager; - } - - @Override - public DataHolder getHolder() { - return holder; - } - - public String getSchema() { - return schema; - } - - public String getTable() { - return table; - } - - public String getIdColumn() { - return idColumn; - } - - @Override - public String toString() { - return "CachedValue{" + - "identifyingKey='" + identifyingKey + - '}'; - } - - @Override - public int hashCode() { - return getKey().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - if (!(obj instanceof CachedValue other)) { - return false; - } - - return getKey().equals(other.getKey()); - } - - @Override - public CachedValue deletionStrategy(DeletionStrategy strategy) { - this.deletionStrategy = strategy; - return this; - } - - @Override - public @NotNull DeletionStrategy getDeletionStrategy() { - return deletionStrategy == null ? DeletionStrategy.NO_ACTION : deletionStrategy; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/DataManager.java b/oldsrc/og/java/net/staticstudios/data/DataManager.java deleted file mode 100644 index 3fa5e751..00000000 --- a/oldsrc/og/java/net/staticstudios/data/DataManager.java +++ /dev/null @@ -1,1148 +0,0 @@ -package net.staticstudios.data; - -import com.google.common.base.Preconditions; -import com.google.common.base.Predicate; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; -import net.staticstudios.data.data.Data; -import net.staticstudios.data.data.InitialValue; -import net.staticstudios.data.data.collection.PersistentManyToManyCollection; -import net.staticstudios.data.data.collection.PersistentUniqueDataCollection; -import net.staticstudios.data.data.collection.SimplePersistentCollection; -import net.staticstudios.data.data.value.InitialCachedValue; -import net.staticstudios.data.data.value.InitialPersistentValue; -import net.staticstudios.data.data.value.Value; -import net.staticstudios.data.impl.CachedValueManager; -import net.staticstudios.data.impl.PersistentCollectionManager; -import net.staticstudios.data.impl.PersistentValueManager; -import net.staticstudios.data.impl.pg.PostgresListener; -import net.staticstudios.data.impl.pg.PostgresOperation; -import net.staticstudios.data.key.CellKey; -import net.staticstudios.data.key.DataKey; -import net.staticstudios.data.key.DatabaseKey; -import net.staticstudios.data.primative.Primitives; -import net.staticstudios.data.util.TaskQueue; -import net.staticstudios.utils.ShutdownStage; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.Blocking; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import redis.clients.jedis.Jedis; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.Instant; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; - -/** - * Manages all data operations. - */ -public class DataManager extends SQLLogger { - private static final Object NULL_MARKER = new Object(); - private final Logger logger = LoggerFactory.getLogger(DataManager.class); - private final Map cache; - private final Multimap> valueUpdateHandlers; - private final Multimap, UUID> uniqueDataIds; - private final Map, Map> uniqueDataCache; - private final Multimap> dummyValueMap; - private final Multimap> dummySimplePersistentCollectionMap; - private final Multimap> dummyPersistentManyToManyCollectionMap; - private final Multimap dummyUniqueDataMap; - private final Map, UniqueData> dummyInstances; - private final Set> loadedDependencies = new HashSet<>(); - private final List> valueSerializers; - private final PostgresListener pgListener; - private final String applicationName; - private final PersistentValueManager persistentValueManager; - private final PersistentCollectionManager persistentCollectionManager; - private final CachedValueManager cachedValueManager; - - private final TaskQueue taskQueue; - - - /** - * Create a new data manager. - * - * @param dataSourceConfig the data source configuration - */ - public DataManager(DataSourceConfig dataSourceConfig) { - this.applicationName = "static_data_manager-" + UUID.randomUUID(); - this.cache = new ConcurrentHashMap<>(); - this.valueUpdateHandlers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); - this.uniqueDataCache = new ConcurrentHashMap<>(); - this.dummyValueMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.dummySimplePersistentCollectionMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.dummyPersistentManyToManyCollectionMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.dummyUniqueDataMap = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.dummyInstances = new ConcurrentHashMap<>(); - this.uniqueDataIds = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.valueSerializers = new CopyOnWriteArrayList<>(); - - pgListener = new PostgresListener(this, dataSourceConfig); - - pgListener.addHandler(notification -> { //Call insert handler first so that we can access the data in the other handlers - if (notification.getOperation() == PostgresOperation.INSERT) { - logger.trace("Handing INSERT operation"); - Collection dummyUniqueData = dummyUniqueDataMap.get(notification.getSchema() + "." + notification.getTable()); - - for (UniqueData dummy : dummyUniqueData) { - String identifierColumn = dummy.getIdentifier().getColumn(); - UUID id = UUID.fromString(notification.getData().newDataValueMap().get(identifierColumn)); - - UniqueData instance = createInstance(dummy.getClass(), id); - addUniqueData(instance); - } - } - }); - - this.persistentValueManager = new PersistentValueManager(this, pgListener); - this.persistentCollectionManager = new PersistentCollectionManager(this, pgListener); - this.cachedValueManager = new CachedValueManager(this, dataSourceConfig); - - pgListener.addHandler(notification -> { //Call delete handler last so that we can access the data in the other handlers - if (notification.getOperation() == PostgresOperation.DELETE) { - logger.trace("Handling DELETE operation"); - Collection dummyUniqueData = dummyUniqueDataMap.get(notification.getSchema() + "." + notification.getTable()); - - for (UniqueData dummy : dummyUniqueData) { - String identifierColumn = dummy.getIdentifier().getColumn(); - UUID id = UUID.fromString(notification.getData().oldDataValueMap().get(identifierColumn)); - removeUniqueData(dummy.getClass(), id); - } - } - }); - - - this.taskQueue = new TaskQueue(dataSourceConfig, applicationName); - - ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, () -> { - cache.clear(); - uniqueDataCache.clear(); - uniqueDataIds.clear(); - }); - } - - /** - * Submit a blocking task to the priority task queue. - * This method is blocking and will wait for the task to complete before returning. - * - * @param task The task to submit - */ - @Blocking - public void submitBlockingTask(ConnectionConsumer task) { - taskQueue.submitTask(task).join(); - } - - public void submitAsyncTask(ConnectionConsumer task) { - taskQueue.submitTask(task) - .exceptionally(e -> { - logger.error("Error submitting async task", e); - return null; - }); - } - - /** - * Submit a blocking task to the priority task queue. - * This method is blocking and will wait for the task to complete before returning. - * - * @param task The task to submit - */ - @Blocking - public void submitBlockingTask(ConnectionJedisConsumer task) { - taskQueue.submitTask(task).join(); - } - - public void submitAsyncTask(ConnectionJedisConsumer task) { - taskQueue.submitTask(task); - } - - public PersistentValueManager getPersistentValueManager() { - return persistentValueManager; - } - - public PersistentCollectionManager getPersistentCollectionManager() { - return persistentCollectionManager; - } - - public CachedValueManager getCachedValueManager() { - return cachedValueManager; - } - - public String getApplicationName() { - return applicationName; - } - - public Collection> getDummyValues(String schemaTable) { - return dummyValueMap.get(schemaTable); - } - - public Collection getDummyUniqueData(String schemaTable) { - return dummyUniqueDataMap.get(schemaTable); - } - - public UniqueData getDummyInstance(Class clazz) { - return dummyInstances.get(clazz); - } - - public Collection> getDummyPersistentCollections(String schemaTable) { - return dummySimplePersistentCollectionMap.get(schemaTable); - } - - public Collection> getDummyPersistentManyToManyCollection(String schemaTable) { - return dummyPersistentManyToManyCollectionMap.get(schemaTable); - } - - public Collection> getAllDummyPersistentManyToManyCollections() { - return new HashSet<>(dummyPersistentManyToManyCollectionMap.values()); - } - - /** - * Load all the data for a given class from the datasource(s) into the cache. - * This method will recursively load all dependencies of the given class. - * Note that calling this method registers a specific data type. - * Without calling this method, the data manager will have no knowledge of the data type. - * This should be called at the start of the application to ensure all data types are registered. - * This method is inherently blocking. - * - * @param clazz The class to load data for - * @param The type of data to load - * @return A list of all the loaded data - */ - @Blocking - public List loadAll(Class clazz) { - logger.debug("Registering: {}", clazz.getName()); - try { - // A dependency is just another UniqueData that is referenced in some way. - // This clazz is also treated as a dependency, these will all be loaded at the same time - Set> dependencies = new HashSet<>(); - extractDependencies(clazz, dependencies); - dependencies.removeIf(loadedDependencies::contains); - - List dummyHolders = new ArrayList<>(); - Multimap dummyDatabaseKeys = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); - Multimap> dummySimplePersistentCollections = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); - Multimap> dummyPersistentManyToManyCollections = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); - Multimap> dummyCachedValues = Multimaps.newSetMultimap(new HashMap<>(), HashSet::new); - - - for (Class dependency : dependencies) { - // A data dependency is some data field on one of our dependencies - List> dataDependencies = extractDataDependencies(dependency); - - dummyHolders.add(createInstance(dependency, null)); - - for (Data data : dataDependencies) { - DataKey key = data.getKey(); - if (key instanceof DatabaseKey dbKey) { - dummyDatabaseKeys.put(data.getHolder().getRootHolder(), dbKey); - } - - // This is our first time seeing this value, so we add it to the map for later use - if (data instanceof PersistentValue value) { - dummyValueMap.put(value.getSchema() + "." + value.getTable(), value); - } - - if (data instanceof Reference reference) { - PersistentValue value = reference.getBackingValue(); - dummyValueMap.put(value.getSchema() + "." + value.getTable(), value); - } - - if (data instanceof SimplePersistentCollection collection) { - dummySimplePersistentCollectionMap.put(collection.getSchema() + "." + collection.getTable(), collection); - dummySimplePersistentCollections.put(data.getHolder().getRootHolder(), collection); - } - - if (data instanceof PersistentManyToManyCollection collection) { - dummyPersistentManyToManyCollectionMap.put(collection.getSchema() + "." + collection.getJunctionTable(), collection); - dummyPersistentManyToManyCollections.put(data.getHolder().getRootHolder(), collection); - } - - if (data instanceof CachedValue cachedValue) { - dummyValueMap.put(cachedValue.getSchema() + "." + cachedValue.getTable(), cachedValue); - dummyCachedValues.put(data.getHolder().getRootHolder(), cachedValue); - } - } - } - - submitBlockingTask((connection, jedis) -> { - // Load all the unique data first - for (UniqueData dummyHolder : dummyHolders) { - loadUniqueData(connection, dummyHolder); - } - - //Load PersistentValues - for (UniqueData dummyHolder : dummyHolders) { - Collection keys = dummyDatabaseKeys.get(dummyHolder); - - // Use a multimap so we can easily group CellKeys together - // The actual key (DataKey) for this map is solely used for grouping entries with the same schema, table, data column, and id column - Multimap persistentDataColumns = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new); - - for (DatabaseKey key : keys) { - if (key instanceof CellKey cellKey) { - persistentDataColumns.put(new DataKey(cellKey.getSchema(), cellKey.getTable(), cellKey.getColumn(), cellKey.getIdColumn()), cellKey); - } - } - - for (DataKey key : persistentDataColumns.keySet()) { - persistentValueManager.loadAllFromDatabase(connection, dummyHolder, persistentDataColumns.get(key)); - } - } - - //Load SimplePersistentCollections - for (UniqueData dummyHolder : dummyHolders) { - Collection> dummyCollections = dummySimplePersistentCollections.get(dummyHolder); - - for (SimplePersistentCollection dummyCollection : dummyCollections) { - persistentCollectionManager.loadAllFromDatabase(connection, dummyHolder, dummyCollection); - } - } - - //Load PersistentManyToManyCollections - for (UniqueData dummyHolder : dummyHolders) { - Collection> dummyCollections = dummyPersistentManyToManyCollections.get(dummyHolder); - - for (PersistentManyToManyCollection dummyCollection : dummyCollections) { - persistentCollectionManager.loadJunctionTablesFromDatabase(connection, dummyCollection); - } - } - - //Load CachedValues - for (UniqueData dummyHolder : dummyHolders) { - Collection> dummyValues = dummyCachedValues.get(dummyHolder); - - for (CachedValue dummyValue : dummyValues) { - cachedValueManager.loadAllFromRedis(jedis, dummyValue); - } - } - }); - - loadedDependencies.addAll(dependencies); - } catch (Exception e) { - throw new RuntimeException(e); - } - - //to load data, we must first find a list of dependencies, and recursively grab their dependencies. after we've found all of them, we should load all data, and then we can establish relationships - -// try (Connection connection = getConnection()) { -// -// } catch (SQLException e) { -// throw new RuntimeException(e); -// } - - // All the entries we either just load or were previously loaded due to them being a dependency are now in the cache, so just return them via getAll() - return getAll(clazz); - } - - /** - * Create a new batch insert operation. - * - * @return The batch insert operation - */ - public final BatchInsert batchInsert() { - return new BatchInsert(this); - } - - /** - * Synchronously insert data into the datasource(s) and cache. - * - * @param batch The batch of data to insert - */ - @Blocking - public final void insertBatch(BatchInsert batch, List intermediateActions, List preInsertActions, List postInsertActions) { - submitBlockingTask((connection, jedis) -> { - connection.setAutoCommit(false); - for (InsertContext context : batch.getInsertionContexts()) { - insertIntoDataSource(connection, jedis, context); - } - for (ConnectionConsumer action : intermediateActions) { - action.accept(connection); - } - connection.setAutoCommit(true); - - for (InsertContext context : batch.getInsertionContexts()) { - insertIntoCache(context); - } - - for (Runnable action : preInsertActions) { - try { - action.run(); - } catch (Exception e) { - logger.error("Error running pre-insert action", e); - } - } - - for (Runnable action : postInsertActions) { - try { - action.run(); - } catch (Exception e) { - logger.error("Error running post-insert action", e); - } - } - }); - } - - /** - * Asynchronously insert data into the datasource(s) and cache. - * The data will be instantly inserted into the cache, and then the datasource(s) will be updated asynchronously. - * This method is non-blocking and will return immediately. - * The cached data can be accessed immediately after this method is called. - * - * @param batch The batch of data to insert - */ - public final void insertBatchAsync(BatchInsert batch, List intermediateActions, List preInsertActions, List postInsertActions) { - for (InsertContext context : batch.getInsertionContexts()) { - insertIntoCache(context); - } - for (Runnable action : preInsertActions) { - try { - action.run(); - } catch (Exception e) { - logger.error("Error running pre-insert action", e); - } - } - - submitAsyncTask((connection, jedis) -> { - connection.setAutoCommit(false); - for (InsertContext context : batch.getInsertionContexts()) { - insertIntoDataSource(connection, jedis, context); - } - for (ConnectionConsumer action : intermediateActions) { - action.accept(connection); - } - connection.setAutoCommit(true); - - for (Runnable action : postInsertActions) { - try { - action.run(); - } catch (Exception e) { - logger.error("Error running post-insert action", e); - } - } - }); - } - - /** - * Synchronously insert data into the datasource(s) and cache. - * - * @param holder The root holder of the data to insert - * @param initialData The initial data to insert - */ - @Blocking - public final void insert(UniqueData holder, InitialValue... initialData) { - InsertContext context = buildInsertContext(holder, initialData); - - submitBlockingTask((connection, jedis) -> { - insertIntoDataSource(connection, jedis, context); - insertIntoCache(context); - }); - } - - /** - * Asynchronously insert data into the datasource(s) and cache. - * The data will be instantly inserted into the cache, and then the datasource(s) will be updated asynchronously. - * This method is non-blocking and will return immediately. - * The cached data can be accessed immediately after this method is called. - * - * @param holder The root holder of the data to insert - * @param initialData The initial data to insert - */ - public final void insertAsync(UniqueData holder, InitialValue... initialData) { - InsertContext context = buildInsertContext(holder, initialData); - insertIntoCache(context); - - submitAsyncTask((connection, jedis) -> { - insertIntoDataSource(connection, jedis, context); - }); - } - - public InsertContext buildInsertContext(UniqueData holder, InitialValue... initialData) { - Map, InitialPersistentValue> initialPersistentValues = new HashMap<>(); - Map, InitialCachedValue> initialCachedValues = new HashMap<>(); - - for (Field field : ReflectionUtils.getFields(holder.getClass())) { - field.setAccessible(true); - - if (Data.class.isAssignableFrom(field.getType())) { - try { - Data data = (Data) field.get(holder); - if (data instanceof PersistentValue pv) { - initialPersistentValues.put(pv, new InitialPersistentValue(pv, pv.getDefaultValue())); - } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - - for (InitialValue data : initialData) { - if (data instanceof InitialPersistentValue initial) { - initialPersistentValues.put(initial.getValue(), initial); - } else if (data instanceof InitialCachedValue initial) { - if (initial.getInitialDataValue() != null) { - initialCachedValues.put(initial.getValue(), initial); - } - } else { - throw new IllegalArgumentException("Unsupported initial data type: " + data.getClass()); - } - } - - for (Map.Entry, InitialPersistentValue> initial : initialPersistentValues.entrySet()) { - PersistentValue pv = initial.getKey(); - InitialPersistentValue value = initial.getValue(); - - if (Primitives.isPrimitive(pv.getDataType()) && !Primitives.getPrimitive(pv.getDataType()).isNullable()) { - Preconditions.checkNotNull(value.getInitialDataValue(), "Initial data value cannot be null for primitive type: " + pv.getDataType()); - } - } - - for (Map.Entry, InitialCachedValue> initial : initialCachedValues.entrySet()) { - CachedValue cv = initial.getKey(); - InitialCachedValue value = initial.getValue(); - - if (Primitives.isPrimitive(cv.getDataType()) && !Primitives.getPrimitive(cv.getDataType()).isNullable()) { - Preconditions.checkNotNull(value.getInitialDataValue(), "Initial data value cannot be null for primitive type: " + cv.getDataType()); - } - } - - return new InsertContext(holder, initialPersistentValues, initialCachedValues); - } - - private void insertIntoCache(InsertContext context) { - addUniqueData(context.holder()); - //todo: REFACTOR: similar to deletions, delegate this to the managers - - List initialPersistentDataWrappers = new ArrayList<>(); - for (InitialPersistentValue data : context.initialPersistentValues().values()) { - InsertionStrategy insertionStrategy = data.getValue().getInsertionStrategy(); - - //do not call PersistentValueManager#updateCache since we need to cache both values - //before PersistentCollectionManager#handlePersistentValueCacheUpdated is called, otherwise we get unexpected behavior - boolean updateCache = !cache.containsKey(data.getValue().getKey()) || insertionStrategy == InsertionStrategy.OVERWRITE_EXISTING; - Object oldValue; - - try { - oldValue = get(data.getValue().getKey()); - if (oldValue == NULL_MARKER) { - oldValue = null; - } - } catch (DataDoesNotExistException e) { - oldValue = null; - } - initialPersistentDataWrappers.add(new InitialPersistentDataWrapper(data, updateCache, oldValue)); - - if (updateCache) { - cache(data.getValue().getKey(), data.getValue().getDataType(), data.getInitialDataValue(), Instant.now(), false); - } - } - - for (InitialPersistentDataWrapper wrapper : initialPersistentDataWrappers) { - InitialPersistentValue data = wrapper.data; - boolean updateCache = wrapper.updateCache; - Object oldValue = wrapper.oldValue; - UniqueData pvHolder = data.getValue().getHolder().getRootHolder(); - CellKey idColumn = new CellKey( - pvHolder.getSchema(), - pvHolder.getTable(), - pvHolder.getIdentifier().getColumn(), - pvHolder.getId(), - pvHolder.getIdentifier().getColumn() - ); - - //Alert the collection manager of this change so it can update what it's keeping track of - if (updateCache) { - persistentCollectionManager.handlePersistentValueCacheUpdated( - data.getValue().getSchema(), - data.getValue().getTable(), - data.getValue().getColumn(), - context.holder().getId(), - data.getValue().getIdColumn(), - oldValue, - data.getInitialDataValue() - ); - } - - persistentCollectionManager.handlePersistentValueCacheUpdated( - idColumn.getSchema(), - idColumn.getTable(), - idColumn.getColumn(), - pvHolder.getId(), - idColumn.getIdColumn(), - null, - pvHolder.getId() - ); - } - - for (InitialCachedValue data : context.initialCachedValues().values()) { - cache(data.getValue().getKey(), data.getValue().getDataType(), data.getInitialDataValue(), Instant.now(), false); - } - } - - private void insertIntoDataSource(Connection connection, Jedis jedis, InsertContext context) throws SQLException { - if (context.initialPersistentValues().isEmpty() && context.initialCachedValues().isEmpty()) { - String sql = "INSERT INTO " + context.holder().getSchema() + "." + context.holder().getTable() + " (" + context.holder().getIdentifier().getColumn() + ") VALUES (?)"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, context.holder().getId()); - statement.executeUpdate(); - } - return; - } - persistentValueManager.insertInDatabase(connection, context.holder(), new ArrayList<>(context.initialPersistentValues().values())); - cachedValueManager.setInRedis(jedis, new ArrayList<>(context.initialCachedValues().values())); - } - - /** - * Delete data from the datasource(s) and cache. - * This method will immediately delete the data from the cache, and then asynchronously delete the data from the datasource(s). - * This method is non-blocking and will return immediately after deleting the data from the cache. - * Data will be recursively deleted based off of the {@link DeletionStrategy} of each member of the data object. - * - * @param holder The root holder of the data to delete - */ - public void delete(UniqueData holder) { - DeleteContext context = buildDeleteContext(holder); - logger.trace("Deleting: {}", context); - deleteFromCache(context); - //todo: i really dislike that update handlers are called when things are deleted. revisit this - - submitAsyncTask((connection, jedis) -> deleteFromDataSource(connection, jedis, context)); - } - - /** - * Synchronously delete data from the datasource(s) and cache. - * Data will be recursively deleted based off of the {@link DeletionStrategy} of each member of the data object. - * This method is inherently blocking. - * - * @param holder The root holder of the data to delete - */ - @Blocking - public void deleteSync(UniqueData holder) { - DeleteContext context = buildDeleteContext(holder); - logger.trace("Deleting: {}", context); - deleteFromCache(context); - - submitBlockingTask((connection, jedis) -> deleteFromDataSource(connection, jedis, context)); - } - - private DeleteContext buildDeleteContext(UniqueData holder) { - Set> toDelete = new HashSet<>(); - Set holders = new HashSet<>(); - extractDataToDelete(holder, holders, toDelete); - Map oldValues = new HashMap<>(); - for (Data data : toDelete) { - if (data instanceof PersistentValue || data instanceof CachedValue) { - try { - Object value = get(data.getKey()); - oldValues.put(data.getKey(), value == NULL_MARKER ? null : value); - } catch (DataDoesNotExistException e) { - // This is fine, it just means the value was null - } - } - } - - return new DeleteContext(holders, toDelete, oldValues); - } - - private void extractDataToDelete(UniqueData holder, Set holders, Set> toDelete) { - if (holders.contains(holder)) { - return; - } - holders.add(holder); - - //Add the root holder's id column to the list of things to delete, just in case the holder is empty - toDelete.add(PersistentValue.of(holder.getRootHolder(), UUID.class, holder.getRootHolder().getIdentifier().getColumn())); - - for (Field field : ReflectionUtils.getFields(holder.getClass())) { - field.setAccessible(true); - - if (Data.class.isAssignableFrom(field.getType())) { - try { - Data data = (Data) field.get(holder); - - //Always delete the backing value since it's in the same table as the holder - if (data instanceof Reference reference) { - toDelete.add(reference.getBackingValue()); - } - - if (data.getDeletionStrategy() == DeletionStrategy.NO_ACTION) { - continue; - } - - if (data instanceof Reference reference) { - UUID id = reference.getForeignId(); - if (id != null) { - UniqueData foreignData = reference.get(); - if (foreignData != null) { - extractDataToDelete(foreignData, holders, toDelete); - } - } - } - - if (data instanceof PersistentUniqueDataCollection collection) { - if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) { - for (UniqueData dataInCollection : collection) { - extractDataToDelete(dataInCollection, holders, toDelete); - } - } - } - - if (data instanceof PersistentManyToManyCollection collection) { - if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) { - for (UniqueData dataInCollection : collection) { - extractDataToDelete(dataInCollection, holders, toDelete); - } - } - } - - toDelete.add(data); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - } - - private void deleteFromCache(DeleteContext context) { - persistentCollectionManager.deleteFromCache(context); - persistentValueManager.deleteFromCache(context); - cachedValueManager.deleteFromCache(context); - - for (UniqueData holder : context.holders()) { - removeUniqueData(holder.getClass(), holder.getId()); - } - } - - @Blocking - private void deleteFromDataSource(Connection connection, Jedis jedis, DeleteContext context) throws SQLException { - for (UniqueData holder : context.holders()) { - String sql = "DELETE FROM " + holder.getSchema() + "." + holder.getTable() + " WHERE " + holder.getIdentifier().getColumn() + " = ?"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, holder.getId()); - statement.executeUpdate(); - } - } - persistentValueManager.deleteFromDatabase(connection, context); - persistentCollectionManager.deleteFromDatabase(connection, context); - cachedValueManager.deleteFromRedis(jedis, context); - } - - /** - * Get all data of a given type from the cache. - * - * @param clazz The class of data to get - * @param The type of data to get - * @return A list of all the data of the given type in the cache - */ - public List getAll(Class clazz) { - return uniqueDataIds.get(clazz).stream().map(id -> get(clazz, id)).toList(); - } - - private void extractDependencies(Class clazz, @NotNull Set> dependencies) throws Exception { - if (dependencies.contains(clazz)) { - return; - } - - dependencies.add(clazz); - - if (!UniqueData.class.isAssignableFrom(clazz)) { - return; - } - - UniqueData dummy = createInstance(clazz, null); - dummyInstances.put(clazz, dummy); - - for (Field field : ReflectionUtils.getFields(clazz)) { - field.setAccessible(true); - - Class type = field.getType(); - - if (Data.class.isAssignableFrom(type)) { - Data data = (Data) field.get(dummy); - Class dataType = data.getDataType(); - - if (UniqueData.class.isAssignableFrom(dataType)) { - extractDependencies((Class) dataType, dependencies); - } - } - } - } - - private List> extractDataDependencies(Class clazz) throws Exception { - UniqueData dummy = createInstance(clazz, null); - - List> dependencies = new ArrayList<>(); - - for (Field field : ReflectionUtils.getFields(clazz)) { - field.setAccessible(true); - - Class type = field.getType(); - - if (Data.class.isAssignableFrom(type)) { - try { - Data data = (Data) field.get(dummy); - dependencies.add(data); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - - dummyUniqueDataMap.put(dummy.getSchema() + "." + dummy.getTable(), dummy); - - return dependencies; - } - - public synchronized void removeFromCacheIf(Predicate predicate) { - Set keysToRemove = cache.keySet().stream().filter(predicate).collect(Collectors.toSet()); - keysToRemove.forEach(cache::remove); - } - - /** - * Dump the internal cache to the log. - */ - public void dump() { - logger.debug("Dumping cache:"); - for (Map.Entry entry : cache.entrySet()) { - logger.debug("{} -> {}", entry.getKey(), entry.getValue().value()); - } - } - - private void loadUniqueData(Connection connection, UniqueData dummyHolder) throws SQLException { - String sql = "SELECT " + dummyHolder.getIdentifier().getColumn() + " FROM " + dummyHolder.getSchema() + "." + dummyHolder.getTable(); - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - ResultSet resultSet = statement.executeQuery(); - while (resultSet.next()) { - UUID id = resultSet.getObject(dummyHolder.getIdentifier().getColumn(), UUID.class); - UniqueData instance = createInstance(dummyHolder.getClass(), id); - addUniqueData(instance); - } - } - - logger.info("Loaded {} instances of {}", uniqueDataIds.get(dummyHolder.getClass()).size(), dummyHolder.getClass().getName()); - } - - /** - * Get a data object from the cache. - * - * @param data The data object to get - * @param The value type stored in the data object - * @return The data object from the cache - * @throws DataDoesNotExistException If the data object does not exist in the cache - */ - public T get(Data data) throws DataDoesNotExistException { - return get(data.getKey()); - } - - /** - * Get a data object from the cache. - * - * @param key The key of the data object to get - * @param The value type stored in the data object - * @return The data object from the cache - * @throws DataDoesNotExistException If the data object does not exist in the cache - */ - @SuppressWarnings("unchecked") - public T get(DataKey key) throws DataDoesNotExistException { - CacheEntry cacheEntry = cache.get(key); - - if (cacheEntry == null) { - throw new DataDoesNotExistException("Data does not exist in cache: " + key); - } - - Object value = cacheEntry.value(); - if (value == NULL_MARKER) { - return null; - } - - return (T) value; - } - - /** - * Get a {@link UniqueData} object from the cache. - * - * @param clazz The class of the data object to get - * @param id The id of the data object to get - * @param The type of data object to get - * @return The data object from the cache, or null if it does not exist - */ - public T get(Class clazz, UUID id) { - if (id == null) { - return null; - } - Map uniqueData = uniqueDataCache.get(clazz); - if (uniqueData != null) { - UniqueData data = uniqueData.get(id); - if (data != null) { - return clazz.cast(data); - } - } - - return null; - } - - public void addUniqueData(UniqueData data) { - logger.trace("Adding unique data to cache: {}({})", data.getClass(), data.getId()); - - CellKey idColumn = new CellKey( - data.getSchema(), - data.getTable(), - data.getIdentifier().getColumn(), - data.getId(), - data.getIdentifier().getColumn() - ); - cache(idColumn, UUID.class, data.getId(), Instant.now(), false); - - uniqueDataIds.put(data.getClass(), data.getId()); - uniqueDataCache.computeIfAbsent(data.getClass(), k -> new ConcurrentHashMap<>()).put(data.getId(), data); - } - - public void removeUniqueData(Class clazz, UUID id) { - try { - logger.trace("Removing unique data from cache: {}({})", clazz, id); - uniqueDataIds.remove(clazz, id); - uniqueDataCache.get(clazz).remove(id); - } catch (DataDoesNotExistException e) { - logger.trace("Data does not exist in cache: {}({})", clazz, id); - } - } - - /** - * Add an object to the cache. Null values are permitted. - * If an entry with the given key already exists in the cache, - * it will only be replaced if this value's instant is newer than the existing entry's instant. - * - * @param key The key to cache the value under - * @param valueDataType The type of the value - * @param value The value to cache - * @param instant The instant at which the value was set - */ - public void cache(DataKey key, Class valueDataType, T value, Instant instant, boolean callUpdateHandlers) { - if (value != null && !valueDataType.isInstance(value)) { - throw new IllegalArgumentException("Value is not of the correct type! Expected: " + valueDataType + ", got: " + value.getClass()); - } - if (Primitives.isPrimitive(valueDataType) && !Primitives.getPrimitive(valueDataType).isNullable()) { - Preconditions.checkNotNull(value, "Value cannot be null for primitive type: " + valueDataType); - } - - CacheEntry existing = cache.get(key); - - if (existing != null && existing.instant().isAfter(instant)) { - logger.trace("Not caching value: {} -> {}, existing entry is newer. {} vs {} (existing)", key, value, instant, existing.instant()); - return; - } else { - logger.trace("Caching value: {} -> {}", key, value); - } - - if (existing != null) { - logger.trace("Caching value to replace existing entry. Difference in instants: {} vs {} (existing)", instant, existing.instant()); - } - - cache.put(key, CacheEntry.of(Objects.requireNonNullElse(value, NULL_MARKER), instant)); - - Collection> updateHandlers = valueUpdateHandlers.get(key); - - Object oldValue = existing == null ? null : existing.value() == NULL_MARKER ? null : existing.value(); - Object newValue = value == NULL_MARKER ? null : value; - - if (Objects.equals(oldValue, newValue)) { - return; - } - - if (callUpdateHandlers) { - - for (ValueUpdateHandler updateHandler : updateHandlers) { - try { - updateHandler.unsafeHandle(oldValue, newValue); - } catch (Exception e) { - logger.error("Error handling value update. Key: {}", key, e); - } - } - } - } - - public int getCacheSize() { - return cache.size(); - } - - public void uncache(DataKey key) { - uncache(key, true); - } - - public void uncache(DataKey key, boolean removeUpdateHandlers) { - Collection> updateHandlers = valueUpdateHandlers.get(key); - CacheEntry existing = cache.get(key); - - for (ValueUpdateHandler updateHandler : updateHandlers) { - try { - updateHandler.unsafeHandle(existing.value(), null); - } catch (Exception e) { - logger.error("Error handling value update. Key: {}", key, e); - } - } - - cache.remove(key); - - if (removeUpdateHandlers) { - valueUpdateHandlers.removeAll(key); - } - } - - public void registerValueUpdateHandler(DataKey key, ValueUpdateHandler handler) { - valueUpdateHandlers.put(key, handler); - } - - public void registerSerializer(ValueSerializer serializer) { - if (Primitives.isPrimitive(serializer.getDeserializedType())) { - throw new IllegalArgumentException("Cannot register a serializer for a primitive type"); - } - - if (!Primitives.isPrimitive(serializer.getSerializedType())) { - throw new IllegalArgumentException("Cannot register a ValueSerializer that serializes to a non-primitive type"); - } - - - for (ValueSerializer v : valueSerializers) { - if (v.getDeserializedType().isAssignableFrom(serializer.getDeserializedType())) { - throw new IllegalArgumentException("A serializer for " + serializer.getDeserializedType() + " is already registered! (" + v.getClass() + ")"); - } - } - - valueSerializers.add(serializer); - } - - public boolean isSupportedType(Class type) { - if (Primitives.isPrimitive(type)) { - return true; - } - - for (ValueSerializer serializer : valueSerializers) { - if (serializer.getDeserializedType().isAssignableFrom(type)) { - return true; - } - } - - return false; - } - - public T deserialize(Class type, Object serialized) { - if (serialized == null) { - return null; - } - if (Primitives.isPrimitive(type)) { - return (T) serialized; - } - - for (ValueSerializer serializer : valueSerializers) { - if (serializer.getDeserializedType().isAssignableFrom(type)) { - return (T) serializer.unsafeDeserialize(serialized); - } - } - - throw new IllegalArgumentException("No serializer found for type: " + type); - } - - public Class getSerializedDataType(Class deserializedType) { - if (Primitives.isPrimitive(deserializedType)) { - return deserializedType; - } - - for (ValueSerializer serializer : valueSerializers) { - if (serializer.getDeserializedType().isAssignableFrom(deserializedType)) { - return serializer.getSerializedType(); - } - } - - throw new IllegalArgumentException("No serializer found for type: " + deserializedType); - } - - public Object serialize(T deserialized) { - if (deserialized == null) { - return null; - } - - if (Primitives.isPrimitive(deserialized.getClass())) { - return deserialized; - } - - for (ValueSerializer serializer : valueSerializers) { - if (serializer.getDeserializedType().isAssignableFrom(deserialized.getClass())) { - return serializer.unsafeSerialize(deserialized); - } - } - - throw new IllegalArgumentException("No serializer found for type: " + deserialized.getClass()); - } - - public Object decode(Class type, String value) { - if (Primitives.isPrimitive(type)) { - return Primitives.decode(type, value); - } - - ValueSerializer serializer = valueSerializers.stream() - .filter(s -> s.getDeserializedType().isAssignableFrom(type)) - .findFirst() - .orElseThrow(); - - Class serializedType = serializer.getSerializedType(); - - return Primitives.decode(serializedType, value); - } - - public UniqueData createInstance(Class clazz, UUID id) { - logger.trace("Creating instance of {} with id {}", clazz, id); - try { - Constructor constructor = clazz.getDeclaredConstructor(DataManager.class, UUID.class); - constructor.setAccessible(true); - return constructor.newInstance(this, id); - } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | - InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - public PostgresListener getPostgresListener() { - return pgListener; - } - - public String getIdColumn(Class clazz) { - if (!dummyInstances.containsKey(clazz)) { - try { - dummyInstances.put(clazz, createInstance(clazz, null)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - return dummyInstances.get(clazz).getIdentifier().getColumn(); - } - - /** - * Block the calling thread until all previously enqueued tasks have been completed - */ - @Blocking - public void flushTaskQueue() { - //This will add a task to the queue and block until it's done - submitBlockingTask(connection -> { - //Ignore - }); - } - - private record InitialPersistentDataWrapper(InitialPersistentValue data, boolean updateCache, Object oldValue) { - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/PersistentCollection.java b/oldsrc/og/java/net/staticstudios/data/PersistentCollection.java deleted file mode 100644 index d6ffdbbd..00000000 --- a/oldsrc/og/java/net/staticstudios/data/PersistentCollection.java +++ /dev/null @@ -1,187 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.data.Data; -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.data.collection.*; -import net.staticstudios.data.key.CollectionKey; -import net.staticstudios.data.util.BatchInsert; -import net.staticstudios.data.util.DeletionStrategy; -import org.jetbrains.annotations.Blocking; - -import java.sql.SQLException; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.function.Predicate; - -/** - * Represents a collection of data that is stored in a database. - * Depending on the implementation, this collection may represent a one-to-many or many-to-many relationship. - * - * @param The type of data that this collection stores. - */ -public interface PersistentCollection extends Collection, DataHolder, Data { - - /** - * Create a new persistent collection with a one-to-many relationship. - * - * @param holder The holder of this collection. - * @param dataType The type of data that this collection stores (must not be a subclass of {@link UniqueData}), see {@link #oneToMany} for one-to-many relationships with {@link UniqueData}. - * @param schema The schema of the table that this collection stores data in. - * @param table The table that this collection stores data in. - * @param linkingColumn The column in the collection's table that links the data to the holder. - * @param dataColumn The column in the collection's table that stores the data itself. - * @param The type of data that this collection stores (must not be a subclass of {@link UniqueData}), see {@link #oneToMany} for one-to-many relationships with {@link UniqueData}. - * @return The persistent collection. - */ - static SimplePersistentCollection of(DataHolder holder, Class dataType, String schema, String table, String linkingColumn, String dataColumn) { - return new PersistentValueCollection<>(holder, dataType, schema, table, "id", linkingColumn, dataColumn); - } - - /** - * Create a new persistent collection with a one-to-many relationship. - * - * @param holder The holder of this collection. - * @param dataType The type of data that this collection stores (must be a subclass of {@link UniqueData}), see {@link #of} for one-to-many relationships with non-unique data. - * @param schema The schema of the table that this collection stores data in. - * @param table The table that this collection stores data in. - * @param entryIdColumn The column in the collection's table that holds the id of each entry. - * @param linkingColumn The column in the collection's table that links the data to the holder. - * @param dataColumn The column in the collection's table that stores the data itself. - * @param The type of data that this collection stores (must be a subclass of {@link UniqueData}), see {@link #of} for one-to-many relationships with non-unique data. - * @return The persistent collection. - */ - static SimplePersistentCollection of(DataHolder holder, Class dataType, String schema, String table, String entryIdColumn, String linkingColumn, String dataColumn) { - return new PersistentValueCollection<>(holder, dataType, schema, table, entryIdColumn, linkingColumn, dataColumn); - } - - /** - * Create a new persistent collection with a one-to-many relationship. - * - * @param holder The holder of this collection. - * @param dataType The type of data that this collection stores (must be a subclass of {@link UniqueData}), see {@link #of} for one-to-many relationships with non-unique data. - * @param schema The schema of the table that this collection stores data in. - * @param table The table that this collection stores data in. - * @param linkingColumn The column in the collection's table that links the data to the holder. - * @param The type of data that this collection stores (must be a subclass of {@link UniqueData}), see {@link #of} for one-to-many relationships with non-unique data. - * @return The persistent collection. - */ - static SimplePersistentCollection oneToMany(DataHolder holder, Class dataType, String schema, String table, String linkingColumn) { - return new PersistentUniqueDataCollection<>(holder, dataType, schema, table, linkingColumn, holder.getDataManager().getIdColumn(dataType)); - } - - /** - * Create a new persistent collection with a one-to-many relationship. - * - * @param holder The holder of this collection. - * @param dataType The type of data that this collection stores (must be a subclass of {@link UniqueData}) - * @param schema The schema of the junction table that links the data to the holder. - * @param junctionTable The junction table that links the data to the holder. - * @param thisIdColumn The column in the junction table that links the holder to the data. - * @param dataIdColumn The column in the junction table that links the data to the holder. - * @param The type of data that this collection stores (must be a subclass of {@link UniqueData}) - * @return The persistent collection. - */ - static PersistentCollection manyToMany(DataHolder holder, Class dataType, String schema, String junctionTable, String thisIdColumn, String dataIdColumn) { - return new PersistentManyToManyCollection<>(holder, dataType, schema, junctionTable, thisIdColumn, dataIdColumn); - } - - @Blocking - default void addBatch(BatchInsert batch, List toAdd) { - throw new UnsupportedOperationException("Batch insert is not supported for this collection type."); - } - - /** - * Similar to {@link #add(Object)}, but this method is blocking and will wait for the database update to complete. - * - * @param t The element to add. - * @return true if the element was added, false otherwise. - */ - @Blocking - boolean addNow(T t); - - /** - * Similar to {@link #addAll(Collection)}, but this method is blocking and will wait for the database update to complete. - * - * @param c The elements to add. - * @return true if the elements were added, false otherwise. - */ - @Blocking - boolean addAllNow(java.util.Collection c); - - /** - * Similar to {@link #remove(Object)}, but this method is blocking and will wait for the database update to complete. - * - * @param t The element to remove. - * @return true if the element was removed, false otherwise. - */ - @Blocking - boolean removeNow(T t); - - /** - * Similar to {@link #removeAll(Collection)}, but this method is blocking and will wait for the database update to complete. - * - * @param c The elements to remove. - * @return true if the elements were removed, false otherwise. - */ - @Blocking - boolean removeAllNow(java.util.Collection c); - - /** - * Similar to {@link #clear()}, but this method is blocking and will wait for the database update to complete. - */ - @Blocking - void clearNow(); - - /** - * Create a blocking iterator for this collection. - * When calling {@link Iterator#remove()}, a blocking remove operation will be performed. - * The remove operation will wait for the database update to complete. - * - * @return The blocking iterator. - */ - @Blocking - Iterator blockingIterator(); - - /** - * Register a handler to be called when an element is added to this collection. - * Note that handlers are called asynchronously. - * - * @param handler The handler to call. - * @return This collection. - */ - PersistentCollection onAdd(PersistentCollectionChangeHandler handler); - - /** - * Register a handler to be called when an element is removed from this collection. - * Note that handlers are called asynchronously. - * - * @param handler The handler to call. - * @return This collection. - */ - PersistentCollection onRemove(PersistentCollectionChangeHandler handler); - - CollectionKey getKey(); - - PersistentCollection deletionStrategy(DeletionStrategy strategy); - - /** - * A blocking version of {@link #removeIf(Predicate)}. - * This method will wait for the database update to complete. - * - * @param filter The filter to apply. - * @return true if any elements were removed, false otherwise. - */ - @Blocking - default boolean removeIfNow(Predicate filter) throws SQLException { - boolean removed = false; - Iterator each = blockingIterator(); - while (each.hasNext()) { - if (filter.test(each.next())) { - each.remove(); - removed = true; - } - } - return removed; - } -} \ No newline at end of file diff --git a/oldsrc/og/java/net/staticstudios/data/PersistentValue.java b/oldsrc/og/java/net/staticstudios/data/PersistentValue.java deleted file mode 100644 index 2e51d70e..00000000 --- a/oldsrc/og/java/net/staticstudios/data/PersistentValue.java +++ /dev/null @@ -1,343 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.data.value.InitialPersistentValue; -import net.staticstudios.data.data.value.Value; -import net.staticstudios.data.impl.PersistentValueManager; -import net.staticstudios.data.key.CellKey; -import net.staticstudios.data.util.DeletionStrategy; -import net.staticstudios.data.util.InsertionStrategy; -import net.staticstudios.data.util.ValueUpdate; -import net.staticstudios.data.util.ValueUpdateHandler; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.Blocking; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Supplier; - -/** - * Represents a value that is stored in a database table. - * - * @param The type of data that this value stores. - */ -public class PersistentValue implements Value { - private final String schema; - private final String table; - private final String column; - private final Class dataType; - private final DataHolder holder; - private final String idColumn; - private final DataManager dataManager; - private Supplier defaultValueSupplier; - private DeletionStrategy deletionStrategy; - private InsertionStrategy insertionStrategy; - private int updateInterval = -1; - - private PersistentValue(String schema, String table, String column, String idColumn, Class dataType, DataHolder holder, DataManager dataManager) { - if (!holder.getDataManager().isSupportedType(dataType)) { - throw new IllegalArgumentException("Unsupported data type: " + dataType); - } - - this.schema = schema; - this.table = table; - this.column = column; - this.dataType = dataType; - this.holder = holder; - this.idColumn = idColumn; - this.dataManager = dataManager; - } - - /** - * Create a new {@link PersistentValue} object. - * - * @param holder The holder of this value. - * @param dataType The type of data that this value stores. - * @param column The column in the holder's table that stores this value. - * @param The type of data that this value stores. - * @return The persistent value. - */ - public static PersistentValue of(UniqueData holder, Class dataType, String column) { - PersistentValue pv = new PersistentValue<>(holder.getSchema(), holder.getTable(), column, holder.getRootHolder().getIdentifier().getColumn(), dataType, holder, holder.getDataManager()); - pv.deletionStrategy = DeletionStrategy.CASCADE; - return pv; - } - - /** - * Create a new {@link PersistentValue} object that stores a value in a different table than its holder. - * By default, this {@link PersistentValue} will have a deletion strategy of {@link DeletionStrategy#NO_ACTION} and - * an insertion strategy of {@link InsertionStrategy#PREFER_EXISTING}. - * - * @param holder The holder of this value. - * @param dataType The type of data that this value stores. - * @param schemaTableColumn The schema.table.column that stores this value. - * @param foreignIdColumn The column in the holder's table that stores the foreign key. - * @param The type of data that this value stores. - * @return The persistent value. - */ - public static PersistentValue foreign(UniqueData holder, Class dataType, String schemaTableColumn, String foreignIdColumn) { - String[] parts = schemaTableColumn.split("\\."); - if (parts.length != 3) { - throw new IllegalArgumentException("Invalid schema.table.column format: " + schemaTableColumn); - } - PersistentValue pv = new PersistentValue<>(parts[0], parts[1], parts[2], foreignIdColumn, dataType, holder, holder.getDataManager()); - pv.deletionStrategy = DeletionStrategy.NO_ACTION; - pv.insertionStrategy = InsertionStrategy.PREFER_EXISTING; - return pv; - } - - /** - * Create a new {@link PersistentValue} object that stores a value in a different table than its holder. - * By default, this {@link PersistentValue} will have a deletion strategy of {@link DeletionStrategy#NO_ACTION} and - * an insertion strategy of {@link InsertionStrategy#PREFER_EXISTING}. - * - * @param holder The holder of this value. - * @param dataType The type of data that this value stores. - * @param schema The schema that stores the value. - * @param table The table that stores the value. - * @param column The column that stores the value. - * @param foreignIdColumn The column in the holder's table that stores the foreign key. - * @param The type of data that this value stores. - * @return The persistent value. - */ - public static PersistentValue foreign(UniqueData holder, Class dataType, String schema, String table, String column, String foreignIdColumn) { - PersistentValue pv = new PersistentValue<>(schema, table, column, foreignIdColumn, dataType, holder, holder.getDataManager()); - pv.deletionStrategy = DeletionStrategy.NO_ACTION; - pv.insertionStrategy = InsertionStrategy.PREFER_EXISTING; - return pv; - } - - /** - * Set the initial value for this object - * - * @param value the initial value, or null if there is no initial value - * @return the initial value object - */ - public InitialPersistentValue initial(@Nullable T value) { - return new InitialPersistentValue(this, value); - } - - /** - * Add an update handler to this value. - * Note that update handlers are run asynchronously. - * - * @param updateHandler the update handler - * @return this - */ - @SuppressWarnings("unchecked") - public PersistentValue onUpdate(ValueUpdateHandler updateHandler) { - dataManager.registerValueUpdateHandler(this.getKey(), update -> ThreadUtils.submit(() -> updateHandler.handle((ValueUpdate) update))); - return this; - } - - /** - * Set the default value for this value. - * - * @param defaultValue the default value, or null if there is no default value - * @return this - */ - public PersistentValue withDefault(@Nullable T defaultValue) { - this.defaultValueSupplier = () -> defaultValue; - return this; - } - - /** - * Set the default value for this value. - * - * @param defaultValueSupplier the default value supplier, or null if there is no default value - * @return this - */ - public PersistentValue withDefault(@Nullable Supplier<@Nullable T> defaultValueSupplier) { - this.defaultValueSupplier = defaultValueSupplier; - return this; - } - - /** - * Get the default value for this value. - * - * @return the default value, or null if there is no default value - */ - public @Nullable T getDefaultValue() { - return defaultValueSupplier == null ? null : defaultValueSupplier.get(); - } - - @Override - public CellKey getKey() { - return new CellKey(this); - } - - public T get() { - return getDataManager().get(this); - } - - public void set(T value) { - PersistentValueManager manager = dataManager.getPersistentValueManager(); - manager.updateCache(this, value); - - Runnable runnable = () -> dataManager.submitAsyncTask(connection -> manager.updateInDatabase(connection, this, value)); - - if (updateInterval > 0) { - manager.enqueueRunnable(getKey(), updateInterval, runnable); - } else { - runnable.run(); - } - } - - /** - * Set the value of this persistent value. - * This method will block until the value is set in the database. - * - * @param value the value to set - */ - @Blocking - public void setNow(T value) { - PersistentValueManager manager = dataManager.getPersistentValueManager(); - manager.updateCache(this, value); - - Runnable runnable = () -> dataManager.submitBlockingTask(connection -> manager.updateInDatabase(connection, this, value)); - - if (updateInterval > 0) { - manager.enqueueRunnable(getKey(), updateInterval, runnable); - } else { - runnable.run(); - } - } - - /** - * Get the schema that this value is stored in. - * - * @return the schema - */ - public String getSchema() { - return schema; - } - - /** - * Get the table that this value is stored in. - * - * @return the table - */ - public String getTable() { - return table; - } - - /** - * Get the column that this value is stored in. - * - * @return the column - */ - public String getColumn() { - return column; - } - - @Override - public Class getDataType() { - return dataType; - } - - /** - * Get the column that stores the id of this value. - * - * @return the id column - */ - public String getIdColumn() { - return idColumn; - } - - @Override - public DataManager getDataManager() { - return dataManager; - } - - @Override - public DataHolder getHolder() { - return holder; - } - - @Override - public PersistentValue deletionStrategy(DeletionStrategy strategy) { - if (holder.getRootHolder().getSchema().equals(schema) && holder.getRootHolder().getTable().equals(table)) { - throw new IllegalArgumentException("Cannot set deletion strategy for a PersistentValue in the same table as it's holder!"); - } - this.deletionStrategy = strategy; - return this; - } - - /** - * Set the insertion strategy for this value. - * - * @param strategy the insertion strategy - * @return this - * @throws IllegalArgumentException if the holder and value are in the same table - */ - public PersistentValue insertionStrategy(InsertionStrategy strategy) { - if (holder.getRootHolder().getSchema().equals(schema) && holder.getRootHolder().getTable().equals(table)) { - throw new IllegalArgumentException("Cannot set deletion strategy for a PersistentValue in the same table as it's holder!"); - } - this.insertionStrategy = strategy; - return this; - } - - @Override - public @NotNull DeletionStrategy getDeletionStrategy() { - return deletionStrategy == null ? DeletionStrategy.NO_ACTION : deletionStrategy; - } - - /** - * Get the insertion strategy for this value. - * - * @return the insertion strategy - */ - public @NotNull InsertionStrategy getInsertionStrategy() { - return insertionStrategy == null ? InsertionStrategy.OVERWRITE_EXISTING : insertionStrategy; - } - - /** - * Set the update interval for this value. - * When set to an integer greater than 0, the database will be updated every n milliseconds, provided the value has changed. - * Further, if the value has changed 10 times, only the 10th change will be written to the database. - * - * @param updateInterval the update interval - * @return this - */ - public PersistentValue updateInterval(int updateInterval) { - this.updateInterval = updateInterval; - return this; - } - - /** - * Get the update interval for this value. - * - * @return the update interval - */ - public int getUpdateInterval() { - return updateInterval; - } - - @Override - public int hashCode() { - return getKey().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - if (!(obj instanceof PersistentValue other)) { - return false; - } - - return getKey().equals(other.getKey()); - } - - @Override - public String toString() { - return "PersistentValue{" + - "schema='" + schema + '\'' + - ", table='" + table + '\'' + - ", column='" + column + '\'' + - '}'; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/Reference.java b/oldsrc/og/java/net/staticstudios/data/Reference.java deleted file mode 100644 index 43d609da..00000000 --- a/oldsrc/og/java/net/staticstudios/data/Reference.java +++ /dev/null @@ -1,220 +0,0 @@ -package net.staticstudios.data; - - -import net.staticstudios.data.data.Data; -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.data.value.InitialPersistentValue; -import net.staticstudios.data.key.DataKey; -import net.staticstudios.data.util.DataDoesNotExistException; -import net.staticstudios.data.util.DeletionStrategy; -import net.staticstudios.data.util.ValueUpdate; -import net.staticstudios.data.util.ValueUpdateHandler; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.UUID; - -/** - * Represents a one-to-one relationship between two {@link UniqueData} objects. - * - * @param The type of data that this reference points to. - */ -public class Reference implements DataHolder, Data { - private final DataHolder holder; - private final PersistentValue id; - private final Class clazz; - private DeletionStrategy deletionStrategy; - - private Reference(UniqueData holder, Class clazz, PersistentValue pv) { - this.holder = holder; - this.clazz = clazz; - this.id = pv; - this.deletionStrategy = DeletionStrategy.NO_ACTION; - } - - /** - * Create a reference to another {@link UniqueData} object. - * - * @param holder The holder of this reference. - * @param clazz The class of the data that this reference points to. - * @param foreignIdColumn The column in the holder's table that stores the foreign key, if the value is null then the reference is considered to be null. - * @param The type of data that this reference points to. - * @return The reference. - */ - public static Reference of(UniqueData holder, Class clazz, String foreignIdColumn) { - return new Reference<>(holder, clazz, PersistentValue.of(holder, UUID.class, foreignIdColumn)); - } - - /** - * Create a reference to another {@link UniqueData} object. - * - * @param holder The holder of this reference. - * @param clazz The class of the data that this reference points to. - * @param schemaTableForeignIdColumn The schema, table, and column in the holder's table that stores the foreign key to the referenced object, if the value is null then the reference is considered to be null. - * @param thisForeignIdColumn The column in the holder's table that stores the foreign key, to this holder. - * @param The type of data that this reference points to. - * @return The reference. - */ - public static Reference foreign(UniqueData holder, Class clazz, String schemaTableForeignIdColumn, String thisForeignIdColumn) { - return new Reference<>(holder, clazz, PersistentValue.foreign(holder, UUID.class, schemaTableForeignIdColumn, thisForeignIdColumn)); - } - - /** - * Adds an update handler for the backing id. - * This is useful if logic is needed when the Reference is set or unlinked. - * - * @param updateHandler the update handler - * @return this - */ - public Reference onUpdate(ValueUpdateHandler updateHandler) { - getBackingValue().onUpdate(updateHandler); - return this; - } - - @Override - public Reference deletionStrategy(DeletionStrategy strategy) { - this.deletionStrategy = strategy; - return this; - } - - @Override - public @NotNull DeletionStrategy getDeletionStrategy() { - return this.deletionStrategy == null ? DeletionStrategy.NO_ACTION : this.deletionStrategy; - } - - /** - * Set the foreign id of this reference. - * - * @param id The id of the foreign data, or null if the reference should be null. - */ - public void setForeignId(@Nullable UUID id) { - this.id.set(id); - } - - /** - * Get the foreign id of this reference. - * - * @return The id of the foreign data, or null if the reference is null. - */ - public @Nullable UUID getForeignId() { - try { - return id.get(); - } catch (DataDoesNotExistException e) { - return null; - } - } - - /** - * Set the initial value for this reference. - * - * @param data The data to set the reference to, or null if the reference should be null. - * @return An {@link InitialPersistentValue} object that can be used to insert the reference into the database. - */ - public InitialPersistentValue initial(T data) { - if (data == null) { - return id.initial(null); - } - return id.initial(data.getId()); - } - - /** - * Set the initial value for this reference. - * - * @param id The id of the data to set the reference to, or null if the reference should be null. - * @return An {@link InitialPersistentValue} object that can be used to insert the reference into the database. - */ - public InitialPersistentValue initial(UUID id) { - return this.id.initial(id); - } - - /** - * Set the value of this reference. - * - * @param data The data to set the reference to, or null if the reference should be null. - */ - public void set(@Nullable T data) { - if (data == null) { - id.set(null); - return; - } - - id.set(data.getId()); - } - - /** - * Get the data that this reference points to. - * - * @return The data that this reference points to, or null if the reference is null. - */ - public @Nullable T get() { - try { - return getDataManager().get(clazz, id.get()); - } catch (DataDoesNotExistException e) { - return null; - } - } - - @Override - public Class getDataType() { - return clazz; - } - - @Override - public DataManager getDataManager() { - return holder.getDataManager(); - } - - @Override - public DataKey getKey() { - return id.getKey(); - } - - @Override - public DataHolder getHolder() { - return holder; - } - - @Override - public UniqueData getRootHolder() { - return holder.getRootHolder(); - } - - - - /** - * Get the backing {@link PersistentValue} object that stores the foreign id. - * This is for internal use only. - * - * @return The backing {@link PersistentValue} object. - */ - public PersistentValue getBackingValue() { - return id; - } - - @Override - public String toString() { - return "Reference{" + - "dataType=" + clazz.getSimpleName() + - ", id=" + id.get() + - '}'; - } - - @Override - public int hashCode() { - return getKey().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - if (!(obj instanceof Reference other)) { - return false; - } - - return getKey().equals(other.getKey()); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/UniqueData.java b/oldsrc/og/java/net/staticstudios/data/UniqueData.java deleted file mode 100644 index e6dde3cc..00000000 --- a/oldsrc/og/java/net/staticstudios/data/UniqueData.java +++ /dev/null @@ -1,157 +0,0 @@ -package net.staticstudios.data; - - -import com.google.common.base.Preconditions; -import net.staticstudios.data.data.Data; -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.data.collection.SimplePersistentCollection; -import net.staticstudios.data.data.value.Value; -import net.staticstudios.data.key.UniqueIdentifier; -import net.staticstudios.data.util.ReflectionUtils; - -import java.lang.reflect.Field; -import java.util.Objects; -import java.util.UUID; - -/** - * Represents a unique data object that is stored in the database and contains other data objects. - */ -public abstract class UniqueData implements DataHolder { - private final DataManager dataManager; - private final String schema; - private final String table; - private final UniqueIdentifier identifier; - - /** - * Create a new unique data object. - * The id column is assumed to be "id". - * See {@link #UniqueData(DataManager, String, String, String, UUID)} if the id column is different. - * - * @param dataManager the data manager responsible for this data object - * @param schema the schema of the table - * @param table the table name - * @param id the id of the data object - */ - protected UniqueData(DataManager dataManager, String schema, String table, UUID id) { - this(dataManager, schema, table, "id", id); - } - - /** - * Create a new unique data object. - * - * @param dataManager the data manager responsible for this data object - * @param schema the schema of the table - * @param table the table name - * @param idColumn the name of the column that stores the id - * @param id the id of the data object - */ - protected UniqueData(DataManager dataManager, String schema, String table, String idColumn, UUID id) { - Preconditions.checkArgument(dataManager.get(this.getClass(), id) == null, "Data with id %s already exists", id); - this.dataManager = dataManager; - this.schema = schema; - this.table = table; - this.identifier = UniqueIdentifier.of(idColumn, id); - } - - /** - * Get the id of this data object. - * - * @return the id - */ - public UUID getId() { - return identifier.getId(); - } - - /** - * Get the table that this data object is stored in. - * - * @return the table name - */ - public String getTable() { - return table; - } - - /** - * Get the schema that this data object is stored in. - * - * @return the schema name - */ - public String getSchema() { - return schema; - } - - /** - * Get the unique identifier of this data object. - * - * @return the unique identifier - */ - public UniqueIdentifier getIdentifier() { - return identifier; - } - - @Override - public DataManager getDataManager() { - return dataManager; - } - - @Override - public UniqueData getRootHolder() { - return this; - } - - @Override - public final boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - UniqueData that = (UniqueData) obj; - return Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - UUID id = getId(); - return id != null ? id.hashCode() : 0; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(this.getClass().getSimpleName()); - sb.append("{"); - sb.append("id=").append(getId()).append("(").append(getSchema()).append(".").append(getTable()).append(".").append(identifier.getColumn()).append(")"); - - for (Field field : ReflectionUtils.getFields(this.getClass())) { - field.setAccessible(true); - Class fieldClass = field.getType(); - - if (!Data.class.isAssignableFrom(fieldClass)) { - continue; - } - - try { - Data data = (Data) field.get(this); - - if (data instanceof Value value) { - sb.append(", "); - sb.append(field.getName()).append("=").append(value.get()); - } else if (data instanceof SimplePersistentCollection collection) { - sb.append(", "); - sb.append(field.getName()).append("=Collection<").append(collection.getDataType().getSimpleName()).append(">{size=").append(collection.size()).append("}"); - } else if (data instanceof Reference reference) { - sb.append(", "); - UniqueData ref = reference.get(); - if (ref == null) { - sb.append(field.getName()).append("=null"); - } else { - sb.append(field.getName()).append("=").append(ref.getClass().getSimpleName()).append("{id=").append(ref.getId()).append("}"); - } - } - - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - sb.append("}"); - - return sb.toString(); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/ValueSerializer.java b/oldsrc/og/java/net/staticstudios/data/ValueSerializer.java deleted file mode 100644 index 68e1bf1d..00000000 --- a/oldsrc/og/java/net/staticstudios/data/ValueSerializer.java +++ /dev/null @@ -1,63 +0,0 @@ -package net.staticstudios.data; - -/** - * A serializer for non-primitive types. - * See {@link net.staticstudios.data.primative.Primitives} for primitive types. - * Nullability depends on the implementation. - * - * @param The deserialized type - * @param The serialized type - */ -public interface ValueSerializer { - /** - * Deserialize the serialized object - * - * @param serialized The serialized object - * @return The deserialized object - */ - D deserialize(S serialized); - - /** - * Serialize the deserialized object - * - * @param deserialized The deserialized object - * @return The serialized object - */ - S serialize(D deserialized); - - /** - * Get the deserialized type - * - * @return The deserialized type - */ - Class getDeserializedType(); - - /** - * Get the serialized type - * - * @return The serialized type - */ - Class getSerializedType(); - - /** - * Deserialize the serialized object without type checking. - * - * @param serialized The serialized object - * @return The deserialized object - */ - @SuppressWarnings("unchecked") - default D unsafeDeserialize(Object serialized) { - return deserialize((S) serialized); - } - - /** - * Serialize the deserialized object without type checking. - * - * @param deserialized The deserialized object - * @return The serialized object - */ - @SuppressWarnings("unchecked") - default S unsafeSerialize(Object deserialized) { - return serialize((D) deserialized); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/Data.java b/oldsrc/og/java/net/staticstudios/data/data/Data.java deleted file mode 100644 index f579c107..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/Data.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.staticstudios.data.data; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.key.DataKey; - -public interface Data extends Deletable { - - /** - * Get the data type of data being stored - * - * @return the data type - */ - Class getDataType(); - - /** - * Get the data manager that this data belongs to - * - * @return the data manager - */ - DataManager getDataManager(); - - /** - * Get the key for this data object - * - * @return the key - */ - DataKey getKey(); - - /** - * Get the holder for this data object - * - * @return the holder - */ - DataHolder getHolder(); -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/DataHolder.java b/oldsrc/og/java/net/staticstudios/data/data/DataHolder.java deleted file mode 100644 index 252b9689..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/DataHolder.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.staticstudios.data.data; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.UniqueData; - -public interface DataHolder { - - /** - * Get the data manager that this holder belongs to. - * - * @return The data manager. - */ - DataManager getDataManager(); - - /** - * Get the root holder of this data holder. - * - * @return The root holder, or this if this is the root holder. - */ - UniqueData getRootHolder(); -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/Deletable.java b/oldsrc/og/java/net/staticstudios/data/data/Deletable.java deleted file mode 100644 index 48dea729..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/Deletable.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.staticstudios.data.data; - -import net.staticstudios.data.util.DeletionStrategy; -import org.jetbrains.annotations.NotNull; - -public interface Deletable { - /** - * Set the deletion strategy for this object. - * - * @param strategy The deletion strategy to use. - * @return This object. - */ - Deletable deletionStrategy(DeletionStrategy strategy); - - /** - * Get the deletion strategy for this object. - * - * @return The deletion strategy. - */ - @NotNull DeletionStrategy getDeletionStrategy(); -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/InitialValue.java b/oldsrc/og/java/net/staticstudios/data/data/InitialValue.java deleted file mode 100644 index a6ee9b06..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/InitialValue.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.staticstudios.data.data; - -import net.staticstudios.data.data.value.Value; - -public interface InitialValue, T> { - V getValue(); - - T getInitialDataValue(); -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntry.java b/oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntry.java deleted file mode 100644 index f7132189..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntry.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.staticstudios.data.data.collection; - -import java.util.UUID; - -/** - * Represents an entry in a collection. - * - * @param id The id of the entry, note that this is not the same as the holder's id - * @param value The value of the entry - */ -public record CollectionEntry(UUID id, Object value) { -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java b/oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java deleted file mode 100644 index 87988712..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.staticstudios.data.data.collection; - -import com.google.common.base.Preconditions; - -import java.util.Objects; -import java.util.UUID; - -public class CollectionEntryIdentifier { - private final String entryIdColumn; - private final UUID entryId; - - public CollectionEntryIdentifier(String entryIdColumn, UUID entryId) { - this.entryIdColumn = Preconditions.checkNotNull(entryIdColumn); - this.entryId = entryId; //can be null for dummy instances - } - - public static CollectionEntryIdentifier of(String column, UUID value) { - return new CollectionEntryIdentifier(column, value); - } - - public String getEntryIdColumn() { - return entryIdColumn; - } - - public UUID getEntryId() { - return entryId; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - CollectionEntryIdentifier entryIdentifier = (CollectionEntryIdentifier) obj; - return this.entryIdColumn.equals(entryIdentifier.entryIdColumn) && this.entryId.equals(entryIdentifier.entryId); - } - - @Override - public int hashCode() { - return Objects.hash(entryIdColumn, entryId); - } - - @Override - public String toString() { - return "CollectionEntryIdentifier{" + - "entryIdColumn='" + entryIdColumn + '\'' + - ", entryId=" + entryId + - '}'; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java b/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java deleted file mode 100644 index 800d90f0..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java +++ /dev/null @@ -1,10 +0,0 @@ -package net.staticstudios.data.data.collection; - -public interface PersistentCollectionChangeHandler { - void onChange(T obj); - - @SuppressWarnings("unchecked") - default void unsafeOnChange(Object obj) { - onChange((T) obj); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java b/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java deleted file mode 100644 index 29f0610d..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java +++ /dev/null @@ -1,407 +0,0 @@ -package net.staticstudios.data.data.collection; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentCollection; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.impl.PersistentCollectionManager; -import net.staticstudios.data.key.CollectionKey; -import net.staticstudios.data.util.BatchInsert; -import net.staticstudios.data.util.DeletionStrategy; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.function.Consumer; - -public class PersistentManyToManyCollection implements PersistentCollection { - private final DataHolder holder; - private final Class dataType; - private final String schema; - private final String junctionTable; - private final String thisIdColumn; - private final String thatIdColumn; - private DeletionStrategy deletionStrategy; - - public PersistentManyToManyCollection(DataHolder holder, Class dataType, String schema, String junctionTable, String thisIdColumn, String thatIdColumn) { - this.holder = holder; - this.dataType = dataType; - this.schema = schema; - this.junctionTable = junctionTable; - this.thisIdColumn = thisIdColumn; - this.thatIdColumn = thatIdColumn; - this.deletionStrategy = DeletionStrategy.UNLINK; - } - - public String getSchema() { - return schema; - } - - public String getJunctionTable() { - return junctionTable; - } - - public String getThisIdColumn() { - return thisIdColumn; - } - - public String getThatIdColumn() { - return thatIdColumn; - } - - @Override - public void addBatch(BatchInsert batch, List holders) { - PersistentCollectionManager manager = getManager(); - List ids = holders.stream().map(UniqueData::getId).toList(); - - batch.early(() -> manager.addToJunctionTableInMemory(this, ids)); - batch.intermediate(connection -> manager.addToJunctionTableInDatabase(connection, this, ids)); - } - - @Override - public boolean addNow(T t) { - PersistentCollectionManager manager = getManager(); - manager.addToJunctionTableInMemory(this, Collections.singletonList(t.getId())); - getDataManager().submitBlockingTask(connection1 -> manager.addToJunctionTableInDatabase(connection1, this, Collections.singletonList(t.getId()))); - - return true; - } - - @Override - public boolean addAllNow(Collection c) { - List ids = new ArrayList<>(); - for (T t : c) { - ids.add(t.getId()); - } - - PersistentCollectionManager manager = getManager(); - manager.addToJunctionTableInMemory(this, ids); - getDataManager().submitBlockingTask(connection -> manager.addToJunctionTableInDatabase(connection, this, ids)); - - return true; - } - - @Override - public boolean removeNow(T t) { - PersistentCollectionManager manager = getManager(); - manager.removeFromJunctionTableInMemory(this, Collections.singletonList(t.getId())); - getDataManager().submitBlockingTask(connection -> manager.removeFromJunctionTableInDatabase(connection, this, Collections.singletonList(t.getId()))); - - return true; - } - - @Override - public boolean removeAllNow(Collection c) { - List ids = new ArrayList<>(); - for (T t : c) { - ids.add(t.getId()); - } - - PersistentCollectionManager manager = getManager(); - manager.removeFromJunctionTableInMemory(this, ids); - getDataManager().submitBlockingTask(connection -> manager.removeFromJunctionTableInDatabase(connection, this, ids)); - - return true; - } - - @Override - public void clearNow() { - PersistentCollectionManager manager = getManager(); - List ids = manager.getJunctionTableEntryIds(this); - manager.removeFromJunctionTableInMemory(this, ids); - getDataManager().submitBlockingTask(connection -> manager.removeFromJunctionTableInDatabase(connection, this, ids)); - } - - @Override - public Iterator blockingIterator() { - return new BlockingItr(getManager().getJunctionTableEntryIds(this).toArray(UUID[]::new)); - } - - @Override - public int size() { - return getManager().getJunctionTableEntryIds(this).size(); - } - - @Override - public boolean isEmpty() { - return size() == 0; - } - - @Override - public boolean contains(Object o) { - if (!dataType.isInstance(o)) { - return false; - } - - return getManager().getJunctionTableEntryIds(this).contains(((UniqueData) o).getId()); - } - - @Override - public @NotNull Iterator iterator() { - return new NonBlockingItr(getManager().getJunctionTableEntryIds(this).toArray(UUID[]::new)); - } - - @Override - public @NotNull Object[] toArray() { - return getManager().getJunctionTableEntryIds(this).stream().map(id -> getDataManager().get(dataType, id)).toArray(); - } - - @Override - public @NotNull T1[] toArray(@NotNull T1[] a) { - return getManager().getJunctionTableEntryIds(this).stream().map(id -> getDataManager().get(dataType, id)).toList().toArray(a); - } - - @Override - public boolean add(T t) { - PersistentCollectionManager manager = getManager(); - manager.addToJunctionTableInMemory(this, Collections.singletonList(t.getId())); - - getDataManager().submitAsyncTask(connection -> manager.addToJunctionTableInDatabase(connection, this, Collections.singletonList(t.getId()))); - - return true; - } - - @Override - public boolean remove(Object o) { - if (!dataType.isInstance(o)) { - return false; - } - - PersistentCollectionManager manager = getManager(); - manager.removeFromJunctionTableInMemory(this, Collections.singletonList(((UniqueData) o).getId())); - getDataManager().submitAsyncTask(connection -> manager.removeFromJunctionTableInDatabase(connection, this, Collections.singletonList(((UniqueData) o).getId()))); - - return true; - } - - @Override - public boolean containsAll(@NotNull Collection c) { - for (Object o : c) { - if (!dataType.isInstance(o)) { - return false; - } - } - - return new HashSet<>(getManager().getJunctionTableEntryIds(this)).containsAll(c.stream().map(o -> ((UniqueData) o).getId()).toList()); - } - - @Override - public boolean addAll(@NotNull Collection c) { - List ids = new ArrayList<>(); - for (T t : c) { - ids.add(t.getId()); - } - - PersistentCollectionManager manager = getManager(); - manager.addToJunctionTableInMemory(this, ids); - getDataManager().submitAsyncTask(connection -> manager.addToJunctionTableInDatabase(connection, this, ids)); - - return true; - } - - @Override - public boolean removeAll(@NotNull Collection c) { - List ids = new ArrayList<>(); - for (Object o : c) { - if (dataType.isInstance(o)) { - ids.add(((UniqueData) o).getId()); - } - } - - PersistentCollectionManager manager = getManager(); - manager.removeFromJunctionTableInMemory(this, ids); - getDataManager().submitAsyncTask(connection -> manager.removeFromJunctionTableInDatabase(connection, this, ids)); - - return true; - } - - @Override - public boolean retainAll(@NotNull Collection c) { - List ids = new ArrayList<>(); - for (Object o : c) { - if (dataType.isInstance(o)) { - ids.add(((UniqueData) o).getId()); - } - } - - List toRemove = new ArrayList<>(); - PersistentCollectionManager manager = getManager(); - for (UUID id : manager.getJunctionTableEntryIds(this)) { - if (!ids.contains(id)) { - toRemove.add(id); - } - } - - manager.removeFromJunctionTableInMemory(this, toRemove); - getDataManager().submitAsyncTask(connection -> manager.removeFromJunctionTableInDatabase(connection, this, toRemove)); - - return !toRemove.isEmpty(); - } - - @Override - public void clear() { - PersistentCollectionManager manager = getManager(); - List ids = manager.getJunctionTableEntryIds(this); - manager.removeFromJunctionTableInMemory(this, ids); - getDataManager().submitAsyncTask(connection -> manager.removeFromJunctionTableInDatabase(connection, this, ids)); - } - - @Override - public Class getDataType() { - return dataType; - } - - @Override - public CollectionKey getKey() { - return new CollectionKey(schema, junctionTable, thisIdColumn, thatIdColumn, holder.getRootHolder().getId()); - } - - @Override - public DataHolder getHolder() { - return holder; - } - - @Override - public DataManager getDataManager() { - return holder.getDataManager(); - } - - @Override - public UniqueData getRootHolder() { - return holder.getRootHolder(); - } - - private PersistentCollectionManager getManager() { - return getDataManager().getPersistentCollectionManager(); - } - - @Override - public PersistentCollection onAdd(PersistentCollectionChangeHandler handler) { - getDataManager().getPersistentCollectionManager().addAddHandler(this, id -> { - T t = getDataManager().get(getDataType(), (UUID) id); - ThreadUtils.submit(() -> handler.onChange(t)); - }); - return this; - } - - @Override - public PersistentCollection onRemove(PersistentCollectionChangeHandler handler) { - getDataManager().getPersistentCollectionManager().addRemoveHandler(this, id -> { - T t = getDataManager().get(getDataType(), (UUID) id); - ThreadUtils.submit(() -> handler.onChange(t)); - }); - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PersistentManyToManyCollection that = (PersistentManyToManyCollection) o; - return this.getKey().equals(that.getKey()); - } - - @Override - public int hashCode() { - return getKey().hashCode(); - } - - @Override - public PersistentManyToManyCollection deletionStrategy(DeletionStrategy strategy) { - this.deletionStrategy = strategy; - return this; - } - - @Override - public @NotNull DeletionStrategy getDeletionStrategy() { - return deletionStrategy == null ? DeletionStrategy.NO_ACTION : deletionStrategy; - } - - private class NonBlockingItr implements Iterator { - private final UUID[] ids; - int cursor; // index of next element to return - int lastRet = -1; // index of last element returned; -1 if no such - - public NonBlockingItr(UUID[] ids) { - this.ids = ids; - } - - public boolean hasNext() { - return cursor != ids.length; - } - - public T next() { - int i = cursor; - if (!hasNext()) - throw new NoSuchElementException(); - cursor = i + 1; - UUID id = ids[lastRet = i]; - return getDataManager().get(getDataType(), id); - } - - public void remove() { - if (lastRet < 0) - throw new IllegalStateException(); - UUID id = ids[lastRet]; - PersistentCollectionManager manager = getManager(); - manager.removeFromJunctionTableInMemory(PersistentManyToManyCollection.this, Collections.singletonList(id)); - getDataManager().submitAsyncTask(connection -> manager.removeFromJunctionTableInDatabase(connection, PersistentManyToManyCollection.this, Collections.singletonList(id))); - - lastRet = -1; - } - - @Override - public void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - for (int i = cursor; i < ids.length; i++) { - UUID id = ids[i]; - action.accept(getDataManager().get(getDataType(), id)); - } - cursor = ids.length; - } - } - - private class BlockingItr implements Iterator { - private final UUID[] ids; - int cursor; // index of next element to return - int lastRet = -1; // index of last element returned; -1 if no such - - public BlockingItr(UUID[] ids) { - this.ids = ids; - } - - public boolean hasNext() { - return cursor != ids.length; - } - - public T next() { - int i = cursor; - if (!hasNext()) - throw new NoSuchElementException(); - cursor = i + 1; - UUID id = ids[lastRet = i]; - return getDataManager().get(getDataType(), id); - } - - public void remove() { - if (lastRet < 0) - throw new IllegalStateException(); - UUID id = ids[lastRet]; - PersistentCollectionManager manager = getManager(); - manager.removeFromJunctionTableInMemory(PersistentManyToManyCollection.this, Collections.singletonList(id)); - getDataManager().submitBlockingTask(connection -> manager.removeFromJunctionTableInDatabase(connection, PersistentManyToManyCollection.this, Collections.singletonList(id))); - - lastRet = -1; - } - - @Override - public void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - for (int i = cursor; i < ids.length; i++) { - UUID id = ids[i]; - action.accept(getDataManager().get(getDataType(), id)); - } - cursor = ids.length; - } - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java b/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java deleted file mode 100644 index d96f2982..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java +++ /dev/null @@ -1,409 +0,0 @@ -package net.staticstudios.data.data.collection; - -import net.staticstudios.data.PersistentCollection; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.impl.PersistentCollectionManager; -import net.staticstudios.data.util.BatchInsert; -import net.staticstudios.data.util.DeletionStrategy; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.function.Consumer; - -public class PersistentUniqueDataCollection extends SimplePersistentCollection { - private final PersistentValueCollection holderIds; - - public PersistentUniqueDataCollection(DataHolder holder, Class dataType, String schema, String table, String linkingColumn, String dataColumn) { - super(holder, - dataType, - schema, - table, - //The entryIdColumn is the id column of the data type, so just create a dummy instance so we can get the id column name - holder.getDataManager().getIdColumn(dataType), - linkingColumn, - dataColumn); - holderIds = new PersistentValueCollection<>(holder, UUID.class, schema, table, getEntryIdColumn(), linkingColumn, dataColumn); - holderIds.deletionStrategy(DeletionStrategy.NO_ACTION); - this.deletionStrategy(DeletionStrategy.UNLINK); - } - - public PersistentValueCollection getHolderIds() { - return holderIds; - } - - @Override - public void addBatch(BatchInsert batch, List holders) { - PersistentCollectionManager manager = getDataManager().getPersistentCollectionManager(); - List entries = new ArrayList<>(); - for (T holder : holders) { - entries.add(new CollectionEntry(holder.getId(), holder.getId())); - } - - batch.early(() -> manager.addEntriesToCache(holderIds, entries)); - batch.intermediate(connection -> manager.addUniqueDataEntryToDatabase(connection, this, entries)); - } - - @Override - public DataHolder getHolder() { - return holderIds.getHolder(); - } - - @Override - public int size() { - return holderIds.size(); - } - - @Override - public boolean isEmpty() { - return holderIds.isEmpty(); - } - - @Override - public boolean contains(Object o) { - if (o instanceof DataHolder holder) { - return holderIds.contains(holder.getRootHolder().getId()); - } - - return false; - } - - @Override - public @NotNull Iterator iterator() { - return new NonBlockingItr(holderIds.toArray(new UUID[0])); - } - - @Override - public @NotNull Object[] toArray() { - Object[] objects = new Object[size()]; - int i = 0; - for (UUID id : holderIds) { - objects[i] = getDataManager().get(getDataType(), id); - i++; - } - - return objects; - } - - @SuppressWarnings("unchecked") - @Override - public T1 @NotNull [] toArray(T1[] a) { - int size = size(); - if (a.length < size) { - return (T1[]) Arrays.copyOf(toArray(), size, a.getClass()); - } - - System.arraycopy(toArray(), 0, a, 0, size); - if (a.length > size) { - a[size()] = null; - } - - return a; - } - - @Override - public boolean add(T t) { - PersistentCollectionManager manager = holderIds.getManager(); - List toAdd = Collections.singletonList(new CollectionEntry(t.getId(), t.getId())); - manager.addEntriesToCache(holderIds, toAdd); - getDataManager().submitAsyncTask(connection -> manager.addUniqueDataEntryToDatabase(connection, this, toAdd)); - - return true; - } - - @Override - public boolean remove(Object o) { - if (o instanceof DataHolder holder) { - UUID id = holder.getRootHolder().getId(); - if (holderIds.contains(id)) { - List toRemove = Collections.singletonList(id); - holderIds.getManager().removeFromUniqueDataCollectionInMemory(holderIds, toRemove); - getDataManager().submitAsyncTask(connection -> holderIds.getManager().removeFromUniqueDataCollectionInDatabase(connection, holderIds, toRemove)); - - return true; - } - } - - return false; - } - - @Override - public boolean containsAll(@NotNull Collection c) { - boolean containsAll = true; - List ids = new ArrayList<>(); - for (Object o : c) { - if (o instanceof DataHolder) { - ids.add(((DataHolder) o).getRootHolder().getId()); - } else { - containsAll = false; - break; - } - } - - return containsAll && holderIds.containsAll(ids); - } - - @Override - public boolean addAll(@NotNull Collection c) { - List toAdd = new ArrayList<>(); - for (T t : c) { - toAdd.add(new CollectionEntry(t.getId(), t.getId())); - } - - PersistentCollectionManager manager = holderIds.getManager(); - manager.addEntriesToCache(holderIds, toAdd); - getDataManager().submitAsyncTask(connection -> manager.addUniqueDataEntryToDatabase(connection, this, toAdd)); - - - return true; - } - - @Override - public boolean removeAll(@NotNull Collection c) { - List toRemove = new ArrayList<>(); - for (Object o : c) { - if (o instanceof DataHolder) { - if (holderIds.contains(((DataHolder) o).getRootHolder().getId())) { - toRemove.add(((DataHolder) o).getRootHolder().getId()); - } - } - } - - holderIds.getManager().removeFromUniqueDataCollectionInMemory(holderIds, toRemove); - getDataManager().submitAsyncTask(connection -> holderIds.getManager().removeFromUniqueDataCollectionInDatabase(connection, holderIds, toRemove)); - - return !toRemove.isEmpty(); - } - - @Override - public boolean retainAll(@NotNull Collection c) { - List ids = new ArrayList<>(); - for (Object o : c) { - if (o instanceof DataHolder) { - if (holderIds.contains(((DataHolder) o).getRootHolder().getId())) { - ids.add(((DataHolder) o).getRootHolder().getId()); - } - } - } - - List toRemove = new ArrayList<>(); - for (UUID id : holderIds) { - if (!ids.contains(id)) { - toRemove.add(id); - } - } - - holderIds.getManager().removeFromUniqueDataCollectionInMemory(holderIds, toRemove); - getDataManager().submitAsyncTask(connection -> holderIds.getManager().removeFromUniqueDataCollectionInDatabase(connection, holderIds, toRemove)); - - return !toRemove.isEmpty(); - } - - @Override - public void clear() { - List toRemove = new ArrayList<>(holderIds); - holderIds.getManager().removeFromUniqueDataCollectionInMemory(holderIds, toRemove); - getDataManager().submitAsyncTask(connection -> holderIds.getManager().removeFromUniqueDataCollectionInDatabase(connection, holderIds, toRemove)); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "{" + - "dataType=" + getDataType() + - ", holderIds=" + holderIds + - '}'; - } - - @Override - public boolean addNow(T t) { - PersistentCollectionManager manager = holderIds.getManager(); - List toAdd = Collections.singletonList(new CollectionEntry(t.getId(), t.getId())); - manager.addEntriesToCache(holderIds, toAdd); - getDataManager().submitBlockingTask(connection -> manager.addUniqueDataEntryToDatabase(connection, this, toAdd)); - return true; - } - - @Override - public boolean addAllNow(Collection c) { - List toAdd = new ArrayList<>(); - for (T t : c) { - toAdd.add(new CollectionEntry(t.getId(), t.getId())); - } - - PersistentCollectionManager manager = holderIds.getManager(); - manager.addEntriesToCache(holderIds, toAdd); - getDataManager().submitBlockingTask(connection -> manager.addUniqueDataEntryToDatabase(connection, this, toAdd)); - return true; - } - - @Override - public boolean removeNow(T t) { - UUID id = t.getId(); - if (holderIds.contains(id)) { - List toRemove = Collections.singletonList(id); - holderIds.getManager().removeFromUniqueDataCollectionInMemory(holderIds, toRemove); - getDataManager().submitBlockingTask(connection -> holderIds.getManager().removeFromUniqueDataCollectionInDatabase(connection, holderIds, toRemove)); - return true; - } - - return false; - } - - @Override - public boolean removeAllNow(Collection c) { - List toRemove = new ArrayList<>(); - for (T t : c) { - if (holderIds.contains(t.getId())) { - toRemove.add(t.getId()); - } - } - - holderIds.getManager().removeFromUniqueDataCollectionInMemory(holderIds, toRemove); - getDataManager().submitBlockingTask(connection -> holderIds.getManager().removeFromUniqueDataCollectionInDatabase(connection, holderIds, toRemove)); - return !toRemove.isEmpty(); - } - - @Override - public void clearNow() { - List toRemove = new ArrayList<>(holderIds); - holderIds.getManager().removeFromUniqueDataCollectionInMemory(holderIds, toRemove); - getDataManager().submitBlockingTask(connection -> holderIds.getManager().removeFromUniqueDataCollectionInDatabase(connection, holderIds, toRemove)); - } - - @Override - public Iterator blockingIterator() { - return new BlockingItr(holderIds.toArray(new UUID[0])); - } - - @Override - public PersistentCollection onAdd(PersistentCollectionChangeHandler handler) { - getDataManager().getPersistentCollectionManager().addAddHandler(this, id -> { - T t = getDataManager().get(getDataType(), (UUID) id); - ThreadUtils.submit(() -> handler.onChange(t)); - }); - return this; - } - - @Override - public PersistentCollection onRemove(PersistentCollectionChangeHandler handler) { - getDataManager().getPersistentCollectionManager().addRemoveHandler(this, id -> { - T t = getDataManager().get(getDataType(), (UUID) id); - ThreadUtils.submit(() -> handler.onChange(t)); - }); - return this; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - if (!super.equals(obj)) return false; - PersistentUniqueDataCollection that = (PersistentUniqueDataCollection) obj; - return holderIds.equals(that.holderIds); - } - - @Override - public int hashCode() { - return holderIds.hashCode(); - } - - @Override - public PersistentUniqueDataCollection deletionStrategy(DeletionStrategy strategy) { - holderIds.deletionStrategy(strategy); - return this; - } - - @Override - public @NotNull DeletionStrategy getDeletionStrategy() { - return holderIds.getDeletionStrategy(); - } - - private class NonBlockingItr implements Iterator { - private final UUID[] ids; - int cursor; // index of next element to return - int lastRet = -1; // index of last element returned; -1 if no such - - public NonBlockingItr(UUID[] ids) { - this.ids = ids; - } - - public boolean hasNext() { - return cursor != ids.length; - } - - public T next() { - int i = cursor; - if (!hasNext()) - throw new NoSuchElementException(); - cursor = i + 1; - UUID id = ids[lastRet = i]; - return getDataManager().get(getDataType(), id); - } - - public void remove() { - if (lastRet < 0) - throw new IllegalStateException(); - UUID id = ids[lastRet]; - PersistentCollectionManager manager = holderIds.getManager(); - manager.removeFromUniqueDataCollectionInMemory(holderIds, Collections.singletonList(id)); - getDataManager().submitAsyncTask(connection -> manager.removeFromUniqueDataCollectionInDatabase(connection, holderIds, Collections.singletonList(id))); - - lastRet = -1; - } - - @Override - public void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - for (int i = cursor; i < ids.length; i++) { - UUID id = ids[i]; - action.accept(getDataManager().get(getDataType(), id)); - } - cursor = ids.length; - } - } - - private class BlockingItr implements Iterator { - private final UUID[] ids; - int cursor; // index of next element to return - int lastRet = -1; // index of last element returned; -1 if no such - - public BlockingItr(UUID[] ids) { - this.ids = ids; - } - - public boolean hasNext() { - return cursor != ids.length; - } - - public T next() { - int i = cursor; - if (!hasNext()) - throw new NoSuchElementException(); - cursor = i + 1; - UUID id = ids[lastRet = i]; - return getDataManager().get(getDataType(), id); - } - - public void remove() { - if (lastRet < 0) - throw new IllegalStateException(); - UUID id = ids[lastRet]; - PersistentCollectionManager manager = holderIds.getManager(); - manager.removeFromUniqueDataCollectionInMemory(holderIds, Collections.singletonList(id)); - getDataManager().submitBlockingTask(connection -> manager.removeFromUniqueDataCollectionInDatabase(connection, holderIds, Collections.singletonList(id))); - - lastRet = -1; - } - - @Override - public void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - for (int i = cursor; i < ids.length; i++) { - UUID id = ids[i]; - action.accept(getDataManager().get(getDataType(), id)); - } - cursor = ids.length; - } - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java b/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java deleted file mode 100644 index 87fa8de7..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/collection/PersistentValueCollection.java +++ /dev/null @@ -1,374 +0,0 @@ -package net.staticstudios.data.data.collection; - -import net.staticstudios.data.PersistentCollection; -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.impl.PersistentCollectionManager; -import net.staticstudios.data.util.DeletionStrategy; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.function.Consumer; - -public class PersistentValueCollection extends SimplePersistentCollection { - private final DataHolder holder; - private DeletionStrategy deletionStrategy; - - public PersistentValueCollection(DataHolder holder, Class dataType, String schema, String table, String entryIdColumn, String linkingColumn, String dataColumn) { - super(holder, dataType, schema, table, entryIdColumn, linkingColumn, dataColumn); - if (!holder.getDataManager().isSupportedType(dataType)) { - throw new IllegalArgumentException("Unsupported data type: " + dataType); - } - - this.holder = holder; - } - - @Override - public DataHolder getHolder() { - return holder; - } - - @Override - public int size() { - return getManager().getEntryKeys(this).size(); - } - - @Override - public boolean isEmpty() { - return size() == 0; - } - - @Override - public boolean contains(Object o) { - return getInternalValues().contains(o); - } - - @Override - public @NotNull Iterator iterator() { - return new NonBlockingItr(getInternalValues().toArray()); - } - - @Override - public @NotNull Object[] toArray() { - return getInternalValues().toArray(); - } - - @Override - public @NotNull T1[] toArray(@NotNull T1[] a) { - return getInternalValues().toArray(a); - } - - @Override - public boolean add(T t) { - PersistentCollectionManager manager = getManager(); - List toAdd = Collections.singletonList(new CollectionEntry(UUID.randomUUID(), t)); - manager.addEntriesToCache(this, toAdd); - getDataManager().submitAsyncTask(connection -> manager.addValueEntryToDatabase(connection, this, toAdd)); - - return true; - } - - @Override - public boolean remove(Object o) { - PersistentCollectionManager manager = getManager(); - - for (CollectionEntry entry : manager.getCollectionEntries(this)) { - if (entry.value().equals(o)) { - List toRemove = Collections.singletonList(entry); - manager.removeEntriesFromCache(this, toRemove); - getDataManager().submitAsyncTask(connection -> manager.removeValueEntryFromDatabase(connection, this, toRemove)); - - return true; - } - } - - return false; - } - - @Override - public boolean containsAll(@NotNull Collection c) { - return getInternalValues().containsAll(c); - } - - @Override - public boolean addAll(@NotNull Collection c) { - PersistentCollectionManager manager = getManager(); - List toAdd = c.stream().map(value -> new CollectionEntry(UUID.randomUUID(), value)).toList(); - manager.addEntriesToCache(this, toAdd); - getDataManager().submitAsyncTask(connection -> manager.addValueEntryToDatabase(connection, this, toAdd)); - - return true; - } - - @Override - public boolean removeAll(@NotNull Collection c) { - boolean changed = false; - - PersistentCollectionManager manager = getManager(); - Collection entries = new ArrayList<>(manager.getCollectionEntries(this)); - List toRemove = new ArrayList<>(); - for (Object o : c) { - for (CollectionEntry entry : entries) { - if (entry.value().equals(o)) { - entries.remove(entry); - toRemove.add(entry); - changed = true; - break; - } - } - } - - manager.removeEntriesFromCache(this, toRemove); - getDataManager().submitAsyncTask(connection -> manager.removeValueEntryFromDatabase(connection, this, toRemove)); - - return changed; - } - - @Override - public boolean retainAll(@NotNull Collection c) { - boolean changed = false; - - PersistentCollectionManager manager = getManager(); - Collection entries = manager.getCollectionEntries(this); - List toRemove = new ArrayList<>(); - for (CollectionEntry entry : entries) { - if (!c.contains(entry.value())) { - toRemove.add(entry); - changed = true; - } - } - - manager.removeEntriesFromCache(this, toRemove); - getDataManager().submitAsyncTask(connection -> manager.removeValueEntryFromDatabase(connection, this, toRemove)); - - return changed; - } - - @Override - public void clear() { - PersistentCollectionManager manager = getManager(); - List toRemove = new ArrayList<>(manager.getCollectionEntries(this)); - manager.removeEntriesFromCache(this, toRemove); - getDataManager().submitAsyncTask(connection -> manager.removeValueEntryFromDatabase(connection, this, toRemove)); - } - - protected PersistentCollectionManager getManager() { - return getDataManager().getPersistentCollectionManager(); - } - - @SuppressWarnings("unchecked") - private Collection getInternalValues() { - return (Collection) getManager().getEntries(this); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PersistentValueCollection that = (PersistentValueCollection) o; - return getKey().equals(that.getKey()); - } - - @Override - public int hashCode() { - return Objects.hash(getKey()); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(this.getClass().getSimpleName()); - sb.append("{"); - for (T value : getInternalValues()) { - sb.append(value).append(", "); - } - - if (!getInternalValues().isEmpty()) { - sb.delete(sb.length() - 2, sb.length()); - } - - sb.append("}"); - - return sb.toString(); - } - - @Override - public boolean addNow(T t) { - PersistentCollectionManager manager = getManager(); - List toAdd = Collections.singletonList(new CollectionEntry(UUID.randomUUID(), t)); - manager.addEntriesToCache(this, toAdd); - getDataManager().submitBlockingTask(connection -> manager.addValueEntryToDatabase(connection, this, toAdd)); - - return true; - } - - @Override - public boolean addAllNow(Collection c) { - PersistentCollectionManager manager = getManager(); - List toAdd = c.stream().map(value -> new CollectionEntry(UUID.randomUUID(), value)).toList(); - manager.addEntriesToCache(this, toAdd); - getDataManager().submitBlockingTask(connection -> manager.addValueEntryToDatabase(connection, this, toAdd)); - - return true; - } - - @Override - public boolean removeNow(T t) { - PersistentCollectionManager manager = getManager(); - - for (CollectionEntry entry : manager.getCollectionEntries(this)) { - if (entry.value().equals(t)) { - List toRemove = Collections.singletonList(entry); - manager.removeEntriesFromCache(this, toRemove); - getDataManager().submitBlockingTask(connection -> manager.removeValueEntryFromDatabase(connection, this, toRemove)); - - return true; - } - } - - return false; - } - - @Override - public boolean removeAllNow(Collection c) { - boolean changed = false; - - PersistentCollectionManager manager = getManager(); - Collection entries = new ArrayList<>(manager.getCollectionEntries(this)); - List toRemove = new ArrayList<>(); - for (Object o : c) { - for (CollectionEntry entry : entries) { - if (entry.value().equals(o)) { - entries.remove(entry); - toRemove.add(entry); - changed = true; - break; - } - } - } - - manager.removeEntriesFromCache(this, toRemove); - getDataManager().submitBlockingTask(connection -> manager.removeValueEntryFromDatabase(connection, this, toRemove)); - - return changed; - } - - @Override - public void clearNow() { - PersistentCollectionManager manager = getManager(); - List toRemove = new ArrayList<>(manager.getCollectionEntries(this)); - manager.removeEntriesFromCache(this, toRemove); - getDataManager().submitBlockingTask(connection -> manager.removeValueEntryFromDatabase(connection, this, toRemove)); - } - - @Override - public Iterator blockingIterator() { - return new BlockingItr(getInternalValues().toArray()); - } - - @Override - @SuppressWarnings("unchecked") - public PersistentCollection onAdd(PersistentCollectionChangeHandler handler) { - getDataManager().getPersistentCollectionManager().addAddHandler(this, change -> ThreadUtils.submit(() -> handler.onChange((T) change))); - return this; - } - - @Override - @SuppressWarnings("unchecked") - public PersistentCollection onRemove(PersistentCollectionChangeHandler handler) { - getDataManager().getPersistentCollectionManager().addRemoveHandler(this, change -> ThreadUtils.submit(() -> handler.onChange((T) change))); - return this; - } - - @Override - public PersistentValueCollection deletionStrategy(DeletionStrategy strategy) { - this.deletionStrategy = strategy; - return this; - } - - @Override - public @NotNull DeletionStrategy getDeletionStrategy() { - return deletionStrategy == null ? DeletionStrategy.NO_ACTION : deletionStrategy; - } - - private class NonBlockingItr implements Iterator { - private final Object[] values; - int cursor; // index of next element to return - int lastRet = -1; // index of last element returned; -1 if no such - - public NonBlockingItr(Object[] values) { - this.values = values; - } - - public boolean hasNext() { - return cursor != values.length; - } - - @SuppressWarnings("unchecked") - public T next() { - int i = cursor; - if (!hasNext()) - throw new NoSuchElementException(); - cursor = i + 1; - return (T) values[lastRet = i]; - } - - public void remove() { - if (lastRet < 0) - throw new IllegalStateException(); - PersistentValueCollection.this.remove(values[lastRet]); - - lastRet = -1; - } - - @SuppressWarnings("unchecked") - @Override - public void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - for (int i = cursor; i < values.length; i++) { - action.accept((T) values[i]); - } - cursor = values.length; - } - } - - private class BlockingItr implements Iterator { - private final Object[] values; - int cursor; // index of next element to return - int lastRet = -1; // index of last element returned; -1 if no such - - public BlockingItr(Object[] values) { - this.values = values; - } - - public boolean hasNext() { - return cursor != values.length; - } - - @SuppressWarnings("unchecked") - public T next() { - int i = cursor; - if (!hasNext()) - throw new NoSuchElementException(); - cursor = i + 1; - return (T) values[lastRet = i]; - } - - public void remove() { - if (lastRet < 0) - throw new IllegalStateException(); - PersistentValueCollection.this.removeNow((T) values[lastRet]); - - lastRet = -1; - } - - @SuppressWarnings("unchecked") - @Override - public void forEachRemaining(Consumer action) { - Objects.requireNonNull(action); - for (int i = cursor; i < values.length; i++) { - action.accept((T) values[i]); - } - cursor = values.length; - } - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java b/oldsrc/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java deleted file mode 100644 index 545d60ff..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.staticstudios.data.data.collection; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentCollection; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.data.DataHolder; -import net.staticstudios.data.key.CollectionKey; - -import java.util.Arrays; - -public abstract class SimplePersistentCollection implements PersistentCollection { - private final DataHolder holder; - private final String schema; - private final String table; - private final String entryIdColumn; - private final String linkingColumn; - private final String dataColumn; - private final Class dataType; - - protected SimplePersistentCollection(DataHolder holder, Class dataType, String schema, String table, String entryIdColumn, String linkingColumn, String dataColumn) { - this.holder = holder; - this.schema = schema; - this.table = table; - this.entryIdColumn = entryIdColumn; - this.linkingColumn = linkingColumn; - this.dataColumn = dataColumn; - this.dataType = dataType; - } - - public String getSchema() { - return schema; - } - - public String getTable() { - return table; - } - - public String getEntryIdColumn() { - return entryIdColumn; - } - - public String getLinkingColumn() { - return linkingColumn; - } - - public String getDataColumn() { - return dataColumn; - } - - @Override - public UniqueData getRootHolder() { - return this.holder.getRootHolder(); - } - - @Override - public DataManager getDataManager() { - return this.holder.getDataManager(); - } - - @Override - public CollectionKey getKey() { - return new CollectionKey( - schema, - table, - linkingColumn, - dataColumn, - getRootHolder().getId() - ); - } - - @Override - public Class getDataType() { - return dataType; - } - - @Override - public String toString() { - return Arrays.toString(toArray()); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/value/InitialCachedValue.java b/oldsrc/og/java/net/staticstudios/data/data/value/InitialCachedValue.java deleted file mode 100644 index d651fb82..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/value/InitialCachedValue.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.staticstudios.data.data.value; - -import net.staticstudios.data.CachedValue; -import net.staticstudios.data.data.InitialValue; - -public class InitialCachedValue implements InitialValue, Object> { - private final CachedValue value; - private final Object initialDataValue; - - public InitialCachedValue(CachedValue value, Object initialDataValue) { - this.value = value; - this.initialDataValue = initialDataValue; - } - - public CachedValue getValue() { - return value; - } - - public Object getInitialDataValue() { - return initialDataValue; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java b/oldsrc/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java deleted file mode 100644 index fdb70db0..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/value/InitialPersistentValue.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.staticstudios.data.data.value; - -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.data.InitialValue; - -public class InitialPersistentValue implements InitialValue, Object> { - private final PersistentValue value; - private final Object initialDataValue; - - public InitialPersistentValue(PersistentValue value, Object initialDataValue) { - this.value = value; - this.initialDataValue = initialDataValue; - } - - public PersistentValue getValue() { - return value; - } - - public Object getInitialDataValue() { - return initialDataValue; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/data/value/Value.java b/oldsrc/og/java/net/staticstudios/data/data/value/Value.java deleted file mode 100644 index 925257ea..00000000 --- a/oldsrc/og/java/net/staticstudios/data/data/value/Value.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.staticstudios.data.data.value; - -import net.staticstudios.data.data.Data; -import org.jetbrains.annotations.Nullable; - -public interface Value extends Data { - /** - * Get the value of this object - * - * @return the value, or null if the value is not set - */ - T get(); - - /** - * Set the value of this object. - * Depending on the type of value, null may be a valid value. - * See {@link net.staticstudios.data.primative.Primitives} for more information. - * - * @param value the value to set - */ - void set(@Nullable T value); -} diff --git a/oldsrc/og/java/net/staticstudios/data/impl/CachedValueManager.java b/oldsrc/og/java/net/staticstudios/data/impl/CachedValueManager.java deleted file mode 100644 index b222ea91..00000000 --- a/oldsrc/og/java/net/staticstudios/data/impl/CachedValueManager.java +++ /dev/null @@ -1,141 +0,0 @@ -package net.staticstudios.data.impl; - -import net.staticstudios.data.CachedValue; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.data.Data; -import net.staticstudios.data.data.value.InitialCachedValue; -import net.staticstudios.data.key.RedisKey; -import net.staticstudios.data.primative.Primitives; -import net.staticstudios.data.util.DataSourceConfig; -import net.staticstudios.data.util.DeleteContext; -import net.staticstudios.utils.ShutdownStage; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.Blocking; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPubSub; - -import java.time.Instant; -import java.util.Arrays; -import java.util.List; -import java.util.Set; - -public class CachedValueManager { - private static final Logger logger = LoggerFactory.getLogger(CachedValueManager.class); - private final DataManager dataManager; - - public CachedValueManager(DataManager dataManager, DataSourceConfig ds) { - this.dataManager = dataManager; - - RedisListener listener = new RedisListener(); - Thread listenerThread = new Thread(() -> { - try (Jedis jedis = new Jedis(ds.redisHost(), ds.redisPort())) { - jedis.psubscribe(listener, Arrays.stream(RedisEvent.values()).map(e -> "__keyevent@0__:" + e.name().toLowerCase()).toArray(String[]::new)); - } - }); - listenerThread.start(); - - ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, () -> { - listener.punsubscribe(); - listenerThread.interrupt(); - }); - } - - public void deleteFromCache(DeleteContext context) { - for (Data data : context.toDelete()) { - if (data instanceof CachedValue cv) { - dataManager.uncache(cv.getKey(), false); - } - } - } - - @Blocking - public void deleteFromRedis(Jedis jedis, DeleteContext context) { - for (Data data : context.toDelete()) { - if (data instanceof CachedValue cv) { - jedis.del(cv.getKey().toString()); - logger.trace("Deleted {}", cv.getKey()); - } - } - } - - @Blocking - public void setInRedis(Jedis jedis, List initialData) { - if (initialData.isEmpty()) { - return; - } - - for (InitialCachedValue initial : initialData) { - RedisKey key = initial.getValue().getKey(); - if (initial.getInitialDataValue() == null) { - jedis.del(key.toString()); - logger.trace("Deleted {}", key); - continue; - } - Object serialized = dataManager.serialize(initial.getInitialDataValue()); - - if (initial.getValue().getExpirySeconds() > 0) { - jedis.setex(key.toString(), initial.getValue().getExpirySeconds(), Primitives.encode(serialized)); - logger.trace("Set {} to {} with expiry of {} seconds", key, serialized, initial.getValue().getExpirySeconds()); - } else { - jedis.set(key.toString(), Primitives.encode(serialized)); - logger.trace("Set {} to {}", key, serialized); - } - } - } - - - @Blocking - public void loadAllFromRedis(Jedis jedis, CachedValue dummyValue) { - Set matchedKeys = jedis.keys(dummyValue.getKey().toPartialKey()); - for (String matchedKey : matchedKeys) { - if (!RedisKey.isRedisKey(matchedKey)) { - continue; - } - String encoded = jedis.get(matchedKey); - - Object serialized = dataManager.decode(dummyValue.getDataType(), encoded); - Object deserialized = dataManager.deserialize(dummyValue.getDataType(), serialized); - dataManager.cache(RedisKey.fromString(matchedKey), dummyValue.getDataType(), deserialized, Instant.now(), false); - } - } - - enum RedisEvent { - SET, - DEL, - EXPIRED - } - - class RedisListener extends JedisPubSub { - @Override - public void onPMessage(String pattern, String channel, String key) { - logger.trace("Received message: {} on channel: {} with pattern: {}", key, channel, pattern); - String eventString = channel.split(":")[1]; - RedisEvent event = RedisEvent.valueOf(eventString.toUpperCase()); - if (!RedisKey.isRedisKey(key)) { - return; - } - RedisKey redisKey = RedisKey.fromString(key); - assert redisKey != null; - Instant now = Instant.now(); - - switch (event) { - case SET -> dataManager.submitAsyncTask((connection, jedis) -> - dataManager.getDummyValues(redisKey.getHolderSchema() + "." + redisKey.getHolderTable()).stream() - .filter(v -> v.getClass() == CachedValue.class) - .map(v -> (CachedValue) v) - .filter(v -> v.getKey().getIdentifyingKey().equals(redisKey.getIdentifyingKey())) - .findFirst() - .ifPresent(dummyValue -> { - String encoded = jedis.get(key); - Object serialized = dataManager.decode(dummyValue.getDataType(), encoded); - Object deserialized = dataManager.deserialize(dummyValue.getDataType(), serialized); - dataManager.cache(redisKey, dummyValue.getDataType(), deserialized, now, true); - }) - ); - case DEL, EXPIRED -> dataManager.uncache(redisKey, false); - } - } - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java b/oldsrc/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java deleted file mode 100644 index 5b4010fa..00000000 --- a/oldsrc/og/java/net/staticstudios/data/impl/PersistentCollectionManager.java +++ /dev/null @@ -1,1074 +0,0 @@ -package net.staticstudios.data.impl; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentCollection; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.data.Data; -import net.staticstudios.data.data.collection.*; -import net.staticstudios.data.impl.pg.PostgresListener; -import net.staticstudios.data.key.CellKey; -import net.staticstudios.data.key.CollectionKey; -import net.staticstudios.data.key.UniqueIdentifier; -import org.jetbrains.annotations.Blocking; -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.time.Instant; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -public class PersistentCollectionManager extends SQLLogger { - private final Logger logger = LoggerFactory.getLogger(PersistentCollectionManager.class); - private final DataManager dataManager; - private final PostgresListener pgListener; - private final Multimap> addHandlers; - private final Multimap> removeHandlers; - /** - * Maps a collection key to a list of data keys for each entry in the collection - */ - private final Multimap collectionEntryHolders; - private final Map junctionTables; - - public PersistentCollectionManager(DataManager dataManager, PostgresListener pgListener) { - this.dataManager = dataManager; - this.pgListener = pgListener; - this.collectionEntryHolders = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.addHandlers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); - this.removeHandlers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); - this.junctionTables = new ConcurrentHashMap<>(); - - pgListener.addHandler(notification -> { - // The PersistentValueManager will handle updating the main cache - // All we have to concern ourselves with is updating the collection entry holders - // Note about updates: we have to care about when the linking column changes, since that's what we use to identify the holder - - Collection> dummyCollections = dataManager.getDummyPersistentCollections(notification.getSchema() + "." + notification.getTable()); - switch (notification.getOperation()) { - case INSERT -> dummyCollections.forEach(dummyCollection -> { - String encodedLinkingId = notification.getData().newDataValueMap().get(dummyCollection.getLinkingColumn()); - if (encodedLinkingId == null) { - return; - } - - UUID linkingId = UUID.fromString(encodedLinkingId); - UUID entryId = UUID.fromString(notification.getData().newDataValueMap().get(dummyCollection.getEntryIdColumn())); - - if (dummyCollection instanceof PersistentValueCollection vc) { - String encoded = notification.getData().newDataValueMap().get(dummyCollection.getDataColumn()); - Object decoded = dataManager.decode(dummyCollection.getDataType(), encoded); - Object deserialized = dataManager.deserialize(dummyCollection.getDataType(), decoded); - - //Ensure that the entry data is there - //For other collections, the PersistentValueManager will handle this - addEntriesToCache(vc, List.of(new CollectionEntry(entryId, deserialized)), linkingId); - } else if (dummyCollection instanceof PersistentUniqueDataCollection udc) { - //Ensure that the entry data is there - //For other collections, the PersistentValueManager will handle this - addEntriesToCache(udc.getHolderIds(), List.of(new CollectionEntry(entryId, entryId)), linkingId); - } - }); - case UPDATE -> dummyCollections.forEach(dummyCollection -> { - String linkingColumn = dummyCollection.getLinkingColumn(); - - String oldLinkingValue = notification.getData().oldDataValueMap().get(linkingColumn); - String newLinkingValue = notification.getData().newDataValueMap().get(linkingColumn); - - if (Objects.equals(oldLinkingValue, newLinkingValue)) { - return; - } - - UUID entryId = UUID.fromString(notification.getData().newDataValueMap().get(dummyCollection.getEntryIdColumn())); - - UUID oldLinkingId = oldLinkingValue == null ? null : UUID.fromString(oldLinkingValue); - UUID newLinkingId = newLinkingValue == null ? null : UUID.fromString(newLinkingValue); - if (oldLinkingId != null) { - CollectionKey collectionKey = new CollectionKey( - dummyCollection.getSchema(), - dummyCollection.getTable(), - linkingColumn, - dummyCollection.getDataColumn(), - UUID.fromString(oldLinkingValue) - ); - - - //Ensure that the entry data is gone - //For other collections, the PersistentValueManager will handle this - if (dummyCollection instanceof PersistentValueCollection) { - removeEntriesFromCache(collectionKey, dummyCollection, List.of(entryId)); - } else if (dummyCollection instanceof PersistentUniqueDataCollection persistentUniqueDataCollection) { - PersistentValueCollection dummyIdsCollection = persistentUniqueDataCollection.getHolderIds(); - CollectionKey idsCollectionKey = new CollectionKey( - dummyIdsCollection.getSchema(), - dummyIdsCollection.getTable(), - dummyIdsCollection.getLinkingColumn(), - dummyIdsCollection.getDataColumn(), - oldLinkingId - ); - removeFromUniqueDataCollectionInMemory(idsCollectionKey, dummyIdsCollection, List.of(entryId)); - } - } - - if (newLinkingId != null) { - if (dummyCollection instanceof PersistentValueCollection pvc) { - String encoded = notification.getData().newDataValueMap().get(dummyCollection.getDataColumn()); - Object decoded = dataManager.decode(dummyCollection.getDataType(), encoded); - Object deserialized = dataManager.deserialize(dummyCollection.getDataType(), decoded); - - //Ensure that the entry data is there - //For other collections, the PersistentValueManager will handle this - addEntriesToCache(pvc, List.of(new CollectionEntry(entryId, deserialized)), newLinkingId); - } else if (dummyCollection instanceof PersistentUniqueDataCollection udc) { - //Ensure that the entry data is there - //For other collections, the PersistentValueManager will handle this - addEntriesToCache(udc.getHolderIds(), List.of(new CollectionEntry(entryId, entryId)), newLinkingId); - } - } - }); - case DELETE -> dummyCollections.forEach(dummyCollection -> { - String encodedLinkingId = notification.getData().oldDataValueMap().get(dummyCollection.getLinkingColumn()); - if (encodedLinkingId == null) { - return; - } - - CollectionKey collectionKey = new CollectionKey( - dummyCollection.getSchema(), - dummyCollection.getTable(), - dummyCollection.getLinkingColumn(), - dummyCollection.getDataColumn(), - UUID.fromString(encodedLinkingId) - ); - UUID entryId = UUID.fromString(notification.getData().oldDataValueMap().get(dummyCollection.getEntryIdColumn())); - - //Ensure that the entry data is gone - //For other collections, the PersistentValueManager will handle this - if (dummyCollection instanceof PersistentValueCollection) { - removeEntriesFromCache(collectionKey, dummyCollection, List.of(entryId)); - } else if (dummyCollection instanceof PersistentUniqueDataCollection persistentUniqueDataCollection) { - PersistentValueCollection dummyIdsCollection = persistentUniqueDataCollection.getHolderIds(); - CollectionKey idsCollectionKey = new CollectionKey( - dummyIdsCollection.getSchema(), - dummyIdsCollection.getTable(), - dummyIdsCollection.getLinkingColumn(), - dummyIdsCollection.getDataColumn(), - UUID.fromString(encodedLinkingId) - ); - removeFromUniqueDataCollectionInMemory(idsCollectionKey, dummyIdsCollection, List.of(entryId)); - } - }); - } - }); - - //Handle junction table updates - pgListener.addHandler(notification -> { - String junctionTable = notification.getSchema() + "." + notification.getTable(); - JunctionTable jt = junctionTables.get(junctionTable); - if (jt == null) { - return; - } - - switch (notification.getOperation()) { - case INSERT -> { - List columns = notification.getData().newDataValueMap().keySet().stream().toList(); - UUID leftId = UUID.fromString(notification.getData().newDataValueMap().get(columns.get(0))); - UUID rightId = UUID.fromString(notification.getData().newDataValueMap().get(columns.get(1))); - - CollectionKey leftCollectionKey = new CollectionKey( - notification.getSchema(), - notification.getTable(), - columns.get(0), - columns.get(1), - leftId - ); - - CollectionKey rightCollectionKey = new CollectionKey( - notification.getSchema(), - notification.getTable(), - columns.get(1), - columns.get(0), - rightId - ); - - dataManager.getDummyPersistentManyToManyCollection(junctionTable).forEach(dummyCollection -> { - jt.add(dummyCollection.getThisIdColumn(), leftId, rightId); - }); - - callAddHandlers(leftCollectionKey, rightId); - callAddHandlers(rightCollectionKey, leftId); - } - case UPDATE -> { - List oldColumns = notification.getData().oldDataValueMap().keySet().stream().toList(); - UUID oldLeftId = UUID.fromString(notification.getData().oldDataValueMap().get(oldColumns.get(0))); - UUID oldRightId = UUID.fromString(notification.getData().oldDataValueMap().get(oldColumns.get(1))); - - List newColumns = notification.getData().newDataValueMap().keySet().stream().toList(); - UUID newLeftId = UUID.fromString(notification.getData().newDataValueMap().get(newColumns.get(0))); - UUID newRightId = UUID.fromString(notification.getData().newDataValueMap().get(newColumns.get(1))); - - CollectionKey oldLeftCollectionKey = new CollectionKey( - notification.getSchema(), - notification.getTable(), - oldColumns.get(0), - oldColumns.get(1), - oldLeftId - ); - - CollectionKey oldRightCollectionKey = new CollectionKey( - notification.getSchema(), - notification.getTable(), - oldColumns.get(1), - oldColumns.get(0), - oldRightId - ); - - CollectionKey newLeftCollectionKey = new CollectionKey( - notification.getSchema(), - notification.getTable(), - newColumns.get(0), - newColumns.get(1), - newLeftId - ); - - CollectionKey newRightCollectionKey = new CollectionKey( - notification.getSchema(), - notification.getTable(), - newColumns.get(1), - newColumns.get(0), - newRightId - ); - - callRemoveHandlers(oldLeftCollectionKey, oldRightId); - callRemoveHandlers(oldRightCollectionKey, oldLeftId); - - dataManager.getDummyPersistentManyToManyCollection(junctionTable).forEach(dummyCollection -> { - jt.remove(dummyCollection.getThisIdColumn(), oldLeftId, oldRightId); - jt.add(dummyCollection.getThisIdColumn(), newLeftId, newRightId); - }); - - callAddHandlers(newLeftCollectionKey, newRightId); - callAddHandlers(newRightCollectionKey, newLeftId); - - } - case DELETE -> { - List columns = notification.getData().oldDataValueMap().keySet().stream().toList(); - UUID leftId = UUID.fromString(notification.getData().oldDataValueMap().get(columns.get(0))); - UUID rightId = UUID.fromString(notification.getData().oldDataValueMap().get(columns.get(1))); - - CollectionKey leftCollectionKey = new CollectionKey( - notification.getSchema(), - notification.getTable(), - columns.get(0), - columns.get(1), - leftId - ); - - CollectionKey rightCollectionKey = new CollectionKey( - notification.getSchema(), - notification.getTable(), - columns.get(1), - columns.get(0), - rightId - ); - - callRemoveHandlers(leftCollectionKey, rightId); - callRemoveHandlers(rightCollectionKey, leftId); - - dataManager.getDummyPersistentManyToManyCollection(junctionTable).forEach(dummyCollection -> { - jt.remove(dummyCollection.getThisIdColumn(), leftId, rightId); - }); - } - } - }); - } - - public void deleteFromCache(DeleteContext context) { - for (Data data : context.toDelete()) { - if (data instanceof PersistentValueCollection collection) { - removeEntriesFromCache(collection, getCollectionEntries(collection)); - } else if (data instanceof PersistentUniqueDataCollection collection) { - if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) { - removeEntriesFromCache(collection, getCollectionEntries(collection)); - } else if (collection.getDeletionStrategy() == DeletionStrategy.UNLINK) { - PersistentValueCollection ids = collection.getHolderIds(); - removeFromUniqueDataCollectionInMemory(ids, new ArrayList<>(ids)); - } - } else if (data instanceof PersistentManyToManyCollection collection) { - //Note that the individual entries are already in the deletion context via the data manager - removeFromJunctionTableInMemory(collection, getJunctionTableEntryIds(collection)); - } - } - - for (Data data : context.toDelete()) { - if (data instanceof PersistentValue pv) { - handlePersistentValueDeletionInMemory(pv); - } - } - } - - public void deleteFromDatabase(Connection connection, DeleteContext context) throws SQLException { - for (Data data : context.toDelete()) { - if (data instanceof PersistentValueCollection collection) { - String sql = "DELETE FROM " + collection.getSchema() + "." + collection.getTable() + " WHERE " + collection.getLinkingColumn() + " = ?"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, collection.getRootHolder().getId()); - statement.executeUpdate(); - } - } else if (data instanceof PersistentUniqueDataCollection collection) { - if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) { - String sql = "DELETE FROM " + collection.getSchema() + "." + collection.getTable() + " WHERE " + collection.getLinkingColumn() + " = ?"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, collection.getRootHolder().getId()); - statement.executeUpdate(); - } - } else if (collection.getDeletionStrategy() == DeletionStrategy.UNLINK) { - String sql = "UPDATE " + collection.getSchema() + "." + collection.getTable() + " SET " + collection.getLinkingColumn() + " = NULL WHERE " + collection.getLinkingColumn() + " = ?"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, collection.getRootHolder().getId()); - statement.executeUpdate(); - } - } - } else if (data instanceof PersistentManyToManyCollection collection) { - if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) { - UniqueData dummyInstance = dataManager.getDummyInstance(collection.getDataType()); - String sql = "DELETE FROM " + dummyInstance.getSchema() + "." + dummyInstance.getTable() + - " WHERE EXISTS ( SELECT 1 FROM " + collection.getSchema() + "." + collection.getJunctionTable() + - " WHERE " + collection.getSchema() + "." + collection.getJunctionTable() + "." + collection.getThisIdColumn() + " = ? AND " + - dummyInstance.getSchema() + "." + dummyInstance.getTable() + "." + dummyInstance.getIdentifier().getColumn() + - " = " + collection.getSchema() + "." + collection.getJunctionTable() + "." + collection.getThatIdColumn() + - ")"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, collection.getRootHolder().getId()); - statement.executeUpdate(); - } - } else if (collection.getDeletionStrategy() == DeletionStrategy.UNLINK) { - String sql = "DELETE FROM " + collection.getSchema() + "." + collection.getJunctionTable() + " WHERE " + collection.getThisIdColumn() + " = ?"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, collection.getRootHolder().getId()); - statement.executeUpdate(); - } - } - } else if (data instanceof PersistentValue pv) { - handlePersistentValueDeletionInDatabase(connection, context, pv); - } - } - } - - private void addEntry(CollectionKey key, CollectionEntryIdentifier identifier, boolean callAddHandlers) { - collectionEntryHolders.put(key, identifier); - - if (callAddHandlers) { - Object newValue = getEntry(key, identifier); - callAddHandlers(key, newValue); - } - } - - private void removeEntry(CollectionKey key, CollectionEntryIdentifier identifier, boolean callRemoveHandlers) { - if (callRemoveHandlers) { - Object oldValue = getEntry(key, identifier); - callRemoveHandlers(key, oldValue); - } - - //remove after handlers are called to avoid DDNEEs - collectionEntryHolders.remove(key, identifier); - } - - private void callAddHandlers(CollectionKey key, Object newValue) { - Collection> addHandlers = this.addHandlers.get(key); - addHandlers.forEach(handler -> { - try { - handler.unsafeOnChange(newValue); - } catch (Exception e) { - logger.error("Error while handling add event", e); - } - }); - } - - private void callRemoveHandlers(CollectionKey key, Object oldValue) { - Collection> removeHandlers = this.removeHandlers.get(key); - removeHandlers.forEach(handler -> { - try { - handler.unsafeOnChange(oldValue); - } catch (Exception e) { - logger.error("Error while handling remove event", e); - } - }); - } - - public void addAddHandler(PersistentCollection collection, PersistentCollectionChangeHandler handler) { - if (collection.getRootHolder().getId() == null) { - return; //Dummy collection - } - - addHandlers.put(collection.getKey(), handler); - } - - public void addRemoveHandler(PersistentCollection collection, PersistentCollectionChangeHandler handler) { - if (collection.getRootHolder().getId() == null) { - return; //Dummy collection - } - - removeHandlers.put(collection.getKey(), handler); - } - - private void handlePersistentValueDeletionInMemory(PersistentValue pv) { - //when a PV is uncached, if it is our linking column, we need to remove the entry from the collection - if (pv.getDataType() != UUID.class) { - return; - } - - try { - if (pv.get() == null) { - return; - } - } catch (DataDoesNotExistException e) { - return; - } - - UUID oldLinkingId = (UUID) pv.get(); - - dataManager.getDummyPersistentCollections(pv.getSchema() + "." + pv.getTable()).stream() - .filter(dummyCollection -> dummyCollection.getLinkingColumn().equals(pv.getColumn())) - .forEach(dummyCollection -> { - - CollectionEntryIdentifier oldIdentifier = CollectionEntryIdentifier.of(dummyCollection.getEntryIdColumn(), pv.getHolder().getRootHolder().getId()); - - CollectionKey oldCollectionKey = new CollectionKey( - dummyCollection.getSchema(), - dummyCollection.getTable(), - dummyCollection.getLinkingColumn(), - dummyCollection.getDataColumn(), - oldLinkingId - ); - - try { - getEntry(oldCollectionKey, oldIdentifier); - } catch (DataDoesNotExistException e) { - // Ignore, this means that the entry was already deleted - // This can happen if we have a cascade deletion and the entry was already deleted via one of collection deletions - return; - } - - removeEntry(oldCollectionKey, oldIdentifier, true); - logger.trace("Removed collection entry holder from map: {} -> {}", oldCollectionKey, oldIdentifier); - }); - - UniqueData holder = pv.getHolder().getRootHolder(); - dataManager.getAllDummyPersistentManyToManyCollections().stream() - .filter(dummyCollection -> dummyCollection.getDataType().equals(holder.getClass())) - .forEach(dummyCollection -> { - JunctionTable jt = junctionTables.get(dummyCollection.getSchema() + "." + dummyCollection.getJunctionTable()); - jt.removeIf(dummyCollection.getThisIdColumn(), (left, right) -> { - if (right.equals(holder.getId())) { - CollectionKey collectionKey = new CollectionKey( - dummyCollection.getSchema(), - dummyCollection.getJunctionTable(), - dummyCollection.getThisIdColumn(), - dummyCollection.getThatIdColumn(), - left - ); - - callRemoveHandlers(collectionKey, right); - return true; - } - return false; - }); - }); - } - - private void handlePersistentValueDeletionInDatabase(Connection connection, DeleteContext context, PersistentValue pv) { - //when a PV is uncached, if it is our linking column, we need to remove the entry from the collection - if (pv.getDataType() != UUID.class) { - return; - } - - UUID oldEntryId = (UUID) context.oldValues().get(pv.getKey()); - if (oldEntryId == null) { - return; - } - - UniqueData holder = pv.getHolder().getRootHolder(); - dataManager.getAllDummyPersistentManyToManyCollections().stream() - .filter(dummyCollection -> dummyCollection.getDataType().equals(holder.getClass())) - .forEach(dummyCollection -> { - String sql = "DELETE FROM " + dummyCollection.getSchema() + "." + dummyCollection.getJunctionTable() + " WHERE " + dummyCollection.getThatIdColumn() + " = ?"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, oldEntryId); - statement.executeUpdate(); - } catch (SQLException e) { - e.printStackTrace(); - } - }); - } - - public void handlePersistentValueCacheUpdated(String schema, String table, String column, UUID holderId, String idColumn, Object oldValue, Object newValue) { - if (Objects.equals(oldValue, newValue)) { - return; - } - if (oldValue != null && !oldValue.getClass().equals(UUID.class)) { - return; - } - - if (newValue != null && !newValue.getClass().equals(UUID.class)) { - return; - } - - logger.trace("Handling PersistentValue cache update: ({}.{}.{}) {} -> {}", schema, table, column, oldValue, newValue); - - UUID newLinkingId = (UUID) newValue; - UUID oldLinkingId = (UUID) oldValue; - - // We need to check if a collection exists for this pv and this pv is the linking column, if so then we need to update the collection entry holder - // This will add or remove the entry from the collection - - dataManager.getDummyPersistentCollections(schema + "." + table).stream() - .filter(dummyCollection -> dummyCollection.getLinkingColumn().equals(column)) - .forEach(dummyCollection -> { - - CellKey entryIdKey = new CellKey( - schema, - table, - dummyCollection.getEntryIdColumn(), - holderId, - idColumn - ); - - if (oldLinkingId != null) { - CollectionKey oldCollectionKey = new CollectionKey( - dummyCollection.getSchema(), - dummyCollection.getTable(), - dummyCollection.getLinkingColumn(), - dummyCollection.getDataColumn(), - oldLinkingId - ); - - UUID oldEntryId = dataManager.get(entryIdKey); - - CollectionEntryIdentifier oldIdentifier = CollectionEntryIdentifier.of(dummyCollection.getEntryIdColumn(), oldEntryId); - removeEntry(oldCollectionKey, oldIdentifier, true); - logger.trace("Removed collection entry holder from map: {} -> {}", dummyCollection.getKey(), oldIdentifier); - } - - if (newLinkingId != null) { - CollectionKey newCollectionKey = new CollectionKey( - dummyCollection.getSchema(), - dummyCollection.getTable(), - dummyCollection.getLinkingColumn(), - dummyCollection.getDataColumn(), - newLinkingId - ); - - UUID newEntryId = dataManager.get(entryIdKey); - - CollectionEntryIdentifier newIdentifier = CollectionEntryIdentifier.of(dummyCollection.getEntryIdColumn(), newEntryId); - addEntry(newCollectionKey, newIdentifier, true); - logger.trace("Added collection entry holder to map: {} -> {}", newCollectionKey, newIdentifier); - } - }); - } - - @Blocking - public void loadAllFromDatabase(Connection connection, UniqueData dummyHolder, SimplePersistentCollection dummyCollection) throws SQLException { - logger.trace("Loading all collection entries for {}", dummyCollection.getKey()); - String schemaTable = dummyCollection.getSchema() + "." + dummyCollection.getTable(); - pgListener.ensureTableHasTrigger(connection, schemaTable); - - Set columns = new HashSet<>(); - - String entryIdColumn = dummyCollection.getEntryIdColumn(); - String entryDataColumn = dummyCollection.getDataColumn(); - String collectionLinkingColumn = dummyCollection.getLinkingColumn(); - - columns.add(entryIdColumn); - columns.add(collectionLinkingColumn); - columns.add(entryDataColumn); - - StringBuilder sqlBuilder = new StringBuilder("SELECT "); - - for (String column : columns) { - sqlBuilder.append(column).append(", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - - sqlBuilder.append(" FROM ").append(schemaTable); - - String sql = sqlBuilder.toString(); - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - ResultSet resultSet = statement.executeQuery(); - - while (resultSet.next()) { - UUID entryId = resultSet.getObject(entryIdColumn, UUID.class); - UUID linkingId = resultSet.getObject(collectionLinkingColumn, UUID.class); - - CollectionKey collectionKey = new CollectionKey( - dummyCollection.getSchema(), - dummyCollection.getTable(), - dummyCollection.getLinkingColumn(), - dummyCollection.getDataColumn(), - linkingId - ); - - CollectionEntryIdentifier identifier = CollectionEntryIdentifier.of(dummyCollection.getEntryIdColumn(), entryId); - CellKey entryDataKey = getEntryDataKey(dummyCollection, entryId); - CellKey entryLinkKey = getEntryLinkingKey(dummyCollection, entryId); - - - // I ordered the logs like this so that the output is in the same order as it is elsewhere in this class - - logger.trace("Adding collection entry to {}", collectionKey); - logger.trace("Adding collection entry link to cache: {} -> {}", entryLinkKey, linkingId); - - if (entryIdColumn.equals(entryDataColumn)) { - // For PersistentValueCollection the entry id will be the data, since that's what we're interested in - dataManager.cache(entryDataKey, UUID.class, entryId, Instant.now(), false); - logger.trace("Adding collection entry data to cache: {} -> {}", entryDataKey, entryId); - } else { - // For PersistentUniqueDataCollection the entry id will be the data, since that's what we're interested in - Object serializedDataValue = resultSet.getObject(entryDataColumn, dataManager.getSerializedDataType(dummyCollection.getDataType())); - Object dataValue = dataManager.deserialize(dummyCollection.getDataType(), serializedDataValue); - dataManager.cache(entryDataKey, dummyCollection.getDataType(), dataValue, Instant.now(), false); - - logger.trace("Adding collection entry data to cache: {} -> {}", entryDataKey, dataValue); - } - - logger.trace("Adding collection entry holder to map: {} -> {}", collectionKey, identifier); - - dataManager.cache(entryLinkKey, UUID.class, linkingId, Instant.now(), false); - addEntry(collectionKey, identifier, false); - } - } - } - - public void addEntriesToCache(PersistentValueCollection collection, Collection entries) { - UniqueData holder = collection.getRootHolder(); - addEntriesToCache(collection, entries, holder.getId()); - } - - public void addEntriesToCache(PersistentValueCollection dummyCollection, Collection entries, UUID holderId) { - CollectionKey collectionKey = new CollectionKey( - dummyCollection.getSchema(), - dummyCollection.getTable(), - dummyCollection.getLinkingColumn(), - dummyCollection.getDataColumn(), - holderId - ); - - for (CollectionEntry entry : entries) { - CollectionEntryIdentifier identifier = CollectionEntryIdentifier.of(dummyCollection.getEntryIdColumn(), entry.id()); - CellKey entryDataKey = getEntryDataKey(dummyCollection, entry.id()); - CellKey entryLinkKey = getEntryLinkingKey(dummyCollection, entry.id()); - - logger.trace("Adding collection entry to {}", collectionKey); - logger.trace("Adding collection entry link to cache: {} -> {}", entryLinkKey, holderId); - logger.trace("Adding collection entry data to cache: {} -> {}", entryDataKey, entry.value()); - logger.trace("Adding collection entry holder to map: {} -> {}", collectionKey, identifier); - - dataManager.cache(entryLinkKey, UUID.class, holderId, Instant.now(), true); - dataManager.cache(entryDataKey, dummyCollection.getDataType(), entry.value(), Instant.now(), true); - addEntry(collectionKey, identifier, true); - } - } - - public void removeEntriesFromCache(SimplePersistentCollection collection, List entries) { - CollectionKey collectionKey = collection.getKey(); - removeEntriesFromCache(collectionKey, collection, entries.stream().map(CollectionEntry::id).toList()); - } - - public void removeEntriesFromCache(CollectionKey collectionKey, SimplePersistentCollection dummyCollection, List entryIds) { - for (UUID entryId : entryIds) { - removeEntry(collectionKey, CollectionEntryIdentifier.of(dummyCollection.getEntryIdColumn(), entryId), true); - dataManager.uncache(getEntryDataKey(dummyCollection, entryId)); - dataManager.uncache(getEntryLinkingKey(dummyCollection, entryId)); - } - } - - private CellKey getEntryDataKey(SimplePersistentCollection collection, UUID entryId) { - return new CellKey(collection.getSchema(), collection.getTable(), collection.getDataColumn(), entryId, collection.getEntryIdColumn()); - } - - public CellKey getEntryLinkingKey(SimplePersistentCollection collection, UUID entryId) { - return new CellKey(collection.getSchema(), collection.getTable(), collection.getLinkingColumn(), entryId, collection.getRootHolder().getIdentifier().getColumn()); - } - - public List getEntryKeys(SimplePersistentCollection collection) { - return collectionEntryHolders.get(collection.getKey()).stream().map(k -> getEntryDataKey(collection, k.getEntryId())).toList(); - } - - public List getEntries(SimplePersistentCollection collection) { - return getEntryKeys(collection).stream().map(dataManager::get).toList(); - } - - public Object getEntry(CollectionKey collectionKey, CollectionEntryIdentifier identifier) { - CellKey entryDataKey = new CellKey(collectionKey.getSchema(), collectionKey.getTable(), collectionKey.getDataColumn(), identifier.getEntryId(), identifier.getEntryIdColumn()); - return dataManager.get(entryDataKey); - } - - public List getCollectionEntries(SimplePersistentCollection collection) { - return collectionEntryHolders.get(collection.getKey()).stream().map(identifier -> { - CellKey entryDataKey = getEntryDataKey(collection, identifier.getEntryId()); - Object value = dataManager.get(entryDataKey); - return new CollectionEntry(identifier.getEntryId(), value); - }).toList(); - } - - public void addValueEntryToDatabase(Connection connection, PersistentValueCollection collection, Collection entries) throws SQLException { - if (entries.isEmpty()) { - return; - } - - String entryIdColumn = collection.getEntryIdColumn(); - - StringBuilder sqlBuilder = new StringBuilder("INSERT INTO ").append(collection.getSchema()).append(".").append(collection.getTable()).append(" ("); - - List columns = new ArrayList<>(); - - columns.add(entryIdColumn); - if (!collection.getDataColumn().equals(entryIdColumn)) { - columns.add(collection.getDataColumn()); - } - columns.add(collection.getLinkingColumn()); - - for (int i = 0; i < columns.size(); i++) { - sqlBuilder.append(columns.get(i)); - if (i < columns.size() - 1) { - sqlBuilder.append(", "); - } - } - - sqlBuilder.append(") VALUES ("); - - for (int i = 0; i < columns.size(); i++) { - sqlBuilder.append("?"); - if (i < columns.size() - 1) { - sqlBuilder.append(", "); - } - } - - sqlBuilder.append(")"); - - sqlBuilder.append(" ON CONFLICT ("); - sqlBuilder.append(entryIdColumn); - sqlBuilder.append(") DO UPDATE SET "); - sqlBuilder.append(collection.getLinkingColumn()).append(" = EXCLUDED.").append(collection.getLinkingColumn()); - - String sql = sqlBuilder.toString(); - logSQL(sql); - - - boolean autoCommit = connection.getAutoCommit(); - connection.setAutoCommit(false); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - for (CollectionEntry entry : entries) { - - int i = 1; - statement.setObject(i, entry.id()); - if (!collection.getDataColumn().equals(entryIdColumn)) { - statement.setObject(++i, dataManager.serialize(entry.value())); - } - statement.setObject(++i, collection.getRootHolder().getId()); - - statement.executeUpdate(); - } - } finally { - connection.setAutoCommit(autoCommit); - } - - } - - public void addUniqueDataEntryToDatabase(Connection connection, PersistentUniqueDataCollection collection, Collection entries) throws SQLException { - if (entries.isEmpty()) { - return; - } - - StringBuilder sqlBuilder = new StringBuilder("UPDATE ").append(collection.getSchema()).append(".").append(collection.getTable()).append(" SET "); - sqlBuilder.append(collection.getLinkingColumn()).append(" = ? WHERE "); - sqlBuilder.append(collection.getEntryIdColumn()).append(" = ?"); - - String sql = sqlBuilder.toString(); - logSQL(sql); - - boolean autoCommit = connection.getAutoCommit(); - connection.setAutoCommit(false); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - for (CollectionEntry entry : entries) { - int i = 1; - statement.setObject(i, collection.getRootHolder().getId()); - statement.setObject(++i, entry.id()); - - statement.executeUpdate(); - } - } finally { - connection.setAutoCommit(autoCommit); - } - } - - - public void removeValueEntryFromDatabase(Connection connection, PersistentValueCollection collection, List entries) throws SQLException { - if (entries.isEmpty()) { - return; - } - - UniqueIdentifier holderIdentifier = collection.getRootHolder().getIdentifier(); - StringBuilder sqlBuilder = new StringBuilder("DELETE FROM ").append(collection.getSchema()).append(".").append(collection.getTable()).append(" WHERE ("); - - sqlBuilder.append(holderIdentifier.getColumn()); - - sqlBuilder.append(") IN ("); - - int totalValues = entries.size(); - - for (int i = 0; i < totalValues; i++) { - sqlBuilder.append("?"); - if (i < totalValues - 1) { - sqlBuilder.append(", "); - } - } - - sqlBuilder.append(")"); - - - String sql = sqlBuilder.toString(); - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - int i = 1; - - for (CollectionEntry entry : entries) { - statement.setObject(i++, entry.id()); - } - - statement.executeUpdate(); - } - } - - public void removeFromUniqueDataCollectionInMemory(PersistentValueCollection idsCollection, List entryIds) { - CollectionKey idsCollectionKey = idsCollection.getKey(); - removeFromUniqueDataCollectionInMemory(idsCollectionKey, idsCollection, entryIds); - } - - public void removeFromUniqueDataCollectionInMemory(CollectionKey idsCollectionKey, PersistentValueCollection dummyIdsCollection, List entryIds) { - if (entryIds.isEmpty()) { - return; - } - - for (UUID entry : entryIds) { - CollectionEntryIdentifier identifier = CollectionEntryIdentifier.of(dummyIdsCollection.getEntryIdColumn(), entry); - callRemoveHandlers(idsCollectionKey, getEntry(idsCollectionKey, identifier)); - - CellKey linkingKey = getEntryLinkingKey(dummyIdsCollection, entry); - dataManager.cache(linkingKey, UUID.class, null, Instant.now(), true); - - //remove after handlers are called to avoid DDNEEs - collectionEntryHolders.remove(idsCollectionKey, identifier); - } - } - - public void removeFromUniqueDataCollectionInDatabase(Connection connection, PersistentValueCollection idsCollection, List entryIds) throws SQLException { - if (entryIds.isEmpty()) { - return; - } - - StringBuilder sqlBuilder = new StringBuilder("UPDATE ").append(idsCollection.getSchema()).append(".").append(idsCollection.getTable()).append(" SET "); - sqlBuilder.append(idsCollection.getLinkingColumn()).append(" = NULL WHERE "); - sqlBuilder.append(idsCollection.getEntryIdColumn()).append(" IN ("); - - int totalValues = entryIds.size(); - - for (int i = 0; i < totalValues; i++) { - sqlBuilder.append("?"); - if (i < totalValues - 1) { - sqlBuilder.append(", "); - } - } - - sqlBuilder.append(")"); - - String sql = sqlBuilder.toString(); - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - int i = 1; - - for (UUID entry : entryIds) { - statement.setObject(i++, entry); - } - - statement.executeUpdate(); - } - } - - - @Blocking - public void loadJunctionTablesFromDatabase(Connection connection, PersistentManyToManyCollection dummyMMCollection) throws SQLException { - String junctionTable = dummyMMCollection.getSchema() + "." + dummyMMCollection.getJunctionTable(); - JunctionTable jt = this.junctionTables.computeIfAbsent(junctionTable, k -> new JunctionTable(dummyMMCollection.getThisIdColumn(), dummyMMCollection.getThatIdColumn())); - pgListener.ensureTableHasTrigger(connection, junctionTable); - String sql = "SELECT * FROM " + junctionTable; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - ResultSet resultSet = statement.executeQuery(); - Preconditions.checkArgument(resultSet.getMetaData().getColumnCount() == 2); - String leftColumn = dummyMMCollection.getThisIdColumn(); - String rightColumn = dummyMMCollection.getThatIdColumn(); - - while (resultSet.next()) { - UUID leftId = resultSet.getObject(leftColumn, UUID.class); - UUID rightId = resultSet.getObject(rightColumn, UUID.class); - - jt.add(leftColumn, leftId, rightId); - } - } - } - - public void addToJunctionTableInMemory(PersistentManyToManyCollection collection, List ids) { - String junctionTable = collection.getSchema() + "." + collection.getJunctionTable(); - JunctionTable jt = junctionTables.get(junctionTable); - Preconditions.checkNotNull(jt, "Junction table not loaded: " + junctionTable); - - for (UUID id : ids) { - jt.add(collection.getThisIdColumn(), collection.getRootHolder().getId(), id); - } - - for (UUID id : ids) { - callAddHandlers(collection.getKey(), id); - - CollectionKey otherCollectionKey = new CollectionKey( - collection.getSchema(), - collection.getJunctionTable(), - collection.getThatIdColumn(), - collection.getThisIdColumn(), - id - ); - callAddHandlers(otherCollectionKey, collection.getRootHolder().getId()); - } - } - - public void removeFromJunctionTableInMemory(PersistentManyToManyCollection collection, List ids) { - String junctionTable = collection.getSchema() + "." + collection.getJunctionTable(); - JunctionTable jt = junctionTables.get(junctionTable); - Preconditions.checkNotNull(jt, "Junction table not loaded: " + junctionTable); - - for (UUID id : ids) { - callRemoveHandlers(collection.getKey(), id); - - CollectionKey otherCollectionKey = new CollectionKey( - collection.getSchema(), - collection.getJunctionTable(), - collection.getThatIdColumn(), - collection.getThisIdColumn(), - id - ); - callRemoveHandlers(otherCollectionKey, collection.getRootHolder().getId()); - } - - //remove after handlers are called to avoid DDNEEs - for (UUID id : ids) { - jt.remove(collection.getThisIdColumn(), collection.getRootHolder().getId(), id); - } - } - - public List getJunctionTableEntryIds(PersistentManyToManyCollection collection) { - String junctionTable = collection.getSchema() + "." + collection.getJunctionTable(); - JunctionTable jt = junctionTables.get(junctionTable); - Preconditions.checkNotNull(jt, "Junction table not loaded: " + junctionTable); - - return jt.get(collection.getThisIdColumn(), collection.getRootHolder().getId()).stream().toList(); - } - - @Blocking - public void removeFromJunctionTableInDatabase(Connection connection, PersistentManyToManyCollection collection, List ids) throws SQLException { - String junctionTable = collection.getSchema() + "." + collection.getJunctionTable(); - if (ids.isEmpty()) { - return; - } - - StringBuilder sqlBuilder = new StringBuilder("DELETE FROM ").append(junctionTable).append(" WHERE "); - sqlBuilder.append(collection.getThisIdColumn()).append(" = ? AND "); - sqlBuilder.append(collection.getThatIdColumn()).append(" IN ("); - - int totalValues = ids.size(); - - for (int i = 0; i < totalValues; i++) { - sqlBuilder.append("?"); - if (i < totalValues - 1) { - sqlBuilder.append(", "); - } - } - - sqlBuilder.append(")"); - - String sql = sqlBuilder.toString(); - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - int i = 1; - statement.setObject(i++, collection.getRootHolder().getId()); - - for (UUID id : ids) { - statement.setObject(i++, id); - } - - statement.executeUpdate(); - } - } - - @Blocking - public void addToJunctionTableInDatabase(Connection connection, PersistentManyToManyCollection collection, List ids) throws SQLException { - String junctionTable = collection.getSchema() + "." + collection.getJunctionTable(); - if (ids.isEmpty()) { - return; - } - - StringBuilder sqlBuilder = new StringBuilder("INSERT INTO ").append(junctionTable).append(" ("); - sqlBuilder.append(collection.getThisIdColumn()).append(", ").append(collection.getThatIdColumn()).append(") VALUES "); - - int totalValues = ids.size(); - - for (int i = 0; i < totalValues; i++) { - sqlBuilder.append("(?, ?)"); - if (i < totalValues - 1) { - sqlBuilder.append(", "); - } - } - - String sql = sqlBuilder.toString(); - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - int i = 1; - - for (UUID id : ids) { - statement.setObject(i++, collection.getRootHolder().getId()); - statement.setObject(i++, id); - } - - statement.executeUpdate(); - } - } - -} diff --git a/oldsrc/og/java/net/staticstudios/data/impl/PersistentValueManager.java b/oldsrc/og/java/net/staticstudios/data/impl/PersistentValueManager.java deleted file mode 100644 index f26300dd..00000000 --- a/oldsrc/og/java/net/staticstudios/data/impl/PersistentValueManager.java +++ /dev/null @@ -1,387 +0,0 @@ -package net.staticstudios.data.impl; - -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.data.Data; -import net.staticstudios.data.data.value.InitialPersistentValue; -import net.staticstudios.data.impl.pg.PostgresListener; -import net.staticstudios.data.impl.pg.PostgresOperation; -import net.staticstudios.data.key.CellKey; -import net.staticstudios.data.util.DataDoesNotExistException; -import net.staticstudios.data.util.DeleteContext; -import net.staticstudios.data.util.InsertionStrategy; -import net.staticstudios.data.util.SQLLogger; -import net.staticstudios.utils.Pair; -import net.staticstudios.utils.ShutdownStage; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.Blocking; -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.time.Instant; -import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -public class PersistentValueManager extends SQLLogger { - - private final Logger logger = LoggerFactory.getLogger(PersistentValueManager.class); - private final DataManager dataManager; - private final PostgresListener pgListener; - private final Map enqueuedDatabaseUpdates = new HashMap<>(); - private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(thread -> { - Thread t = new Thread(thread); - t.setName("PersistentValueManager-ScheduledExecutor"); - return t; - }); - - @SuppressWarnings("rawtypes") - public PersistentValueManager(DataManager dataManager, PostgresListener pgListener) { - this.dataManager = dataManager; - this.pgListener = pgListener; - - pgListener.addHandler(notification -> { - String schema = notification.getSchema(); - String table = notification.getTable(); - List dummyPersistentValues = dataManager.getDummyValues(schema + "." + table).stream() - .filter(value -> value.getClass() == PersistentValue.class) - .map(value -> (PersistentValue) value) - .toList(); - - switch (notification.getOperation()) { - case PostgresOperation.UPDATE, PostgresOperation.INSERT -> { - Map newDataValueMap = notification.getData().newDataValueMap(); - for (Map.Entry entry : newDataValueMap.entrySet()) { - String column = entry.getKey(); - PersistentValue dummyPV = dummyPersistentValues.stream() - .filter(pv -> pv.getColumn().equals(column)) - .findFirst() - .orElse(null); - - if (dummyPV == null) { - continue; - } - - String idColumn = dummyPV.getIdColumn(); - UUID id = UUID.fromString(newDataValueMap.get(idColumn)); - - CellKey dataKey = new CellKey(schema, table, column, id, idColumn); - CellKey idKey = new CellKey(schema, table, idColumn, id, idColumn); - - String encodedValue = newDataValueMap.get(column); //Raw value as string - Object rawValue = dataManager.decode(dummyPV.getDataType(), encodedValue); - Object deserialized = dataManager.deserialize(dummyPV.getDataType(), rawValue); - - dataManager.cache(dataKey, dummyPV.getDataType(), deserialized, notification.getInstant(), true); - dataManager.cache(idKey, UUID.class, id, notification.getInstant(), true); - } - } - case PostgresOperation.DELETE -> { - Map oldDataValueMap = notification.getData().oldDataValueMap(); - for (Map.Entry entry : oldDataValueMap.entrySet()) { - String column = entry.getKey(); - PersistentValue dummyPV = dummyPersistentValues.stream() - .filter(pv -> pv.getColumn().equals(column)) - .findFirst() - .orElse(null); - - if (dummyPV == null) { - continue; - } - - String idColumn = dummyPV.getIdColumn(); - UUID id = UUID.fromString(oldDataValueMap.get(idColumn)); - - CellKey key = new CellKey(schema, table, column, id, idColumn); - - dataManager.uncache(key); - } - } - } - }); - - ThreadUtils.onShutdownRunSync(ShutdownStage.EARLY, () -> { - List tasks = scheduledExecutorService.shutdownNow(); - - logger.info("Shutting down PersistentValueManager, running {} enqueued tasks", tasks.size()); - for (Runnable task : tasks) { - task.run(); - } - }); - } - - public void deleteFromCache(DeleteContext context) { - //Since the whole table is being deleted, we can just remove all the values from the cache - Set> schemaTableIdSet = new HashSet<>(); - for (Data data : context.toDelete()) { - if (data instanceof PersistentValue pv) { - schemaTableIdSet.add(Pair.of(pv.getSchema() + "." + pv.getTable(), pv.getHolder().getRootHolder().getId())); - } - } - - dataManager.removeFromCacheIf(key -> { - if (!(key instanceof CellKey cellKey)) { - return false; - } - return schemaTableIdSet.contains(Pair.of(cellKey.getSchema() + "." + cellKey.getTable(), cellKey.getRootHolderId())); - -// Object oldValue = dataManager.get(key); -// try { -// oldValue = dataManager.get(key); -// } catch (DataDoesNotExistException ignored) { -// } -// dataManager.getPersistentCollectionManager().handlePersistentValueUncache( -// cellKey.getSchema(), -// cellKey.getTable(), -// cellKey.getColumn(), -// cellKey.getRootHolderId(), -// cellKey.getIdColumn(), -// oldValue -// ); - }); - } - - @Blocking - public void deleteFromDatabase(Connection connection, DeleteContext context) throws SQLException { - Map> schemaTableMap = new HashMap<>(); - for (Data data : context.toDelete()) { - if (data instanceof PersistentValue pv) { - schemaTableMap.put(pv.getSchema() + "." + pv.getTable(), pv); - } - } - - boolean autoCommit = connection.getAutoCommit(); - connection.setAutoCommit(false); - - for (String schemaTable : schemaTableMap.keySet()) { - PersistentValue pv = schemaTableMap.get(schemaTable); - String idColumn = pv.getIdColumn(); - String sql = "DELETE FROM " + schemaTable + " WHERE " + idColumn + " = ?"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, pv.getHolder().getRootHolder().getId()); - statement.executeUpdate(); - } - } - - connection.setAutoCommit(autoCommit); - } - - public void updateCache(PersistentValue persistentValue, Object value) { - updateCache(persistentValue.getSchema(), - persistentValue.getTable(), - persistentValue.getColumn(), - persistentValue.getHolder().getRootHolder().getId(), - persistentValue.getIdColumn(), - persistentValue.getDataType(), - value - ); - } - - public void updateCache(String schema, String table, String column, UUID holderId, String idColumn, Class valueDataType, Object value) { - Object oldValue = null; - - CellKey key = new CellKey(schema, table, column, holderId, idColumn); - - try { - oldValue = dataManager.get(key); - } catch (DataDoesNotExistException ignored) { - } - - dataManager.cache(key, valueDataType, value, Instant.now(), true); - dataManager.getPersistentCollectionManager().handlePersistentValueCacheUpdated(schema, table, column, holderId, idColumn, oldValue, value); - } - - @Blocking - public void insertInDatabase(Connection connection, UniqueData holder, List initialData) throws SQLException { - if (initialData.isEmpty()) { - return; - } - - boolean autoCommit = connection.getAutoCommit(); - connection.setAutoCommit(false); - - //Group by id.schema.table - Multimap initialDataMap = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new); - - for (InitialPersistentValue initial : initialData) { - PersistentValue pv = initial.getValue(); - String schemaTable = pv.getIdColumn() + "." + pv.getSchema() + "." + pv.getTable(); - initialDataMap.put(schemaTable, initial); - } - - for (String idSchemaTable : initialDataMap.keySet()) { - String idColumn = idSchemaTable.split("\\.", 2)[0]; - String schemaTable = idSchemaTable.split("\\.", 2)[1]; - List initialDataValues = new ArrayList<>(initialDataMap.get(idSchemaTable)); - initialDataValues.removeIf(i -> i.getValue().getColumn().equals(idColumn)); - List overwriteExisting = new ArrayList<>(); - for (InitialPersistentValue initial : initialDataValues) { - if (initial.getValue().getInsertionStrategy() == InsertionStrategy.OVERWRITE_EXISTING) { - overwriteExisting.add(initial); - } - } - - StringBuilder sqlBuilder = new StringBuilder("INSERT INTO "); - sqlBuilder.append(schemaTable); - sqlBuilder.append(" ("); - sqlBuilder.append(idColumn); - sqlBuilder.append(", "); - for (InitialPersistentValue initial : initialDataValues) { - sqlBuilder.append(initial.getValue().getColumn()); - sqlBuilder.append(", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - - sqlBuilder.append(") VALUES (?, "); - sqlBuilder.append("?, ".repeat(initialDataValues.size())); - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(")"); - - if (!overwriteExisting.isEmpty()) { - sqlBuilder.append(" ON CONFLICT ("); - sqlBuilder.append(idColumn); - sqlBuilder.append(") DO UPDATE SET "); - for (InitialPersistentValue initial : overwriteExisting) { - sqlBuilder.append(initial.getValue().getColumn()); - sqlBuilder.append(" = EXCLUDED."); - sqlBuilder.append(initial.getValue().getColumn()); - sqlBuilder.append(", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - } else { - sqlBuilder.append(" ON CONFLICT ("); - sqlBuilder.append(idColumn); - sqlBuilder.append(") DO NOTHING"); - } - String sql = sqlBuilder.toString(); - - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setObject(1, holder.getId()); - int i = 2; - for (InitialPersistentValue initial : initialDataValues) { - Object initialDataValue = initial.getInitialDataValue(); - Object serialized = dataManager.serialize(initialDataValue); - statement.setObject(i++, serialized); - } - - statement.executeUpdate(); - } - } - - connection.setAutoCommit(autoCommit); - } - - @Blocking - public void updateInDatabase(Connection connection, PersistentValue persistentValue, Object value) throws SQLException { - String schemaTable = persistentValue.getSchema() + "." + persistentValue.getTable(); - String idColumn = persistentValue.getIdColumn(); - String column = persistentValue.getColumn(); - - String sql = "UPDATE " + schemaTable + " SET " + column + " = ? WHERE " + idColumn + " = ?"; - logSQL(sql); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - Object serialized = dataManager.serialize(value); - statement.setObject(1, serialized); - statement.setObject(2, persistentValue.getHolder().getRootHolder().getId()); - - statement.executeUpdate(); - } - } - - /** - * Column keys are expected to all be in the same table, AND are expected to all have the same id column - */ - @SuppressWarnings("rawtypes") - public void loadAllFromDatabase(Connection connection, UniqueData dummyHolder, Collection dummyCellKeys) throws SQLException { - if (dummyCellKeys.isEmpty()) { - return; - } - - CellKey firstCellKey = dummyCellKeys.iterator().next(); - String schemaTable = firstCellKey.getSchema() + "." + firstCellKey.getTable(); - pgListener.ensureTableHasTrigger(connection, schemaTable); - - String idColumn = firstCellKey.getIdColumn(); - - Set dataColumns = new HashSet<>(); - - for (CellKey cellKey : dummyCellKeys) { - dataColumns.add(cellKey.getColumn()); - } - - dataColumns.remove(idColumn); - - StringBuilder sqlBuilder = new StringBuilder("SELECT "); - for (String column : dataColumns) { - sqlBuilder.append(column); - sqlBuilder.append(", "); - } - sqlBuilder.append(idColumn); - - sqlBuilder.append(" FROM "); - sqlBuilder.append(schemaTable); - String sql = sqlBuilder.toString(); - logSQL(sql); - - List dummyPersistentValues = dataManager.getDummyValues(schemaTable).stream() - .filter(value -> value.getClass() == PersistentValue.class) - .map(value -> (PersistentValue) value) - .toList(); - - try (PreparedStatement statement = connection.prepareStatement(sql)) { - ResultSet resultSet = statement.executeQuery(); - while (resultSet.next()) { - UUID id = resultSet.getObject(idColumn, UUID.class); - for (String column : dataColumns) { - PersistentValue dummyPV = dummyPersistentValues.stream() - .filter(pv -> pv.getColumn().equals(column)) - .findFirst() - .orElse(null); - - if (dummyPV == null) { - continue; - } - - Object value = resultSet.getObject(column, dataManager.getSerializedDataType(dummyPV.getDataType())); - Object deserialized = dataManager.deserialize(dummyPV.getDataType(), value); - dataManager.cache(new CellKey(firstCellKey.getSchema(), firstCellKey.getTable(), column, id, idColumn), dummyPV.getDataType(), deserialized, Instant.now(), false); - } - - if (!dataColumns.contains(idColumn)) { - dataManager.cache(new CellKey(firstCellKey.getSchema(), firstCellKey.getTable(), idColumn, id, idColumn), UUID.class, id, Instant.now(), false); - } - } - } - } - - public synchronized void enqueueRunnable(CellKey cellKey, int duration, Runnable runnable) { - if (ThreadUtils.isShuttingDown()) { - logger.warn("enqueueRunnable called while shutting down, running immediately"); - runnable.run(); - return; - } - - if (enqueuedDatabaseUpdates.put(cellKey, runnable) == null) { - scheduledExecutorService.schedule(() -> { - Runnable toRun = enqueuedDatabaseUpdates.remove(cellKey); - if (toRun != null) { - toRun.run(); - } - }, duration, TimeUnit.MILLISECONDS); - } - - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresListener.java b/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresListener.java deleted file mode 100644 index c41315d6..00000000 --- a/oldsrc/og/java/net/staticstudios/data/impl/pg/PostgresListener.java +++ /dev/null @@ -1,190 +0,0 @@ -package net.staticstudios.data.impl.pg; - -import com.google.gson.Gson; -import com.impossibl.postgres.api.jdbc.PGConnection; -import com.impossibl.postgres.api.jdbc.PGNotificationListener; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.util.DataSourceConfig; -import net.staticstudios.utils.ShutdownStage; -import net.staticstudios.utils.ThreadUtils; -import org.jetbrains.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.sql.Statement; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -public class PostgresListener { - public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSX"); - public static String CREATE_DATA_NOTIFY_FUNCTION = """ - create or replace function propagate_data_update() returns trigger as $$ - declare - notification text; - begin - notification := to_char(current_timestamp AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.FF6"Z"') || ',' || tg_table_schema || ',' || TG_TABLE_NAME || ',' || TG_OP || ',' || current_setting('application_name') || ',' || - json_build_object( - 'old', (case when TG_OP = 'INSERT' then '{}' else row_to_json(OLD) end), - 'new', (case when TG_OP = 'DELETE' then '{}' else row_to_json(NEW) end) - )::text; - - perform pg_notify('data_notification', notification); - - return new; - end; - $$ language plpgsql; - """; - public static String CREATE_TRIGGER = """ - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_trigger - WHERE tgname = 'propagate_data_update_trigger' - AND tgrelid = '%s'::regclass - ) THEN - CREATE TRIGGER propagate_data_update_trigger - AFTER INSERT OR UPDATE OR DELETE ON %s - FOR EACH ROW EXECUTE PROCEDURE propagate_data_update(); - END IF; - END; - $$ - """; - private final Logger logger = LoggerFactory.getLogger(PostgresListener.class); - private final Set tablesTriggered = Collections.synchronizedSet(new HashSet<>()); - private final ConcurrentLinkedDeque> notificationHandlers = new ConcurrentLinkedDeque<>(); - private final Gson gson = new Gson(); - private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); - public @VisibleForTesting PGConnection pgConnection; - - public PostgresListener(DataManager dataManager, DataSourceConfig ds) { - try { - Class.forName("com.impossibl.postgres.jdbc.PGDriver"); - - setPgConnection(dataManager, ds); - - scheduledExecutorService.scheduleAtFixedRate(() -> { - if (ThreadUtils.isShuttingDown()) { - return; - } - try { - if (pgConnection.isClosed()) { - logger.warn("Connection closed, re-establishing connection"); - try { - setPgConnection(dataManager, ds); - } catch (SQLException e) { - logger.error("Error re-establishing connection", e); - } - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - }, 1, 1, TimeUnit.SECONDS); - - } catch (SQLException | ClassNotFoundException e) { - throw new RuntimeException(e); - } - logger.debug("Notification listener started"); - - ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, () -> { - try { - scheduledExecutorService.shutdownNow(); - pgConnection.close(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - }); - } - - private void setPgConnection(DataManager dataManager, DataSourceConfig ds) throws SQLException { - this.pgConnection = DriverManager.getConnection("jdbc:pgsql://" + ds.databaseHost() + ":" + ds.databasePort() + "/" + ds.databaseName(), ds.databaseUsername(), ds.databasePassword()).unwrap(PGConnection.class); - - try (Statement statement = pgConnection.createStatement()) { - logger.trace("Creating data_notify function"); - statement.execute(CREATE_DATA_NOTIFY_FUNCTION); - } - - pgConnection.addNotificationListener("data_notification", new PGNotificationListener() { - @Override - public void notification(int processId, String channelName, String payload) { - logger.trace("Received notification. PID: {}, Channel: {}, Payload: {}", processId, channelName, payload); - String[] parts = payload.split(",", 6); - String encodedTimestamp = parts[0]; - String schema = parts[1]; - String table = parts[2]; - String encodedOperation = parts[3]; - String applicationName = parts[4]; - String encodedData = parts[5]; - - //Filter out notifications from this application (data manager session) - if (dataManager.getApplicationName().equals(applicationName)) { - logger.trace("Ignoring notification from this session"); - return; - } - - PostgresData data = gson.fromJson(encodedData, PostgresData.class); - - OffsetDateTime offsetDateTime = OffsetDateTime.parse(encodedTimestamp, DATE_TIME_FORMATTER); - - PostgresNotification notification = new PostgresNotification( - offsetDateTime.toInstant(), - schema, - table, - PostgresOperation.valueOf(encodedOperation), - data - ); - - for (Consumer handler : notificationHandlers) { - try { - handler.accept(notification); - } catch (Exception e) { - logger.error("Error handling notification", e); - } - } - } - }); - - try (Statement statement = pgConnection.createStatement()) { - statement.execute("LISTEN data_notification"); - } - } - - public void addHandler(Consumer handler) { - notificationHandlers.add(handler); - } - - - /** - * Whenever we see a new table, make sure the trigger is added to it - * - * @param connection the connection to the database - * @param schemaTable the table to add the trigger to - */ - public void ensureTableHasTrigger(Connection connection, String schemaTable) { - if (tablesTriggered.contains(schemaTable)) { - return; - } - - String sql = CREATE_TRIGGER.formatted(schemaTable, schemaTable); - logger.debug("Adding propagate_data_update_trigger to table: {}", schemaTable); - - try (Statement statement = connection.createStatement()) { - statement.execute(sql); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - tablesTriggered.add(schemaTable); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/key/CellKey.java b/oldsrc/og/java/net/staticstudios/data/key/CellKey.java deleted file mode 100644 index 10d320c9..00000000 --- a/oldsrc/og/java/net/staticstudios/data/key/CellKey.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.staticstudios.data.key; - -import net.staticstudios.data.PersistentValue; - -import java.util.UUID; - -public class CellKey extends DatabaseKey { - private final String schema; - private final String table; - private final String column; - private final String idColumn; - private final UUID rootHolderId; - - public CellKey(String schema, String table, String column, UUID rootHolderId, String idColumn) { - super(schema, table, column, rootHolderId, idColumn); - this.schema = schema; - this.table = table; - this.column = column; - this.idColumn = idColumn; - this.rootHolderId = rootHolderId; - } - - public CellKey(PersistentValue data) { - this(data.getSchema(), data.getTable(), data.getColumn(), data.getHolder().getRootHolder().getId(), data.getIdColumn()); - } - - @Override - public String getSchema() { - return schema; - } - - @Override - public String getTable() { - return table; - } - - public String getColumn() { - return column; - } - - public String getIdColumn() { - return idColumn; - } - - public UUID getRootHolderId() { - return rootHolderId; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/key/CollectionKey.java b/oldsrc/og/java/net/staticstudios/data/key/CollectionKey.java deleted file mode 100644 index 5b4e4d12..00000000 --- a/oldsrc/og/java/net/staticstudios/data/key/CollectionKey.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.staticstudios.data.key; - -import java.util.UUID; - -/** - * A collection key is unique to a one-to-many relationship instance. - */ -public class CollectionKey extends DatabaseKey { - private final String schema; - private final String table; - private final String linkingColumn; - private final String dataColumn; - - public CollectionKey(String schema, String table, String linkingColumn, String dataColumn, UUID rootHolderId) { - super(schema, table, linkingColumn, dataColumn, rootHolderId); - this.schema = schema; - this.table = table; - this.linkingColumn = linkingColumn; - this.dataColumn = dataColumn; - } - - @Override - public String getSchema() { - return schema; - } - - @Override - public String getTable() { - return table; - } - - public String getLinkingColumn() { - return linkingColumn; - } - - public String getDataColumn() { - return dataColumn; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/key/DataKey.java b/oldsrc/og/java/net/staticstudios/data/key/DataKey.java deleted file mode 100644 index 5e1857c4..00000000 --- a/oldsrc/og/java/net/staticstudios/data/key/DataKey.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.staticstudios.data.key; - -import java.util.Arrays; - -public class DataKey { - private final Object[] parts; - - public DataKey(Object... parts) { - this.parts = parts; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - DataKey dataKey = (DataKey) obj; - return Arrays.equals(parts, dataKey.parts); - } - - @Override - public int hashCode() { - return Arrays.hashCode(parts) + getClass().hashCode(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "{" + - "parts=" + Arrays.toString(parts) + - '}'; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/key/DatabaseKey.java b/oldsrc/og/java/net/staticstudios/data/key/DatabaseKey.java deleted file mode 100644 index 26501312..00000000 --- a/oldsrc/og/java/net/staticstudios/data/key/DatabaseKey.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.staticstudios.data.key; - -public abstract class DatabaseKey extends DataKey { - public DatabaseKey(Object... parts) { - super(parts); - } - - public abstract String getSchema(); - - public abstract String getTable(); -} diff --git a/oldsrc/og/java/net/staticstudios/data/key/RedisKey.java b/oldsrc/og/java/net/staticstudios/data/key/RedisKey.java deleted file mode 100644 index 7cd61b5e..00000000 --- a/oldsrc/og/java/net/staticstudios/data/key/RedisKey.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.staticstudios.data.key; - -import com.google.common.base.Preconditions; -import net.staticstudios.data.CachedValue; - -import java.util.UUID; - -public class RedisKey extends DataKey { - private final String identifyingKey; - private final String holderSchema; - private final String holderTable; - private final String holderIdColumn; - private final UUID rootHolderId; - - public RedisKey(String holderSchema, String holderTable, String holderIdColumn, UUID rootHolderId, String identifyingKey) { - super(holderSchema, holderTable, holderIdColumn, rootHolderId, identifyingKey); - Preconditions.checkArgument(!identifyingKey.contains(":"), "Identifying key cannot contain ':'"); - this.identifyingKey = identifyingKey; - this.holderSchema = holderSchema; - this.holderTable = holderTable; - this.holderIdColumn = holderIdColumn; - this.rootHolderId = rootHolderId; - } - - public RedisKey(CachedValue data) { - this( - data.getSchema(), - data.getTable(), - data.getIdColumn(), - data.getHolder().getRootHolder().getRootHolder().getId(), - data.getIdentifyingKey() - ); - } - - public static RedisKey fromString(String key) { - if (!key.startsWith("static-data:")) { - return null; - } - String[] parts = key.split(":"); - return new RedisKey(parts[1], parts[2], parts[3], UUID.fromString(parts[4]), parts[5]); - } - - public static boolean isRedisKey(String key) { - return fromString(key) != null; - } - - /** - * Omit the root holder id from the key - * - * @return the partial key - */ - public String toPartialKey() { - return String.format("static-data:%s:%s:%s:*:%s", holderSchema, holderTable, holderIdColumn, identifyingKey); - } - - public String getIdentifyingKey() { - return identifyingKey; - } - - public String getHolderSchema() { - return holderSchema; - } - - public String getHolderTable() { - return holderTable; - } - - public String getHolderIdColumn() { - return holderIdColumn; - } - - public UUID getRootHolderId() { - return rootHolderId; - } - - @Override - public String toString() { - return String.format("static-data:%s:%s:%s:%s:%s", holderSchema, holderTable, holderIdColumn, rootHolderId, identifyingKey); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/key/UniqueIdentifier.java b/oldsrc/og/java/net/staticstudios/data/key/UniqueIdentifier.java deleted file mode 100644 index d9f8f0d3..00000000 --- a/oldsrc/og/java/net/staticstudios/data/key/UniqueIdentifier.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.staticstudios.data.key; - -import com.impossibl.postgres.utils.guava.Preconditions; - -import java.util.Objects; -import java.util.UUID; - -public class UniqueIdentifier { - private final String column; - private final UUID id; - - public UniqueIdentifier(String column, UUID id) { - this.column = Preconditions.checkNotNull(column); - this.id = id; //can be null for dummy instances - } - - public static UniqueIdentifier of(String column, UUID value) { - return new UniqueIdentifier(column, value); - } - - public String getColumn() { - return column; - } - - public UUID getId() { - return id; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - UniqueIdentifier uniqueIdentifier = (UniqueIdentifier) obj; - return this.column.equals(uniqueIdentifier.column) && this.id.equals(uniqueIdentifier.id); - } - - @Override - public int hashCode() { - return Objects.hash(column, id); - } - - @Override - public String toString() { - return "UniqueIdentifier{" + - "column='" + column + '\'' + - ", id=" + id + - '}'; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/primative/Primitive.java b/oldsrc/og/java/net/staticstudios/data/primative/Primitive.java deleted file mode 100644 index 2634fd64..00000000 --- a/oldsrc/og/java/net/staticstudios/data/primative/Primitive.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.staticstudios.data.primative; - -import java.util.function.Function; - -public class Primitive { - private final Class runtimeType; - private final Function decoder; - private final Function encoder; - private final boolean nullable; - private final T defaultValue; - - public Primitive(Class runtimeType, Function decoder, Function encoder, boolean nullable, T defaultValue) { - this.runtimeType = runtimeType; - this.decoder = decoder; - this.encoder = encoder; - this.nullable = nullable; - this.defaultValue = defaultValue; - } - - public static PrimitiveBuilder builder(Class runtimeType) { - return new PrimitiveBuilder<>(runtimeType); - } - - public T decode(String value) { - return decoder.apply(value); - } - - public String encode(T value) { - return encoder.apply(value); - } - - public String unsafeEncode(Object value) { - return encoder.apply(runtimeType.cast(value)); - } - - public boolean isNullable() { - return nullable; - } - - public T getDefaultValue() { - return defaultValue; - } - - public Class getRuntimeType() { - return runtimeType; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java b/oldsrc/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java deleted file mode 100644 index e40bed41..00000000 --- a/oldsrc/og/java/net/staticstudios/data/primative/PrimitiveBuilder.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.staticstudios.data.primative; - -import com.google.common.base.Preconditions; - -import java.util.function.Consumer; -import java.util.function.Function; - -public class PrimitiveBuilder { - private final Class runtimeType; - private Function decoder; - private Function encoder; - private Boolean nullable; - private T defaultValue; - - public PrimitiveBuilder(Class runtimeType) { - this.runtimeType = runtimeType; - } - - public PrimitiveBuilder decoder(Function decoder) { - this.decoder = decoder; - return this; - } - - /** - * Note that the encoder should encode the value to a string the exact same as Postgres would. - * - * @param encoder The encoder function - * @return The builder - */ - public PrimitiveBuilder encoder(Function encoder) { - this.encoder = encoder; - return this; - } - - public PrimitiveBuilder nullable(boolean nullable) { - this.nullable = nullable; - return this; - } - - public PrimitiveBuilder defaultValue(T defaultValue) { - this.defaultValue = defaultValue; - return this; - } - - public Primitive build(Consumer> consumer) { - Preconditions.checkNotNull(decoder, "Decoder is null"); - Preconditions.checkNotNull(encoder, "Encoder is null"); - Preconditions.checkNotNull(consumer, "Consumer is null"); - Preconditions.checkNotNull(nullable, "Nullable flag is null"); - - if (!nullable) { - Preconditions.checkNotNull(defaultValue, "Default value is null"); - } - - Primitive primitive = new Primitive<>(runtimeType, decoder, encoder, nullable, defaultValue); - consumer.accept(primitive); - - return primitive; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/primative/Primitives.java b/oldsrc/og/java/net/staticstudios/data/primative/Primitives.java deleted file mode 100644 index 0441ab08..00000000 --- a/oldsrc/og/java/net/staticstudios/data/primative/Primitives.java +++ /dev/null @@ -1,143 +0,0 @@ -package net.staticstudios.data.primative; - -import net.staticstudios.data.util.PostgresUtils; - -import java.sql.Timestamp; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.HashMap; -import java.util.Map; - -@SuppressWarnings("unused") -public class Primitives { - // General rule of thumb: if the Primitive is a Java primitive, it should not be nullable, everything else should be nullable. - private static final DateTimeFormatter TIMESTAMP_FORMATTER = new DateTimeFormatterBuilder() - .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - .appendPattern("xxx") - .toFormatter() - .withZone(ZoneId.of("UTC")); - - private static Map, Primitive> primitives; - public static final Primitive STRING = Primitive.builder(String.class) - .nullable(true) - .encoder(s -> s) - .decoder(s -> s) - .build(Primitives::register); - public static final Primitive CHARACTER = Primitive.builder(Character.class) - .nullable(false) - .defaultValue((char) 0) - .encoder(c -> Character.toString(c)) - .decoder(s -> s.charAt(0)) - .build(Primitives::register); - public static final Primitive BYTE = Primitive.builder(Byte.class) - .nullable(false) - .defaultValue((byte) 0) - .encoder(b -> Byte.toString(b)) - .decoder(Byte::parseByte) - .build(Primitives::register); - public static final Primitive SHORT = Primitive.builder(Short.class) - .nullable(false) - .defaultValue((short) 0) - .encoder(s -> Short.toString(s)) - .decoder(Short::parseShort) - .build(Primitives::register); - public static final Primitive INTEGER = Primitive.builder(Integer.class) - .nullable(false) - .defaultValue(0) - .encoder(i -> Integer.toString(i)) - .decoder(Integer::parseInt) - .build(Primitives::register); - public static final Primitive LONG = Primitive.builder(Long.class) - .nullable(false) - .defaultValue(0L) - .encoder(l -> Long.toString(l)) - .decoder(Long::parseLong) - .build(Primitives::register); - public static final Primitive FLOAT = Primitive.builder(Float.class) - .nullable(false) - .defaultValue(0.0f) - .encoder(f -> Float.toString(f)) - .decoder(Float::parseFloat) - .build(Primitives::register); - public static final Primitive DOUBLE = Primitive.builder(Double.class) - .nullable(false) - .defaultValue(0.0) - .encoder(d -> Double.toString(d)) - .decoder(Double::parseDouble) - .build(Primitives::register); - public static final Primitive BOOLEAN = Primitive.builder(Boolean.class) - .nullable(false) - .defaultValue(false) - .encoder(b -> Boolean.toString(b)) - .decoder(Boolean::parseBoolean) - .build(Primitives::register); - public static final Primitive UUID = Primitive.builder(java.util.UUID.class) - .nullable(true) - .encoder(uuid -> uuid == null ? null : uuid.toString()) - .decoder(s -> s == null ? null : java.util.UUID.fromString(s)) - .build(Primitives::register); - public static final Primitive TIMESTAMP = Primitive.builder(Timestamp.class) - .nullable(true) - .encoder(timestamp -> { - if (timestamp == null) { - return null; - } - - return TIMESTAMP_FORMATTER.format(timestamp.toInstant()); - }) - .decoder(s -> { - if (s == null) { - return null; - } - - OffsetDateTime parsedTimestamp = OffsetDateTime.parse(s, TIMESTAMP_FORMATTER); - return Timestamp.from(parsedTimestamp.toInstant()); - }) - .build(Primitives::register); - public static final Primitive BYTE_ARRAY = Primitive.builder(byte[].class) - .nullable(true) - .encoder(b -> { - if (b == null) { - return null; - } - - return PostgresUtils.toHex(b); - }) - .decoder(s -> { - if (s == null) { - return null; - } - - return PostgresUtils.toBytes(s); - }) - .build(Primitives::register); - - public static Primitive getPrimitive(Class type) { - return primitives.get(type); - } - - public static boolean isPrimitive(Class type) { - return primitives.containsKey(type); - } - - @SuppressWarnings("unchecked") - public static T decode(Class type, String value) { - return (T) getPrimitive(type).decode(value); - } - - public static String encode(Object value) { - if (value == null) { - return null; - } - return getPrimitive(value.getClass()).unsafeEncode(value); - } - - private static void register(Primitive primitive) { - if (primitives == null) { - primitives = new HashMap<>(); - } - primitives.put(primitive.getRuntimeType(), primitive); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/BatchInsert.java b/oldsrc/og/java/net/staticstudios/data/util/BatchInsert.java deleted file mode 100644 index c197c3d7..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/BatchInsert.java +++ /dev/null @@ -1,124 +0,0 @@ -package net.staticstudios.data.util; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.data.InitialValue; -import org.jetbrains.annotations.Blocking; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Predicate; - -/** - * Represents a batch of insertions to be performed. - * This is especially useful when foreign key constraints are involved, as it allows for all insertions to be performed - * in a single transaction. - */ -public class BatchInsert { - private final List insertionContexts = new CopyOnWriteArrayList<>(); - private final List postInsertActions = new CopyOnWriteArrayList<>(); - private final List preInsertActions = new CopyOnWriteArrayList<>(); - private final List> preconditions = new CopyOnWriteArrayList<>(); - private final List intermediateActions = new CopyOnWriteArrayList<>(); - private final DataManager dataManager; - private transient boolean flushed = false; - - /** - * Create a new batch of insertions. - * - * @param dataManager The data manager to use for the insertions - */ - public BatchInsert(DataManager dataManager) { - this.dataManager = dataManager; - } - - /** - * Add a new insertion to the batch. - * - * @param holder The holder of the data to be inserted - * @param initialData The initial data to be inserted - */ - public void add(UniqueData holder, InitialValue... initialData) { - if (flushed) { - throw new IllegalStateException("BatchInsertion has already been flushed"); - } - InsertContext context = dataManager.buildInsertContext(holder, initialData); - insertionContexts.add(context); - } - - public void post(Runnable action) { - if (flushed) { - throw new IllegalStateException("BatchInsertion has already been flushed"); - } - postInsertActions.add(action); - } - - public void intermediate(ConnectionConsumer action) { - if (flushed) { - throw new IllegalStateException("BatchInsertion has already been flushed"); - } - intermediateActions.add(action); - } - - public void early(Runnable action) { - if (flushed) { - throw new IllegalStateException("BatchInsertion has already been flushed"); - } - preInsertActions.add(action); - } - - public void precondition(Predicate precondition) { - if (flushed) { - throw new IllegalStateException("BatchInsertion has already been flushed"); - } - preconditions.add(precondition); - } - - /** - * Get the insertion contexts in this batch. - * - * @return The insertion contexts - */ - public List getInsertionContexts() { - return insertionContexts; - } - - /** - * Asynchronously insert all data in this batch. - */ - public void insertAsync() { - updateFlushed(); - - for (Predicate precondition : preconditions) { - if (!precondition.test(this)) { - throw new IllegalStateException("Precondition failed, batch insertion aborted"); - } - } - - dataManager.insertBatchAsync(this, intermediateActions, preInsertActions, postInsertActions); - } - - /** - * Synchronously insert all data in this batch. - */ - @Blocking - public void insert() { - updateFlushed(); - - for (Predicate precondition : preconditions) { - if (!precondition.test(this)) { - throw new IllegalStateException("Precondition failed, batch insertion aborted"); - } - } - - dataManager.insertBatch(this, intermediateActions, preInsertActions, postInsertActions); - } - - private synchronized void updateFlushed() { - if (flushed) { - throw new IllegalStateException("BatchInsertion has already been flushed"); - } - - flushed = true; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/CacheEntry.java b/oldsrc/og/java/net/staticstudios/data/util/CacheEntry.java deleted file mode 100644 index 7697c9be..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/CacheEntry.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.staticstudios.data.util; - -import java.time.Instant; - -public record CacheEntry(Object value, Instant instant) { - - public static CacheEntry of(T value) { - return new CacheEntry(value, Instant.now()); - } - - public static CacheEntry of(T value, Instant instant) { - return new CacheEntry(value, instant); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/DataDoesNotExistException.java b/oldsrc/og/java/net/staticstudios/data/util/DataDoesNotExistException.java deleted file mode 100644 index deff9cf4..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/DataDoesNotExistException.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.staticstudios.data.util; - -public class DataDoesNotExistException extends RuntimeException { - public DataDoesNotExistException(String message) { - super(message); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/DeleteContext.java b/oldsrc/og/java/net/staticstudios/data/util/DeleteContext.java deleted file mode 100644 index 99c42819..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/DeleteContext.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.staticstudios.data.util; - -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.data.Data; -import net.staticstudios.data.key.DataKey; - -import java.util.Map; -import java.util.Set; - -public record DeleteContext(Set holders, Set> toDelete, Map oldValues) { -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/DeletionStrategy.java b/oldsrc/og/java/net/staticstudios/data/util/DeletionStrategy.java deleted file mode 100644 index 87143962..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/DeletionStrategy.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.staticstudios.data.util; - -import net.staticstudios.data.PersistentCollection; - -public enum DeletionStrategy { - /** - * When the parent holder is deleted, delete this data as well. - */ - CASCADE, - /** - * Do nothing when the parent holder is deleted. - */ - NO_ACTION, - /** - * This is only for use in PersistentCollections created via - * {@link PersistentCollection#oneToMany} or - * {@link PersistentCollection#manyToMany} - */ - UNLINK -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/InsertContext.java b/oldsrc/og/java/net/staticstudios/data/util/InsertContext.java deleted file mode 100644 index 0cda8116..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/InsertContext.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.staticstudios.data.util; - -import net.staticstudios.data.CachedValue; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.data.value.InitialCachedValue; -import net.staticstudios.data.data.value.InitialPersistentValue; - -import java.util.Map; - -public record InsertContext( - UniqueData holder, - Map, InitialPersistentValue> initialPersistentValues, - Map, InitialCachedValue> initialCachedValues -) { -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/InsertionStrategy.java b/oldsrc/og/java/net/staticstudios/data/util/InsertionStrategy.java deleted file mode 100644 index 844c88e6..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/InsertionStrategy.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.staticstudios.data.util; - -public enum InsertionStrategy { - /** - * Overwrite existing data with new data. - */ - OVERWRITE_EXISTING, - /** - * Do not overwrite existing data, only insert if no data exists. - */ - PREFER_EXISTING, -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/JunctionTable.java b/oldsrc/og/java/net/staticstudios/data/util/JunctionTable.java deleted file mode 100644 index 9cb08520..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/JunctionTable.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.staticstudios.data.util; - -import com.google.common.base.Preconditions; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; - -import java.util.Collection; -import java.util.UUID; -import java.util.function.BiPredicate; - -public class JunctionTable { - private final Multimap entriesLeftToRight; - private final Multimap entriesRightToLeft; - private final String left; - private final String right; - - public JunctionTable(String left, String right) { - this.left = left; - this.right = right; - this.entriesLeftToRight = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - this.entriesRightToLeft = Multimaps.synchronizedSetMultimap(HashMultimap.create()); - } - - public void add(String left, UUID leftId, UUID rightId) { - Preconditions.checkNotNull(leftId); - Preconditions.checkNotNull(rightId); - if (this.left.equals(left)) { - entriesLeftToRight.put(leftId, rightId); - entriesRightToLeft.put(rightId, leftId); - } else if (this.right.equals(left)) { - entriesLeftToRight.put(rightId, leftId); - entriesRightToLeft.put(leftId, rightId); - } else { - throw new IllegalArgumentException("Invalid left/right identifier! Expected " + this.left + " or " + this.right + ", got " + left); - } - } - - public void remove(String left, UUID leftId, UUID rightId) { - if (this.left.equals(left)) { - entriesLeftToRight.remove(leftId, rightId); - entriesRightToLeft.remove(rightId, leftId); - } else if (this.right.equals(left)) { - entriesLeftToRight.remove(rightId, leftId); - entriesRightToLeft.remove(leftId, rightId); - } else { - throw new IllegalArgumentException("Invalid left/right identifier! Expected " + this.left + " or " + this.right + ", got " + left); - } - } - - public void removeIf(String left, BiPredicate predicate) { - if (this.left.equals(left)) { - entriesLeftToRight.entries().removeIf(e -> predicate.test(e.getKey(), e.getValue())); - } else if (this.right.equals(left)) { - entriesRightToLeft.entries().removeIf(e -> predicate.test(e.getKey(), e.getValue())); - } else { - throw new IllegalArgumentException("Invalid left/right identifier! Expected " + this.left + " or " + this.right + ", got " + left); - } - } - - public Collection get(String left, UUID leftId) { - if (this.left.equals(left)) { - return entriesLeftToRight.get(leftId); - } else if (this.right.equals(left)) { - return entriesRightToLeft.get(leftId); - } else { - throw new IllegalArgumentException("Invalid left/right identifier! Expected " + this.left + " or " + this.right + ", got " + left); - } - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/ReflectionUtils.java b/oldsrc/og/java/net/staticstudios/data/util/ReflectionUtils.java deleted file mode 100644 index c9b1eba9..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/ReflectionUtils.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.staticstudios.data.util; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; - -public class ReflectionUtils { - - /** - * Get all fields from this class AND its superclasses - * - * @param clazz The class to get fields from - * @return A list of fields - */ - public static List getFields(Class clazz) { - List fields = new ArrayList<>(List.of(clazz.getDeclaredFields())); - - if (clazz.getSuperclass() != null) { - fields.addAll(getFields(clazz.getSuperclass())); - } - - return fields; - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/SQLLogger.java b/oldsrc/og/java/net/staticstudios/data/util/SQLLogger.java deleted file mode 100644 index 3035439c..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/SQLLogger.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.staticstudios.data.util; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class SQLLogger { - private final Logger logger = LoggerFactory.getLogger(getClass()); - - protected void logSQL(String sql) { - logger.debug("Executing SQL: {}", sql); - } -} diff --git a/oldsrc/og/java/net/staticstudios/data/util/ValueUpdateHandler.java b/oldsrc/og/java/net/staticstudios/data/util/ValueUpdateHandler.java deleted file mode 100644 index 043bbbbd..00000000 --- a/oldsrc/og/java/net/staticstudios/data/util/ValueUpdateHandler.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.staticstudios.data.util; - -public interface ValueUpdateHandler { - - void handle(ValueUpdate update); - - @SuppressWarnings("unchecked") - default void unsafeHandle(Object oldValue, Object newValue) { - handle(new ValueUpdate<>((T) oldValue, (T) newValue)); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/CachedValueTest.java b/oldsrc/ogtest/java/net/staticstudios/data/CachedValueTest.java deleted file mode 100644 index a53810c6..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/CachedValueTest.java +++ /dev/null @@ -1,226 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.primaryKey.RedisKey; -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; -import net.staticstudios.data.mock.cachedvalue.RedditUser; -import net.staticstudios.data.primative.Primitives; -import org.junit.jupiter.api.BeforeEach; -import org.junitpioneer.jupiter.RetryingTest; -import redis.clients.jedis.Jedis; - -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -public class CachedValueTest extends DataTest { - - //todo: test blocking #set calls - - @BeforeEach - public void init() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - drop schema if exists reddit cascade; - create schema if not exists reddit; - create table if not exists reddit.users ( - id uuid primary primaryKey - ); - """); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - getMockEnvironments().forEach(env -> { - DataManager dataManager = env.dataManager(); - dataManager.loadAll(RedditUser.class); - }); - } - - @RetryingTest(5) - public void testSetCachedValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - RedditUser user = RedditUser.createSync(dataManager); - - Jedis jedis = getJedis(); - - assertFalse(jedis.exists(user.status.getKey().toString())); //it's null by default - assertFalse(jedis.exists(user.lastLogin.getKey().toString())); //it's null by default - - user.setStatus("Hello, World!"); - assertEquals("Hello, World!", user.getStatus()); - - user.setLastLogin(Timestamp.from(Instant.EPOCH)); - assertEquals(Timestamp.from(Instant.EPOCH), user.getLastLogin()); - - waitForDataPropagation(); - - assertEquals("Hello, World!", jedis.get(user.status.getKey().toString())); - assertEquals(Primitives.encode(Timestamp.from(Instant.EPOCH)), jedis.get(user.lastLogin.getKey().toString())); - - user.setStatus("Goodbye, World!"); - assertEquals("Goodbye, World!", user.getStatus()); - - waitForDataPropagation(); - - assertEquals("Goodbye, World!", jedis.get(user.status.getKey().toString())); - - jedis.del(user.status.getKey().toString()); - - waitForDataPropagation(); - - assertNull(user.getStatus()); - - jedis.set(user.status.getKey().toString(), Primitives.encode("Hello, World!")); - - waitForDataPropagation(); - - assertEquals("Hello, World!", user.getStatus()); - } - - @RetryingTest(5) - public void testExpiringCachedValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - RedditUser user = RedditUser.createSync(dataManager); - - Jedis jedis = getJedis(); - - assertFalse(jedis.exists(user.suspended.getKey().toString())); - - user.setSuspended(true); - assertTrue(user.isSuspended()); - - waitForDataPropagation(); - - assertTrue(jedis.exists(user.suspended.getKey().toString())); - - //wait for the primaryKey to expire - try { - Thread.sleep(4000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - assertFalse(jedis.exists(user.suspended.getKey().toString())); - assertFalse(user.isSuspended()); - } - - @RetryingTest(5) - public void testFallbackValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - RedditUser user = RedditUser.createSync(dataManager); - - Jedis jedis = getJedis(); - - assertFalse(jedis.exists(user.bio.getKey().toString())); - - assertEquals("This user has not set a bio yet.", user.getBio()); - - user.setBio("Hello, World!"); - assertEquals("Hello, World!", user.getBio()); - - waitForDataPropagation(); - - assertEquals("Hello, World!", jedis.get(user.bio.getKey().toString())); - - jedis.del(user.bio.getKey().toString()); - - waitForDataPropagation(); - - assertEquals("This user has not set a bio yet.", user.getBio()); - - user.setBio("Goodbye, World!"); - assertEquals("Goodbye, World!", user.getBio()); - - user.setBio(null); - assertEquals("This user has not set a bio yet.", user.getBio()); - } - - @RetryingTest(5) - public void testUpdateHandler() { - //Note that update handlers are called async - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - RedditUser user = RedditUser.createSync(dataManager); - - Jedis jedis = getJedis(); - - assertEquals(0, user.getStatusUpdates()); - - user.setStatus("Hello, World!"); - waitForDataPropagation(); - assertEquals(1, user.getStatusUpdates()); - - // Unlike persistent values, we can't ignore updates from ourselves. - // However, the update handler won't be called when we get the notification from redis if the value is the same. - // So that's why we wait, to skip that additional call. - waitForDataPropagation(); - - user.setStatus("Goodbye, World!"); - waitForDataPropagation(); - assertEquals(2, user.getStatusUpdates()); - - waitForDataPropagation(); - - jedis.set(user.status.getKey().toString(), Primitives.encode("Hello, World!")); - - waitForDataPropagation(); - - assertEquals(3, user.getStatusUpdates()); - } - - @RetryingTest(5) - public void testLoading() { - List ids = new ArrayList<>(); - Jedis jedis = getJedis(); - try (Statement statement = getConnection().createStatement()) { - for (int i = 0; i < 10; i++) { - UUID id = UUID.randomUUID(); - ids.add(id); - statement.executeUpdate("insert into reddit.users (id) values ('" + id + "')"); - RedisKey primaryKey = new RedisKey("reddit", "users", "id", id, "status"); - jedis.set(primaryKey.toString(), Primitives.encode("Hey, I'm user " + i)); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - - MockEnvironment environment = createMockEnvironment(); - getMockEnvironments().add(environment); - - DataManager dataManager = environment.dataManager(); - - dataManager.loadAll(RedditUser.class); - assertEquals(10, dataManager.getAll(RedditUser.class).size()); - - for (UUID id : ids) { - RedditUser user = dataManager.get(RedditUser.class, id); - assertEquals("Hey, I'm user " + ids.indexOf(id), user.getStatus()); - } - } - - @Override - public void waitForDataPropagation() { - //Redis is taking a while to update so we need to wait a bit longer - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - super.waitForDataPropagation(); - } -} \ No newline at end of file diff --git a/oldsrc/ogtest/java/net/staticstudios/data/DeletionTest.java b/oldsrc/ogtest/java/net/staticstudios/data/DeletionTest.java deleted file mode 100644 index f243bd3d..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/DeletionTest.java +++ /dev/null @@ -1,301 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.data.collection.SimplePersistentCollection; -import net.staticstudios.data.primaryKey.RedisKey; -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; -import net.staticstudios.data.misc.TestUtils; -import net.staticstudios.data.mock.deletions.*; -import org.junit.jupiter.api.BeforeEach; -import org.junitpioneer.jupiter.RetryingTest; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Timestamp; - -import static org.junit.jupiter.api.Assertions.*; - -public class DeletionTest extends DataTest { - @BeforeEach - public void init() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - drop schema if exists minecraft cascade; - create schema if not exists minecraft; - create table if not exists minecraft.users ( - id uuid primary primaryKey, - name text not null - ); - create table if not exists minecraft.user_meta ( - id uuid primary primaryKey, - account_creation timestamp not null - ); - create table if not exists minecraft.user_stats ( - id uuid primary primaryKey - ); - create table if not exists minecraft.servers ( - id uuid primary primaryKey, - name text not null - ); - create table if not exists minecraft.skins ( - id uuid primary primaryKey, - user_id uuid, - name text not null - ); - create table if not exists minecraft.user_servers ( - user_id uuid not null, - server_id uuid not null, - primary primaryKey (user_id, server_id) - ); - create table if not exists minecraft.worlds ( - id uuid primary primaryKey, - user_id uuid not null, - name text not null - ); - """); - getJedis().del("*"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testCascadeDeletionStrategy() { - getMockEnvironments().forEach(env -> { - DataManager dataManager = env.dataManager(); - dataManager.loadAll(MinecraftUserWithCascadeDeletionStrategy.class); - }); - - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - int initialCacheSize = dataManager.getCacheSize(); - - MinecraftUserWithCascadeDeletionStrategy user = MinecraftUserWithCascadeDeletionStrategy.createSync(dataManager, "Steve", "0.0.0.0"); - MinecraftServer server1 = MinecraftServer.createSync(dataManager, "Server 1"); - MinecraftServer server2 = MinecraftServer.createSync(dataManager, "Server 2"); - MinecraftSkin skin1 = MinecraftSkin.createSync(dataManager, "Skin 1"); - MinecraftSkin skin2 = MinecraftSkin.createSync(dataManager, "Skin 2"); - - user.servers.add(server1); - user.servers.add(server2); - user.skins.add(skin1); - user.skins.add(skin2); - user.worldNames.add("World 1"); - user.worldNames.add("World 2"); - - waitForDataPropagation(); - - //Validate the data exists in the database - try (Statement statement = getConnection().createStatement()) { - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.users where id = '" + user.getId() + "'"))); - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_meta where id = '" + user.getId() + "'"))); - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_stats where id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.servers join minecraft.user_servers on servers.id = user_servers.server_id where user_servers.user_id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.skins where user_id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.worlds where user_id = '" + user.getId() + "'"))); - } catch (SQLException e) { - throw new RuntimeException(e); - } - RedisKey rs = user.ipAddress.getKey(); - assertEquals("0.0.0.0", getJedis().get(rs.toString())); - dataManager.delete(user); - - assertNull(dataManager.get(MinecraftUserWithCascadeDeletionStrategy.class, user.getId())); - assertNull(dataManager.get(MinecraftUserStatistics.class, user.getId())); - assertNull(dataManager.get(MinecraftServer.class, server1.getId())); - assertNull(dataManager.get(MinecraftServer.class, server2.getId())); - assertNull(dataManager.get(MinecraftSkin.class, skin1.getId())); - assertNull(dataManager.get(MinecraftSkin.class, skin2.getId())); - assertEquals(0, dataManager.getPersistentCollectionManager().getCollectionEntries((SimplePersistentCollection) user.worldNames).size()); - - assertEquals(initialCacheSize, dataManager.getCacheSize()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.users"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_meta"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_stats"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_servers"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.servers"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.skins"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.worlds"))); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - assertEquals(0, getJedis().keys(rs.toString()).size()); - } - - @RetryingTest(5) - public void testNoActionDeletionStrategy() { - getMockEnvironments().forEach(env -> { - DataManager dataManager = env.dataManager(); - dataManager.loadAll(MinecraftUserWithNoActionDeletionStrategy.class); - }); - - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - MinecraftUserWithNoActionDeletionStrategy user = MinecraftUserWithNoActionDeletionStrategy.createSync(dataManager, "Steve", "0.0.0.0"); - MinecraftUserStatistics stats = user.statistics.get(); - Timestamp accountCreation = user.accountCreation.get(); - MinecraftServer server1 = MinecraftServer.createSync(dataManager, "Server 1"); - MinecraftServer server2 = MinecraftServer.createSync(dataManager, "Server 2"); - MinecraftSkin skin1 = MinecraftSkin.createSync(dataManager, "Skin 1"); - MinecraftSkin skin2 = MinecraftSkin.createSync(dataManager, "Skin 2"); - - user.servers.add(server1); - user.servers.add(server2); - user.skins.add(skin1); - user.skins.add(skin2); - user.worldNames.add("World 1"); - user.worldNames.add("World 2"); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.users where id = '" + user.getId() + "'"))); - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_meta where id = '" + user.getId() + "'"))); - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_stats where id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.servers join minecraft.user_servers on servers.id = user_servers.server_id where user_servers.user_id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.skins where user_id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.worlds where user_id = '" + user.getId() + "'"))); - } catch (SQLException e) { - throw new RuntimeException(e); - } - RedisKey redisKey = user.ipAddress.getKey(); - assertEquals("0.0.0.0", getJedis().get(redisKey.toString())); - - dataManager.delete(user); - - assertNull(dataManager.get(MinecraftUserWithNoActionDeletionStrategy.class, user.getId())); - assertEquals(stats, dataManager.get(MinecraftUserStatistics.class, user.getId())); - assertEquals(2, user.servers.size()); - assertEquals(2, user.skins.size()); - assertEquals(2, user.worldNames.size()); - assertEquals("0.0.0.0", user.ipAddress.get()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.users where id = '" + user.getId() + "'"))); - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_meta where id = '" + user.getId() + "'"))); - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_stats where id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.servers join minecraft.user_servers on servers.id = user_servers.server_id where user_servers.user_id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.skins where user_id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.worlds where user_id = '" + user.getId() + "'"))); - - ResultSet rs; - rs = statement.executeQuery("select * from minecraft.user_meta where id = '" + user.getId() + "'"); - rs.next(); - assertEquals(accountCreation.toInstant().toEpochMilli(), rs.getTimestamp("account_creation").toInstant().toEpochMilli()); //Use mills to account for different amounts of precision - rs.close(); - rs = statement.executeQuery("select * from minecraft.user_stats where id = '" + user.getId() + "'"); - rs.next(); - assertEquals(user.getId(), rs.getObject("id")); - rs.close(); - rs = statement.executeQuery("select * from minecraft.servers join minecraft.user_servers on servers.id = user_servers.server_id where user_servers.user_id = '" + user.getId() + "'"); - rs.next(); - assertEquals(server1.getId(), rs.getObject("id")); - rs.next(); - assertEquals(server2.getId(), rs.getObject("id")); - rs.close(); - rs = statement.executeQuery("select * from minecraft.skins where user_id = '" + user.getId() + "'"); - rs.next(); - assertEquals(skin1.getId(), rs.getObject("id")); - rs.next(); - assertEquals(skin2.getId(), rs.getObject("id")); - rs.close(); - rs = statement.executeQuery("select * from minecraft.worlds where user_id = '" + user.getId() + "'"); - rs.next(); - assertEquals("World 1", rs.getString("name")); - rs.next(); - assertEquals("World 2", rs.getString("name")); - rs.close(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - assertEquals("0.0.0.0", getJedis().get(redisKey.toString())); - } - - @RetryingTest(5) - public void testUnlinkDeletionStrategy() { - //Note that DeletionStrategy.UNLINK acts like DeletionStrategy.CASCADE, for everything but PersistentUniqueDataCollections & PersistentManyToManyCollections - getMockEnvironments().forEach(env -> { - DataManager dataManager = env.dataManager(); - dataManager.loadAll(MinecraftUserWithUnlinkDeletionStrategy.class); - }); - - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - MinecraftUserWithUnlinkDeletionStrategy user = MinecraftUserWithUnlinkDeletionStrategy.createSync(dataManager, "Steve", "0.0.0.0"); - MinecraftServer server1 = MinecraftServer.createSync(dataManager, "Server 1"); - MinecraftServer server2 = MinecraftServer.createSync(dataManager, "Server 2"); - MinecraftSkin skin1 = MinecraftSkin.createSync(dataManager, "Skin 1"); - MinecraftSkin skin2 = MinecraftSkin.createSync(dataManager, "Skin 2"); - - user.servers.add(server1); - user.servers.add(server2); - user.skins.add(skin1); - user.skins.add(skin2); - user.worldNames.add("World 1"); - user.worldNames.add("World 2"); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.users where id = '" + user.getId() + "'"))); - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_meta where id = '" + user.getId() + "'"))); - assertEquals(1, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_stats where id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.servers join minecraft.user_servers on servers.id = user_servers.server_id where user_servers.user_id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.skins where user_id = '" + user.getId() + "'"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.worlds where user_id = '" + user.getId() + "'"))); - } catch (SQLException e) { - throw new RuntimeException(e); - } - RedisKey redisKey = user.ipAddress.getKey(); - assertEquals("0.0.0.0", getJedis().get(redisKey.toString())); - - dataManager.delete(user); - - assertNull(dataManager.get(MinecraftUserWithUnlinkDeletionStrategy.class, user.getId())); - assertNull(dataManager.get(MinecraftUserStatistics.class, user.getId())); - assertNotNull(dataManager.get(MinecraftServer.class, server1.getId())); - assertNotNull(dataManager.get(MinecraftServer.class, server2.getId())); - assertNotNull(dataManager.get(MinecraftSkin.class, skin1.getId())); - assertNotNull(dataManager.get(MinecraftSkin.class, skin2.getId())); - assertEquals(0, dataManager.getPersistentCollectionManager().getCollectionEntries((SimplePersistentCollection) user.worldNames).size()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.users where id = '" + user.getId() + "'"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_meta where id = '" + user.getId() + "'"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_stats where id = '" + user.getId() + "'"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.servers join minecraft.user_servers on servers.id = user_servers.server_id where user_servers.user_id = '" + user.getId() + "'"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.skins where user_id = '" + user.getId() + "'"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.worlds where user_id = '" + user.getId() + "'"))); - - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.servers"))); - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.skins"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.worlds"))); - assertEquals(0, TestUtils.getResultCount(statement.executeQuery("select * from minecraft.user_servers"))); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - assertEquals(0, getJedis().keys(redisKey.toString()).size()); - } - - - //other tests: - //todo: test insert - //todo: test insertAsync - //todo: test value serializers - -} \ No newline at end of file diff --git a/oldsrc/ogtest/java/net/staticstudios/data/InsertionTest.java b/oldsrc/ogtest/java/net/staticstudios/data/InsertionTest.java deleted file mode 100644 index 57f90afb..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/InsertionTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; -import net.staticstudios.data.misc.TestUtils; -import net.staticstudios.data.mock.insertions.TwitchChatMessage; -import net.staticstudios.data.mock.insertions.TwitchUser; -import net.staticstudios.data.util.BatchInsert; -import org.junit.jupiter.api.BeforeEach; -import org.junitpioneer.jupiter.RetryingTest; - -import java.sql.SQLException; -import java.sql.Statement; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -public class InsertionTest extends DataTest { - @BeforeEach - public void init() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - drop schema if exists twitch cascade; - create schema if not exists twitch; - create table if not exists twitch.users ( - id uuid primary primaryKey, - name text not null - ); - create table if not exists twitch.chat_messages ( - id uuid primary primaryKey, - sender_id uuid not null references twitch.users(id) on delete set null deferrable initially deferred - ); - """); - - for (MockEnvironment environment : getMockEnvironments()) { - environment.dataManager().loadAll(TwitchUser.class); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBatchInsert() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - BatchInsert batch = dataManager.batchInsert(); - TwitchUser user = TwitchUser.enqueueCreation(batch, dataManager, "test user"); - TwitchChatMessage message1 = TwitchChatMessage.enqueueCreation(batch, dataManager, user); - TwitchChatMessage message2 = TwitchChatMessage.enqueueCreation(batch, dataManager, user); - batch.insert(); - - assertNotNull(user.getId()); - assertNotNull(message1.getId()); - assertNotNull(message2.getId()); - assertEquals(user, message1.sender.get()); - assertEquals(user, message2.sender.get()); - assertEquals(2, user.messages.size()); - - try (Statement statement = getConnection().createStatement()) { - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("select * from twitch.chat_messages where sender_id = '" + user.getId() + "'"))); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - } - -} \ No newline at end of file diff --git a/oldsrc/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java b/oldsrc/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java deleted file mode 100644 index 0704a5bf..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/PersistentCollectionTest.java +++ /dev/null @@ -1,2382 +0,0 @@ -package net.staticstudios.data; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; -import net.staticstudios.data.misc.TestUtils; -import net.staticstudios.data.mock.persistentcollection.FacebookPost; -import net.staticstudios.data.mock.persistentcollection.FacebookUser; -import org.junit.jupiter.api.BeforeEach; -import org.junitpioneer.jupiter.RetryingTest; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -public class PersistentCollectionTest extends DataTest { - - @BeforeEach - public void init() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - drop schema if exists facebook cascade; - create schema if not exists facebook; - create table if not exists facebook.users ( - id uuid primary primaryKey - ); - create table if not exists facebook.posts ( - id uuid primary primaryKey, - user_id uuid, - description text not null default '', - likes int not null default 0 - ); - create table if not exists facebook.favorite_quotes ( - id uuid primary primaryKey, - user_id uuid, - quote text not null default '' - ); - create table if not exists facebook.user_following ( - user_id uuid, - following_id uuid - ); - create table if not exists facebook.user_liked_posts ( - user_id uuid, - post_id uuid - ); - """); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - getMockEnvironments().forEach(env -> { - DataManager dataManager = env.dataManager(); - dataManager.loadAll(FacebookUser.class); - }); - } - - @RetryingTest(5) - public void testAddToUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", null); - facebookUser.getPosts().add(post3); - - assertEquals(3, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post3)); - assertEquals(facebookUser, post3.getUser()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertEquals(facebookUser.getId(), resultSet.getObject("user_id")); - assertEquals("Here's some post description", resultSet.getString("description")); - assertEquals(0, resultSet.getInt("likes")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post3.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertEquals(facebookUser.getId(), resultSet.getObject("user_id")); - assertEquals("Here's some post description", resultSet.getString("description")); - assertEquals(0, resultSet.getInt("likes")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingAddToUniqueDataCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", null); - facebookUser.getPosts().addNow(post3); - - assertEquals(3, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post3)); - assertEquals(facebookUser, post3.getUser()); - - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertEquals(facebookUser.getId(), resultSet.getObject("user_id")); - assertEquals("Here's some post description", resultSet.getString("description")); - assertEquals(0, resultSet.getInt("likes")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post3.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertEquals(facebookUser.getId(), resultSet.getObject("user_id")); - assertEquals("Here's some post description", resultSet.getString("description")); - assertEquals(0, resultSet.getInt("likes")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testAddAllToUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", null); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", null); - - assertEquals(0, facebookUser.getPosts().size()); - - facebookUser.getPosts().addAll(List.of(post1, post2)); - - assertEquals(2, facebookUser.getPosts().size()); - - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertEquals(facebookUser.getId(), resultSet.getObject("user_id")); - assertEquals("Here's some post description", resultSet.getString("description")); - assertEquals(0, resultSet.getInt("likes")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingAddAllToUniqueDataCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", null); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", null); - - assertEquals(0, facebookUser.getPosts().size()); - - facebookUser.getPosts().addAllNow(List.of(post1, post2)); - - assertEquals(2, facebookUser.getPosts().size()); - - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertEquals(facebookUser.getId(), resultSet.getObject("user_id")); - assertEquals("Here's some post description", resultSet.getString("description")); - assertEquals(0, resultSet.getInt("likes")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testRemoveFromUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - - waitForDataPropagation(); - - assertTrue(facebookUser.getPosts().remove(post1)); - - assertNull(post1.getUser()); - - assertEquals(1, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post2.getUser()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertNull(resultSet.getObject("user_id")); - assertEquals("Here's some post description", resultSet.getString("description")); - assertEquals(0, resultSet.getInt("likes")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - try (PreparedStatement statement = getConnection().prepareStatement("update facebook.posts set user_id = null where id = ?")) { - statement.setObject(1, post2.getId()); - statement.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertFalse(facebookUser.getPosts().contains(post2)); - assertNull(post2.getUser()); - } - - @RetryingTest(5) - public void testRemoveFromUniqueDataCollectionViaDeletion() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(3, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertTrue(facebookUser.getPosts().contains(post3)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - assertEquals(facebookUser, post3.getUser()); - - dataManager.delete(post3); - - assertEquals(2, facebookUser.getPosts().size()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - assertEquals(2, TestUtils.getResultCount(statement.executeQuery("SELECT * FROM facebook.posts WHERE user_id = '" + facebookUser.getId() + "'"))); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("DELETE FROM facebook.posts WHERE id = '" + post1.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(1, facebookUser.getPosts().size()); - } - - @RetryingTest(5) - public void testBlockingRemoveFromUniqueDataCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - - assertTrue(facebookUser.getPosts().removeNow(post1)); - - assertEquals(1, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post2.getUser()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertNull(resultSet.getObject("user_id")); - assertEquals("Here's some post description", resultSet.getString("description")); - assertEquals(0, resultSet.getInt("likes")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testRemoveAllFromUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(3, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertTrue(facebookUser.getPosts().contains(post3)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - assertEquals(facebookUser, post3.getUser()); - - waitForDataPropagation(); - - assertTrue(facebookUser.getPosts().removeAll(List.of(post1, post2))); - - assertEquals(1, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post3)); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingRemoveAllFromUniqueDataCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(3, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertTrue(facebookUser.getPosts().contains(post3)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - assertEquals(facebookUser, post3.getUser()); - - assertTrue(facebookUser.getPosts().removeAllNow(List.of(post1, post2))); - - assertEquals(1, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post3)); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testClearInUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - - facebookUser.getPosts().clear(); - - assertEquals(0, facebookUser.getPosts().size()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertNull(resultSet.getObject("user_id")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingClearInUniqueDataCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertEquals(facebookUser, post1.getUser()); - assertEquals(facebookUser, post2.getUser()); - - facebookUser.getPosts().clearNow(); - - assertEquals(0, facebookUser.getPosts().size()); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE id = ?")) { - statement.setObject(1, post1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertTrue(resultSet.next()); - assertNull(resultSet.getObject("user_id")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testContainsInUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(1, facebookUser.getPosts().size()); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", null); - facebookUser.getPosts().add(post2); - assertEquals(2, facebookUser.getPosts().size()); - - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", null); - - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - - assertFalse(facebookUser.getPosts().contains(post3)); - assertFalse(facebookUser.getPosts().contains("Not a post")); - assertFalse(facebookUser.getPosts().contains(null)); - } - - @RetryingTest(5) - public void testContainsAllInUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", null); - - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertTrue(facebookUser.getPosts().containsAll(List.of(post1, post2))); - assertFalse(facebookUser.getPosts().containsAll(List.of(post1, post2, post3))); - - assertFalse(facebookUser.getPosts().containsAll(List.of(post1, post2, "Not a post"))); - List bad = new ArrayList<>(); - bad.add(post1); - bad.add(post2); - bad.add(null); - assertFalse(facebookUser.getPosts().containsAll(bad)); - assertTrue(facebookUser.getPosts().containsAll(List.of())); - } - - @RetryingTest(5) - public void testRetainAllInUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", null); - - assertEquals(2, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertTrue(facebookUser.getPosts().contains(post2)); - assertFalse(facebookUser.getPosts().contains(post3)); - - assertTrue(facebookUser.getPosts().retainAll(List.of(post1, post3))); - assertEquals(1, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertFalse(facebookUser.getPosts().contains(post2)); - assertFalse(facebookUser.getPosts().contains(post3)); - - assertFalse(facebookUser.getPosts().retainAll(List.of(post1))); - assertEquals(1, facebookUser.getPosts().size()); - assertTrue(facebookUser.getPosts().contains(post1)); - assertFalse(facebookUser.getPosts().contains(post2)); - assertFalse(facebookUser.getPosts().contains(post3)); - } - - @RetryingTest(5) - public void testToArrayInUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - - FacebookPost[] posts = facebookUser.getPosts().toArray(new FacebookPost[0]); - assertEquals(2, posts.length); - boolean containsPost1 = false; - boolean containsPost2 = false; - - for (FacebookPost post : posts) { - if (post.equals(post1)) { - containsPost1 = true; - } else if (post.equals(post2)) { - containsPost2 = true; - } - } - - assertTrue(containsPost1); - assertTrue(containsPost2); - - Object[] objects = facebookUser.getPosts().toArray(); - assertEquals(2, objects.length); - boolean containsPost1Object = false; - boolean containsPost2Object = false; - - for (Object object : objects) { - if (object.equals(post1)) { - containsPost1Object = true; - } else if (object.equals(post2)) { - containsPost2Object = true; - } - } - - assertTrue(containsPost1Object); - assertTrue(containsPost2Object); - } - - @RetryingTest(5) - public void testIteratorInUniqueDataCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - - Iterator iterator = facebookUser.getPosts().iterator(); - assertTrue(iterator.hasNext()); - FacebookPost next = iterator.next(); - assertTrue(next.equals(post1) || next.equals(post2)); - assertTrue(iterator.hasNext()); - next = iterator.next(); - assertTrue(next.equals(post1) || next.equals(post2)); - assertFalse(iterator.hasNext()); - - iterator = facebookUser.getPosts().iterator(); - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(1, facebookUser.getPosts().size()); - - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(0, facebookUser.getPosts().size()); - - assertFalse(iterator.hasNext()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingIteratorInUniqueDataCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - - assertEquals(2, facebookUser.getPosts().size()); - - Iterator iterator = facebookUser.getPosts().blockingIterator(); - assertTrue(iterator.hasNext()); - FacebookPost next = iterator.next(); - assertTrue(next.equals(post1) || next.equals(post2)); - assertTrue(iterator.hasNext()); - next = iterator.next(); - assertTrue(next.equals(post1) || next.equals(post2)); - assertFalse(iterator.hasNext()); - - iterator = facebookUser.getPosts().blockingIterator(); - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(1, facebookUser.getPosts().size()); - - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(0, facebookUser.getPosts().size()); - - assertFalse(iterator.hasNext()); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.posts WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testLoadingUniqueDataCollection() { - List ids = new ArrayList<>(); - try (Statement statement = getConnection().createStatement()) { - for (int i = 0; i < 10; i++) { - UUID id = UUID.randomUUID(); - ids.add(id); - statement.executeUpdate("insert into facebook.users (id) values ('" + id + "')"); - statement.executeUpdate("insert into facebook.posts (id, user_id, description) values ('" + UUID.randomUUID() + "', '" + id + "', 'post - " + id + "')"); - statement.executeUpdate("insert into facebook.posts (id, user_id, description) values ('" + UUID.randomUUID() + "', '" + id + "', 'post 2 - " + id + "')"); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - - MockEnvironment environment = createMockEnvironment(); - getMockEnvironments().add(environment); - - DataManager dataManager = environment.dataManager(); - dataManager.loadAll(FacebookUser.class); - - for (UUID id : ids) { - FacebookUser user = dataManager.get(FacebookUser.class, id); - assertEquals(2, user.getPosts().size()); - assertTrue(user.getPosts().stream().anyMatch(post -> post.getDescription().equals("post - " + id))); - assertTrue(user.getPosts().stream().anyMatch(post -> post.getDescription().equals("post 2 - " + id))); - } - } - - @RetryingTest(5) - public void testUpdatingUniqueDataCollectionInDatabase() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - assertEquals(0, facebookUser.getPosts().size()); - assertEquals(0, facebookUser.postAdditions.get()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("insert into facebook.posts (id, user_id, description) values ('" + UUID.randomUUID() + "', '" + facebookUser.getId() + "', 'post - " + facebookUser.getId() + "')"); - statement.executeUpdate("insert into facebook.posts (id, user_id, description) values ('" + UUID.randomUUID() + "', '" + facebookUser.getId() + "', 'post 2 - " + facebookUser.getId() + "')"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(2, facebookUser.getPosts().size()); - assertEquals(2, facebookUser.postAdditions.get()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update facebook.posts set user_id = null where user_id = '" + facebookUser.getId() + "' and id = (select id from facebook.posts where user_id = '" + facebookUser.getId() + "' limit 1)"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(1, facebookUser.getPosts().size()); - assertEquals(2, facebookUser.postAdditions.get()); - assertEquals(1, facebookUser.postRemovals.get()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("delete from facebook.posts where user_id = '" + facebookUser.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(0, facebookUser.getPosts().size()); - assertEquals(2, facebookUser.postAdditions.get()); - assertEquals(2, facebookUser.postRemovals.get()); - } - - // ----- Test PersistentValueCollections ----- // - - @RetryingTest(5) - public void testAddToValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List quotes = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - quotes.add(resultSet.getString("quote")); - } - - assertEquals(3, quotes.size()); - assertTrue(quotes.contains("Here's a quote")); - assertTrue(quotes.contains("Here's another quote")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingAddToValueCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().addNow("Here's a quote"); - facebookUser.getFavoriteQuotes().addNow("Here's a quote"); - facebookUser.getFavoriteQuotes().addNow("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List quotes = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - quotes.add(resultSet.getString("quote")); - } - - assertEquals(3, quotes.size()); - assertTrue(quotes.contains("Here's a quote")); - assertTrue(quotes.contains("Here's another quote")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testAddAllToValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().addAll(List.of("Here's a quote", "Here's a quote", "Here's another quote")); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List quotes = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - quotes.add(resultSet.getString("quote")); - } - - assertEquals(3, quotes.size()); - assertTrue(quotes.contains("Here's a quote")); - assertTrue(quotes.contains("Here's another quote")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingAddAllToValueCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().addAllNow(List.of("Here's a quote", "Here's a quote", "Here's another quote")); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List quotes = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - quotes.add(resultSet.getString("quote")); - } - - assertEquals(3, quotes.size()); - assertTrue(quotes.contains("Here's a quote")); - assertTrue(quotes.contains("Here's another quote")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testRemoveFromValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertTrue(facebookUser.getFavoriteQuotes().remove("Here's a quote")); - - assertEquals(2, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertTrue(facebookUser.getFavoriteQuotes().remove("Here's a quote")); - - assertEquals(1, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertFalse(facebookUser.getFavoriteQuotes().remove("Here's a quote")); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List quotes = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - quotes.add(resultSet.getString("quote")); - } - - assertEquals(1, quotes.size()); - assertTrue(quotes.contains("Here's another quote")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingRemoveFromValueCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().addNow("Here's a quote"); - facebookUser.getFavoriteQuotes().addNow("Here's a quote"); - facebookUser.getFavoriteQuotes().addNow("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertTrue(facebookUser.getFavoriteQuotes().removeNow("Here's a quote")); - - assertEquals(2, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertTrue(facebookUser.getFavoriteQuotes().removeNow("Here's a quote")); - - assertEquals(1, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertFalse(facebookUser.getFavoriteQuotes().removeNow("Here's a quote")); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List quotes = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - quotes.add(resultSet.getString("quote")); - } - - assertEquals(1, quotes.size()); - assertTrue(quotes.contains("Here's another quote")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testRemoveAllFromValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertTrue(facebookUser.getFavoriteQuotes().removeAll(List.of("Here's a quote", "Here's another quote"))); - - assertEquals(1, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List quotes = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - quotes.add(resultSet.getString("quote")); - } - - assertEquals(1, quotes.size()); - assertTrue(quotes.contains("Here's a quote")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingRemoveAllFromValueCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().addNow("Here's a quote"); - facebookUser.getFavoriteQuotes().addNow("Here's a quote"); - facebookUser.getFavoriteQuotes().addNow("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertTrue(facebookUser.getFavoriteQuotes().removeAllNow(List.of("Here's a quote", "Here's another quote"))); - - assertEquals(1, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List quotes = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - quotes.add(resultSet.getString("quote")); - } - - assertEquals(1, quotes.size()); - assertTrue(quotes.contains("Here's a quote")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testClearInValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - facebookUser.getFavoriteQuotes().clear(); - - assertEquals(0, facebookUser.getFavoriteQuotes().size()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingClearInValueCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().addNow("Here's a quote"); - facebookUser.getFavoriteQuotes().addNow("Here's a quote"); - facebookUser.getFavoriteQuotes().addNow("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - facebookUser.getFavoriteQuotes().clearNow(); - - assertEquals(0, facebookUser.getFavoriteQuotes().size()); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testContainsInValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertFalse(facebookUser.getFavoriteQuotes().contains("Not a quote")); - assertFalse(facebookUser.getFavoriteQuotes().contains(null)); - } - - @RetryingTest(5) - public void testContainsAllInValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertTrue(facebookUser.getFavoriteQuotes().containsAll(List.of("Here's a quote", "Here's another quote"))); - assertFalse(facebookUser.getFavoriteQuotes().containsAll(List.of("Here's a quote", "Here's another quote", "Not a quote"))); - List bad = new ArrayList<>(); - bad.add("Here's a quote"); - bad.add("Here's another quote"); - bad.add(null); - assertFalse(facebookUser.getFavoriteQuotes().containsAll(bad)); - assertTrue(facebookUser.getFavoriteQuotes().containsAll(List.of())); - } - - @RetryingTest(5) - public void testRetainAllInValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertFalse(facebookUser.getFavoriteQuotes().retainAll(List.of("Here's a quote", "Here's another quote"))); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - - assertTrue(facebookUser.getFavoriteQuotes().retainAll(List.of("Here's a quote"))); - - assertEquals(2, facebookUser.getFavoriteQuotes().size()); - assertTrue(facebookUser.getFavoriteQuotes().contains("Here's a quote")); - assertFalse(facebookUser.getFavoriteQuotes().contains("Here's another quote")); - } - - @RetryingTest(5) - public void testToArrayInValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - - String[] quotes = facebookUser.getFavoriteQuotes().toArray(new String[0]); - assertEquals(3, quotes.length); - int quote1Count = 0; - boolean containsQuote2 = false; - - for (String quote : quotes) { - if (quote.equals("Here's a quote")) { - quote1Count++; - } else if (quote.equals("Here's another quote")) { - containsQuote2 = true; - } - } - - assertEquals(2, quote1Count); - assertTrue(containsQuote2); - - Object[] objects = facebookUser.getFavoriteQuotes().toArray(); - assertEquals(3, objects.length); - int quote1CountObject = 0; - boolean containsQuote2Object = false; - - for (Object object : objects) { - if (object.equals("Here's a quote")) { - quote1CountObject++; - } else if (object.equals("Here's another quote")) { - containsQuote2Object = true; - } - } - - assertEquals(2, quote1CountObject); - assertTrue(containsQuote2Object); - } - - @RetryingTest(5) - public void testIteratorInValueCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - - Iterator iterator = facebookUser.getFavoriteQuotes().iterator(); - assertTrue(iterator.hasNext()); - String next = iterator.next(); - assertTrue(next.equals("Here's a quote") || next.equals("Here's another quote")); - assertTrue(iterator.hasNext()); - next = iterator.next(); - assertTrue(next.equals("Here's a quote") || next.equals("Here's another quote")); - assertTrue(iterator.hasNext()); - next = iterator.next(); - assertTrue(next.equals("Here's a quote") || next.equals("Here's another quote")); - assertFalse(iterator.hasNext()); - - iterator = facebookUser.getFavoriteQuotes().iterator(); - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(2, facebookUser.getFavoriteQuotes().size()); - - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(1, facebookUser.getFavoriteQuotes().size()); - - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(0, facebookUser.getFavoriteQuotes().size()); - - assertFalse(iterator.hasNext()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingIteratorInValueCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - //Duplicates are permitted - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - - assertEquals(3, facebookUser.getFavoriteQuotes().size()); - - Iterator iterator = facebookUser.getFavoriteQuotes().blockingIterator(); - assertTrue(iterator.hasNext()); - String next = iterator.next(); - assertTrue(next.equals("Here's a quote") || next.equals("Here's another quote")); - assertTrue(iterator.hasNext()); - next = iterator.next(); - assertTrue(next.equals("Here's a quote") || next.equals("Here's another quote")); - assertTrue(iterator.hasNext()); - next = iterator.next(); - assertTrue(next.equals("Here's a quote") || next.equals("Here's another quote")); - assertFalse(iterator.hasNext()); - - iterator = facebookUser.getFavoriteQuotes().blockingIterator(); - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(2, facebookUser.getFavoriteQuotes().size()); - - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(1, facebookUser.getFavoriteQuotes().size()); - - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(0, facebookUser.getFavoriteQuotes().size()); - - assertFalse(iterator.hasNext()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.favorite_quotes WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testLoadingValueCollection() { - List ids = new ArrayList<>(); - try (Statement statement = getConnection().createStatement()) { - for (int i = 0; i < 10; i++) { - UUID id = UUID.randomUUID(); - ids.add(id); - statement.executeUpdate("insert into facebook.users (id) values ('" + id + "')"); - statement.executeUpdate("insert into facebook.favorite_quotes (id, user_id, quote) values ('" + UUID.randomUUID() + "', '" + id + "', 'quote - " + id + "')"); - statement.executeUpdate("insert into facebook.favorite_quotes (id, user_id, quote) values ('" + UUID.randomUUID() + "', '" + id + "', 'quote 2 - " + id + "')"); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - - MockEnvironment environment = createMockEnvironment(); - getMockEnvironments().add(environment); - - DataManager dataManager = environment.dataManager(); - dataManager.loadAll(FacebookUser.class); - - for (UUID id : ids) { - FacebookUser user = dataManager.get(FacebookUser.class, id); - assertEquals(2, user.getFavoriteQuotes().size()); - assertTrue(user.getFavoriteQuotes().contains("quote - " + id)); - assertTrue(user.getFavoriteQuotes().contains("quote 2 - " + id)); - } - } - - @RetryingTest(5) - public void testUpdatingValueCollectionInDatabase() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - assertEquals(0, facebookUser.getFavoriteQuotes().size()); - assertEquals(0, facebookUser.favoriteQuoteAdditions.get()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("insert into facebook.favorite_quotes (id, user_id, quote) values ('" + UUID.randomUUID() + "', '" + facebookUser.getId() + "', 'quote - " + facebookUser.getId() + "')"); - statement.executeUpdate("insert into facebook.favorite_quotes (id, user_id, quote) values ('" + UUID.randomUUID() + "', '" + facebookUser.getId() + "', 'quote 2 - " + facebookUser.getId() + "')"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(2, facebookUser.getFavoriteQuotes().size()); - assertEquals(2, facebookUser.favoriteQuoteAdditions.get()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update facebook.favorite_quotes set user_id = null where user_id = '" + facebookUser.getId() + "' and id = (select id from facebook.favorite_quotes where user_id = '" + facebookUser.getId() + "' limit 1)"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(1, facebookUser.getFavoriteQuotes().size()); - assertEquals(2, facebookUser.favoriteQuoteAdditions.get()); - assertEquals(1, facebookUser.favoriteQuoteRemovals.get()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("delete from facebook.favorite_quotes where user_id = '" + facebookUser.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(0, facebookUser.getFavoriteQuotes().size()); - assertEquals(2, facebookUser.favoriteQuoteAdditions.get()); - assertEquals(2, facebookUser.favoriteQuoteRemovals.get()); - } - - // ----- Test PersistentManyToManyCollections ----- // - - @RetryingTest(5) - public void testAddToManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().add(following1); - assertEquals(1, facebookUser.getFollowing().size()); - facebookUser.getFollowing().add(following2); - assertEquals(2, facebookUser.getFollowing().size()); - - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - assertTrue(following1.getFollowers().contains(facebookUser)); - assertTrue(following2.getFollowers().contains(facebookUser)); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List followingIds = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - followingIds.add(resultSet.getObject("following_id", UUID.class)); - } - - assertEquals(2, followingIds.size()); - assertTrue(followingIds.contains(following1.getId())); - assertTrue(followingIds.contains(following2.getId())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingAddToManyToManyCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().addNow(following1); - assertEquals(1, facebookUser.getFollowing().size()); - facebookUser.getFollowing().addNow(following2); - assertEquals(2, facebookUser.getFollowing().size()); - - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - assertTrue(following1.getFollowers().contains(facebookUser)); - assertTrue(following2.getFollowers().contains(facebookUser)); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List followingIds = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - followingIds.add(resultSet.getObject("following_id", UUID.class)); - } - - assertEquals(2, followingIds.size()); - assertTrue(followingIds.contains(following1.getId())); - assertTrue(followingIds.contains(following2.getId())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testAddAllToManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().addAll(List.of(following1, following2)); - assertEquals(2, facebookUser.getFollowing().size()); - - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - assertTrue(following1.getFollowers().contains(facebookUser)); - assertTrue(following2.getFollowers().contains(facebookUser)); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List followingIds = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - followingIds.add(resultSet.getObject("following_id", UUID.class)); - } - - assertEquals(2, followingIds.size()); - assertTrue(followingIds.contains(following1.getId())); - assertTrue(followingIds.contains(following2.getId())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingAddAllToManyToManyCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().addAllNow(List.of(following1, following2)); - assertEquals(2, facebookUser.getFollowing().size()); - - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - assertTrue(following1.getFollowers().contains(facebookUser)); - assertTrue(following2.getFollowers().contains(facebookUser)); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List followingIds = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - followingIds.add(resultSet.getObject("following_id", UUID.class)); - } - - assertEquals(2, followingIds.size()); - assertTrue(followingIds.contains(following1.getId())); - assertTrue(followingIds.contains(following2.getId())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testRemoveFromManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().add(following1); - facebookUser.getFollowing().add(following2); - - assertEquals(2, facebookUser.getFollowing().size()); - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - assertTrue(facebookUser.getFollowing().remove(following1)); - - assertEquals(1, facebookUser.getFollowing().size()); - assertFalse(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - assertFalse(following1.getFollowers().contains(facebookUser)); - assertTrue(following2.getFollowers().contains(facebookUser)); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List followingIds = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - followingIds.add(resultSet.getObject("following_id", UUID.class)); - } - - assertEquals(1, followingIds.size()); - assertFalse(followingIds.contains(following1.getId())); - assertTrue(followingIds.contains(following2.getId())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testRemoveFromManyToManyCollectionViaDeletion() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Some post", null); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Some post", null); - FacebookPost post3 = FacebookPost.createSync(dataManager, "Some post", null); - - facebookUser.getLiked().add(post1); - facebookUser.getLiked().add(post2); - facebookUser.getLiked().add(post3); - - - assertEquals(3, facebookUser.getLiked().size()); - assertTrue(facebookUser.getLiked().contains(post1)); - assertTrue(facebookUser.getLiked().contains(post2)); - assertTrue(facebookUser.getLiked().contains(post3)); - - dataManager.delete(post1); - - assertEquals(2, facebookUser.getLiked().size()); - - assertFalse(facebookUser.getLiked().contains(post1)); - assertTrue(facebookUser.getLiked().contains(post2)); - assertTrue(facebookUser.getLiked().contains(post3)); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_liked_posts WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List likedIds = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - likedIds.add(resultSet.getObject("post_id", UUID.class)); - } - - assertEquals(2, likedIds.size()); - assertFalse(likedIds.contains(post1.getId())); - assertTrue(likedIds.contains(post2.getId())); - assertTrue(likedIds.contains(post3.getId())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("delete from facebook.user_liked_posts where post_id = '" + post2.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(1, facebookUser.getLiked().size()); - } - - @RetryingTest(5) - public void testBlockingRemoveFromManyToManyCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().addNow(following1); - facebookUser.getFollowing().addNow(following2); - - assertEquals(2, facebookUser.getFollowing().size()); - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - assertTrue(facebookUser.getFollowing().removeNow(following1)); - - assertEquals(1, facebookUser.getFollowing().size()); - assertFalse(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - assertFalse(following1.getFollowers().contains(facebookUser)); - assertTrue(following2.getFollowers().contains(facebookUser)); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List followingIds = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - followingIds.add(resultSet.getObject("following_id", UUID.class)); - } - - assertEquals(1, followingIds.size()); - assertFalse(followingIds.contains(following1.getId())); - assertTrue(followingIds.contains(following2.getId())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testRemoveAllFromManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - FacebookUser following3 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().add(following1); - facebookUser.getFollowing().add(following2); - facebookUser.getFollowing().add(following3); - - assertEquals(3, facebookUser.getFollowing().size()); - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - assertTrue(facebookUser.getFollowing().contains(following3)); - - assertTrue(facebookUser.getFollowing().removeAll(List.of(following1, following2))); - - assertEquals(1, facebookUser.getFollowing().size()); - assertFalse(facebookUser.getFollowing().contains(following1)); - assertFalse(facebookUser.getFollowing().contains(following2)); - assertTrue(facebookUser.getFollowing().contains(following3)); - - assertFalse(following1.getFollowers().contains(facebookUser)); - assertFalse(following2.getFollowers().contains(facebookUser)); - assertTrue(following3.getFollowers().contains(facebookUser)); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List followingIds = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - followingIds.add(resultSet.getObject("following_id", UUID.class)); - } - - assertEquals(1, followingIds.size()); - assertFalse(followingIds.contains(following1.getId())); - assertFalse(followingIds.contains(following2.getId())); - assertTrue(followingIds.contains(following3.getId())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingRemoveAllFromManyToManyCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - FacebookUser following3 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().addNow(following1); - facebookUser.getFollowing().addNow(following2); - facebookUser.getFollowing().addNow(following3); - - assertEquals(3, facebookUser.getFollowing().size()); - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - assertTrue(facebookUser.getFollowing().contains(following3)); - - assertTrue(facebookUser.getFollowing().removeAllNow(List.of(following1, following2))); - - assertEquals(1, facebookUser.getFollowing().size()); - assertFalse(facebookUser.getFollowing().contains(following1)); - assertFalse(facebookUser.getFollowing().contains(following2)); - assertTrue(facebookUser.getFollowing().contains(following3)); - - assertFalse(following1.getFollowers().contains(facebookUser)); - assertFalse(following2.getFollowers().contains(facebookUser)); - assertTrue(following3.getFollowers().contains(facebookUser)); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - List followingIds = new ArrayList<>(); - ResultSet resultSet = statement.getResultSet(); - while (resultSet.next()) { - followingIds.add(resultSet.getObject("following_id", UUID.class)); - } - - assertEquals(1, followingIds.size()); - assertFalse(followingIds.contains(following1.getId())); - assertFalse(followingIds.contains(following2.getId())); - assertTrue(followingIds.contains(following3.getId())); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testClearInManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().add(following1); - facebookUser.getFollowing().add(following2); - - assertEquals(2, facebookUser.getFollowing().size()); - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - facebookUser.getFollowing().clear(); - - assertEquals(0, facebookUser.getFollowing().size()); - - assertFalse(following1.getFollowers().contains(facebookUser)); - assertFalse(following2.getFollowers().contains(facebookUser)); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingClearInManyToManyCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser.getFollowing().addNow(following1); - facebookUser.getFollowing().addNow(following2); - - assertEquals(2, facebookUser.getFollowing().size()); - assertTrue(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - - facebookUser.getFollowing().clearNow(); - - assertEquals(0, facebookUser.getFollowing().size()); - - assertFalse(following1.getFollowers().contains(facebookUser)); - assertFalse(following2.getFollowers().contains(facebookUser)); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testContainsInManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser1 = FacebookUser.createSync(dataManager); - FacebookUser facebookUser2 = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser1.getFollowing().add(following1); - facebookUser1.getFollowing().add(following2); - - facebookUser2.getFollowing().add(following1); - - assertTrue(facebookUser1.getFollowing().contains(following1)); - assertTrue(facebookUser1.getFollowing().contains(following2)); - assertTrue(facebookUser2.getFollowing().contains(following1)); - assertFalse(facebookUser2.getFollowing().contains(following2)); - assertFalse(facebookUser1.getFollowing().contains(facebookUser2)); - assertFalse(facebookUser1.getFollowing().contains("Not a user")); - assertFalse(facebookUser1.getFollowing().contains(null)); - } - - @RetryingTest(5) - public void testContainsAllInManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser1 = FacebookUser.createSync(dataManager); - FacebookUser facebookUser2 = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser1.getFollowing().add(following1); - facebookUser1.getFollowing().add(following2); - - facebookUser2.getFollowing().add(following1); - - assertTrue(facebookUser1.getFollowing().contains(following1)); - assertTrue(facebookUser1.getFollowing().contains(following2)); - assertTrue(facebookUser2.getFollowing().contains(following1)); - assertFalse(facebookUser2.getFollowing().contains(following2)); - - assertTrue(facebookUser1.getFollowing().containsAll(List.of(following1, following2))); - assertFalse(facebookUser1.getFollowing().containsAll(List.of(following1, following2, "Not a user"))); - List bad = new ArrayList<>(); - bad.add(following1); - bad.add(following2); - bad.add(null); - assertFalse(facebookUser1.getFollowing().containsAll(bad)); - assertTrue(facebookUser1.getFollowing().containsAll(List.of())); - } - - @RetryingTest(5) - public void testRetainAllInManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser1 = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - FacebookUser following3 = FacebookUser.createSync(dataManager); - - facebookUser1.getFollowing().add(following1); - facebookUser1.getFollowing().add(following2); - facebookUser1.getFollowing().add(following3); - - assertEquals(3, facebookUser1.getFollowing().size()); - - assertTrue(facebookUser1.getFollowing().retainAll(List.of(following1, following2))); - - assertEquals(2, facebookUser1.getFollowing().size()); - assertTrue(facebookUser1.getFollowing().contains(following1)); - assertTrue(facebookUser1.getFollowing().contains(following2)); - assertFalse(facebookUser1.getFollowing().contains(following3)); - - assertTrue(facebookUser1.getFollowing().retainAll(List.of(following1))); - - assertEquals(1, facebookUser1.getFollowing().size()); - assertTrue(facebookUser1.getFollowing().contains(following1)); - assertFalse(facebookUser1.getFollowing().contains(following2)); - assertFalse(facebookUser1.getFollowing().contains(following3)); - } - - @RetryingTest(5) - public void testToArrayInManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser1 = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser1.getFollowing().add(following1); - facebookUser1.getFollowing().add(following2); - - assertEquals(2, facebookUser1.getFollowing().size()); - - FacebookUser[] followings = facebookUser1.getFollowing().toArray(new FacebookUser[0]); - assertEquals(2, followings.length); - boolean containsFollowing1 = false; - boolean containsFollowing2 = false; - - for (FacebookUser following : followings) { - if (following.equals(following1)) { - containsFollowing1 = true; - } else if (following.equals(following2)) { - containsFollowing2 = true; - } - } - - assertTrue(containsFollowing1); - assertTrue(containsFollowing2); - - Object[] objects = facebookUser1.getFollowing().toArray(); - assertEquals(2, objects.length); - boolean containsFollowing1Object = false; - boolean containsFollowing2Object = false; - - for (Object object : objects) { - if (object.equals(following1)) { - containsFollowing1Object = true; - } else if (object.equals(following2)) { - containsFollowing2Object = true; - } - } - - assertTrue(containsFollowing1Object); - assertTrue(containsFollowing2Object); - } - - @RetryingTest(5) - public void testIteratorInManyToManyCollection() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser1 = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser1.getFollowing().add(following1); - facebookUser1.getFollowing().add(following2); - - assertEquals(2, facebookUser1.getFollowing().size()); - - Iterator iterator = facebookUser1.getFollowing().iterator(); - assertTrue(iterator.hasNext()); - FacebookUser next = iterator.next(); - assertTrue(next.equals(following1) || next.equals(following2)); - assertTrue(iterator.hasNext()); - next = iterator.next(); - assertTrue(next.equals(following1) || next.equals(following2)); - assertFalse(iterator.hasNext()); - - iterator = facebookUser1.getFollowing().iterator(); - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(1, facebookUser1.getFollowing().size()); - - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(0, facebookUser1.getFollowing().size()); - - assertFalse(iterator.hasNext()); - - waitForDataPropagation(); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testBlockingIteratorInManyToManyCollection() throws SQLException { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser1 = FacebookUser.createSync(dataManager); - - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - facebookUser1.getFollowing().addNow(following1); - facebookUser1.getFollowing().addNow(following2); - - assertEquals(2, facebookUser1.getFollowing().size()); - - Iterator iterator = facebookUser1.getFollowing().blockingIterator(); - assertTrue(iterator.hasNext()); - FacebookUser next = iterator.next(); - assertTrue(next.equals(following1) || next.equals(following2)); - assertTrue(iterator.hasNext()); - next = iterator.next(); - assertTrue(next.equals(following1) || next.equals(following2)); - assertFalse(iterator.hasNext()); - - iterator = facebookUser1.getFollowing().blockingIterator(); - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(1, facebookUser1.getFollowing().size()); - - assertTrue(iterator.hasNext()); - iterator.next(); - iterator.remove(); - - assertEquals(0, facebookUser1.getFollowing().size()); - - assertFalse(iterator.hasNext()); - - try (PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM facebook.user_following WHERE user_id = ?")) { - statement.setObject(1, facebookUser1.getId()); - assertTrue(statement.execute()); - ResultSet resultSet = statement.getResultSet(); - assertFalse(resultSet.next()); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testLoadingManyToManyCollection() { - Multimap followingMap = HashMultimap.create(); - try (Statement statement = getConnection().createStatement()) { - for (int i = 0; i < 10; i++) { - UUID userId = UUID.randomUUID(); - UUID followingId1 = UUID.randomUUID(); - UUID followingId2 = UUID.randomUUID(); - followingMap.put(userId, followingId1); - followingMap.put(userId, followingId2); - statement.executeUpdate("insert into facebook.users (id) values ('" + userId + "')"); - statement.executeUpdate("insert into facebook.users (id) values ('" + followingId1 + "')"); - statement.executeUpdate("insert into facebook.users (id) values ('" + followingId2 + "')"); - statement.executeUpdate("insert into facebook.user_following (user_id, following_id) values ('" + userId + "', '" + followingId1 + "')"); - statement.executeUpdate("insert into facebook.user_following (user_id, following_id) values ('" + userId + "', '" + followingId2 + "')"); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - - MockEnvironment environment = createMockEnvironment(); - getMockEnvironments().add(environment); - - DataManager dataManager = environment.dataManager(); - dataManager.loadAll(FacebookUser.class); - - for (UUID userId : followingMap.keySet()) { - FacebookUser user = dataManager.get(FacebookUser.class, userId); - assertEquals(2, user.getFollowing().size()); - assertTrue(user.getFollowing().stream().anyMatch(following -> followingMap.get(userId).contains(following.getId()))); - } - } - - @RetryingTest(5) - public void testUpdatingManyToManyCollectionInDatabase() { - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - assertEquals(0, facebookUser.followingAdditions.get()); - assertEquals(0, facebookUser.followingRemovals.get()); - - try (PreparedStatement statement = getConnection().prepareStatement("insert into facebook.user_following (user_id, following_id) values (?, ?)")) { - statement.setObject(1, facebookUser.getId()); - statement.setObject(2, following1.getId()); - statement.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(1, facebookUser.followingAdditions.get()); - - assertEquals(1, facebookUser.getFollowing().size()); - assertTrue(facebookUser.getFollowing().contains(following1)); - assertEquals(1, following1.getFollowers().size()); - assertTrue(following1.getFollowers().contains(facebookUser)); - - try (PreparedStatement statement = getConnection().prepareStatement("update facebook.user_following set following_id = ? where user_id = ?")) { - statement.setObject(1, following2.getId()); - statement.setObject(2, facebookUser.getId()); - statement.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(2, facebookUser.followingAdditions.get()); - assertEquals(1, facebookUser.followingRemovals.get()); - - assertEquals(1, facebookUser.getFollowing().size()); - assertFalse(facebookUser.getFollowing().contains(following1)); - assertTrue(facebookUser.getFollowing().contains(following2)); - assertEquals(1, following2.getFollowers().size()); - assertTrue(following2.getFollowers().contains(facebookUser)); - assertEquals(0, following1.getFollowers().size()); - - try (PreparedStatement statement = getConnection().prepareStatement("delete from facebook.user_following where user_id = ?")) { - statement.setObject(1, facebookUser.getId()); - statement.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(2, facebookUser.followingAdditions.get()); - - assertEquals(0, facebookUser.getFollowing().size()); - assertEquals(0, following2.getFollowers().size()); - } - - @RetryingTest(5) - public void testAddHandlers() { - //Note that add handlers are called async - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - - assertEquals(0, facebookUser.followingAdditions.get()); - assertEquals(0, facebookUser.favoriteQuoteAdditions.get()); - assertEquals(0, facebookUser.postAdditions.get()); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - waitForDataPropagation(); - assertEquals(1, facebookUser.postAdditions.get()); - - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", null); - waitForDataPropagation(); - assertEquals(1, facebookUser.postAdditions.get()); - - facebookUser.getPosts().add(post2); - waitForDataPropagation(); - assertEquals(2, facebookUser.postAdditions.get()); - - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", null); - post3.setUser(facebookUser); - waitForDataPropagation(); - assertEquals(3, facebookUser.postAdditions.get()); - - facebookUser.getFavoriteQuotes().add("Here's a quote"); - waitForDataPropagation(); - assertEquals(1, facebookUser.favoriteQuoteAdditions.get()); - - facebookUser.getFollowing().add(FacebookUser.createSync(dataManager)); - waitForDataPropagation(); - assertEquals(1, facebookUser.followingAdditions.get()); - - FacebookUser otherUser = FacebookUser.createSync(dataManager); - otherUser.getFollowers().add(facebookUser); - waitForDataPropagation(); - assertEquals(2, facebookUser.followingAdditions.get()); - } - - @RetryingTest(5) - public void testRemoveHandlers() { - //Note that remove handlers are called async - MockEnvironment mockEnvironment = getMockEnvironments().getFirst(); - DataManager dataManager = mockEnvironment.dataManager(); - - FacebookUser facebookUser = FacebookUser.createSync(dataManager); - FacebookUser following1 = FacebookUser.createSync(dataManager); - FacebookUser following2 = FacebookUser.createSync(dataManager); - - FacebookPost post1 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post2 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - FacebookPost post3 = FacebookPost.createSync(dataManager, "Here's some post description", facebookUser); - facebookUser.getFavoriteQuotes().add("Here's a quote"); - facebookUser.getFavoriteQuotes().add("Here's another quote"); - facebookUser.getFollowing().add(following1); - facebookUser.getFollowing().add(following2); - - assertEquals(3, facebookUser.getPosts().size()); - assertEquals(2, facebookUser.getFavoriteQuotes().size()); - assertEquals(2, facebookUser.getFollowing().size()); - - - assertEquals(0, facebookUser.followingRemovals.get()); - assertEquals(0, facebookUser.favoriteQuoteRemovals.get()); - assertEquals(0, facebookUser.postRemovals.get()); - - facebookUser.getPosts().remove(post1); - waitForDataPropagation(); - assertEquals(1, facebookUser.postRemovals.get()); - - facebookUser.getPosts().remove(post2); - waitForDataPropagation(); - assertEquals(2, facebookUser.postRemovals.get()); - - facebookUser.getPosts().remove(post3); - waitForDataPropagation(); - assertEquals(3, facebookUser.postRemovals.get()); - - facebookUser.getFavoriteQuotes().remove("Here's a quote"); - waitForDataPropagation(); - assertEquals(1, facebookUser.favoriteQuoteRemovals.get()); - - facebookUser.getFavoriteQuotes().remove("Here's another quote"); - waitForDataPropagation(); - assertEquals(2, facebookUser.favoriteQuoteRemovals.get()); - - facebookUser.getFollowing().remove(following1); - waitForDataPropagation(); - assertEquals(1, facebookUser.followingRemovals.get()); - - following2.getFollowers().remove(facebookUser); - waitForDataPropagation(); - assertEquals(2, facebookUser.followingRemovals.get()); - } -} \ No newline at end of file diff --git a/oldsrc/ogtest/java/net/staticstudios/data/PersistentValueTest.java b/oldsrc/ogtest/java/net/staticstudios/data/PersistentValueTest.java deleted file mode 100644 index 0230b51c..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/PersistentValueTest.java +++ /dev/null @@ -1,354 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; -import net.staticstudios.data.mock.persistentvalue.DiscordUser; -import net.staticstudios.data.mock.persistentvalue.DiscordUserSettings; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junitpioneer.jupiter.RetryingTest; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -public class PersistentValueTest extends DataTest { - - //todo: we need to test what happens when we manually edit the db. do the values get set on insert, update, and delete - //todo: test default values - //todo: test blocking #set calls - - @BeforeEach - public void init() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - drop schema if exists discord cascade; - create schema if not exists discord; - create table if not exists discord.users ( - id uuid primary primaryKey, - name text not null - ); - create table if not exists discord.user_meta ( - id uuid primary primaryKey, - name_updates_called int not null default 0, - enable_friend_requests_updates_called int not null default 0 - ); - create table if not exists discord.user_settings ( - user_id uuid primary primaryKey, - enable_friend_requests boolean not null default true - ); - """); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - getMockEnvironments().forEach(env -> { - DataManager dataManager = env.dataManager(); - dataManager.loadAll(DiscordUser.class); - dataManager.loadAll(DiscordUserSettings.class); - }); - } - - @RetryingTest(5) - public void testSetPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); - assertEquals("John Doe", user.getName()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.users set name = 'Jane Doe' where id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals("Jane Doe", user.getName()); - - user.setName("User"); - assertEquals("User", user.getName()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); - resultSet.next(); - assertEquals("User", resultSet.getString("name")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testSetForeignPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); - assertTrue(user.getEnableFriendRequests()); //defaults to true - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + user.getId() + "'"); - resultSet.next(); - assertTrue(resultSet.getBoolean("enable_friend_requests")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - user.setEnableFriendRequests(false); - assertFalse(user.getEnableFriendRequests()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + user.getId() + "'"); - resultSet.next(); - assertFalse(resultSet.getBoolean("enable_friend_requests")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - user.setEnableFriendRequests(true); - assertTrue(user.getEnableFriendRequests()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + user.getId() + "'"); - resultSet.next(); - assertTrue(resultSet.getBoolean("enable_friend_requests")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.user_settings set enable_friend_requests = false where user_id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertFalse(user.getEnableFriendRequests()); - } - - @RetryingTest(5) - public void testForeignPersistentValuePerferExistingInsertionStrategy() { - UUID id = UUID.randomUUID(); - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("insert into discord.user_settings (user_id, enable_friend_requests) values ('" + id + "', false)"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - MockEnvironment environment = createMockEnvironment(); - DataManager dataManager = environment.dataManager(); - - dataManager.loadAll(DiscordUser.class); - dataManager.loadAll(DiscordUserSettings.class); - - assertNull(dataManager.get(DiscordUser.class, id)); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", id); - assertFalse(user.getEnableFriendRequests()); - - user.setEnableFriendRequests(true); - assertTrue(user.getEnableFriendRequests()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select enable_friend_requests from discord.user_settings where user_id = '" + id + "'"); - resultSet.next(); - assertTrue(resultSet.getBoolean("enable_friend_requests")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - @Disabled("see todo message. the datamanager no longer receives its own pg notifications, so this will never work until we implement the TODO") - public void testSetForeignPersistentValueAndSeePersistentValueUpdate() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); - assertTrue(user.getEnableFriendRequests()); //defaults to true - - //todo: the settings should be created as soon as the user is, but this is not the case since they are completely separate entities - // this functionality needs to be added to the data manager. - // maybe instead of having the data manager do all the work, we can let it know somehow that DiscordUserSettings should be created when a DiscordUser is created - // currently it is being created when the postgres notification comes back altering us that the user_settings table has had a new row inserted -// DiscordSettings settings = dataManager.getUniqueData(DiscordSettings.class, user.getId()); - - - waitForDataPropagation(); - - DiscordUserSettings settings = dataManager.get(DiscordUserSettings.class, user.getId()); - - assertTrue(settings.getEnableFriendRequests()); - - user.setEnableFriendRequests(false); - assertFalse(user.getEnableFriendRequests()); - assertFalse(settings.getEnableFriendRequests()); - - settings.setEnableFriendRequests(true); - assertTrue(user.getEnableFriendRequests()); - assertTrue(settings.getEnableFriendRequests()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.user_settings set enable_friend_requests = false where user_id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertFalse(user.getEnableFriendRequests()); - assertFalse(settings.getEnableFriendRequests()); - } - - @RetryingTest(5) - public void testPersistentValueUpdateHandlers() { - //Note that update handlers are called async - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "John Doe", UUID.randomUUID()); - assertEquals(0, user.getNameUpdatesCalled()); - assertEquals(0, user.getEnableFriendRequestsUpdatesCalled()); - - user.setName("Jane Doe"); - waitForDataPropagation(); - assertEquals(1, user.getNameUpdatesCalled()); - - user.setEnableFriendRequests(false); - waitForDataPropagation(); - assertEquals(1, user.getEnableFriendRequestsUpdatesCalled()); - - user.setEnableFriendRequests(true); - waitForDataPropagation(); - assertEquals(2, user.getEnableFriendRequestsUpdatesCalled()); - - user.setName("John Doe"); - waitForDataPropagation(); - assertEquals(2, user.getNameUpdatesCalled()); - - waitForDataPropagation(); - - assertEquals(2, user.getNameUpdatesCalled()); - assertEquals(2, user.getEnableFriendRequestsUpdatesCalled()); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.users set name = 'Jane Doe' where id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update discord.user_settings set enable_friend_requests = false where user_id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(3, user.getNameUpdatesCalled()); - assertEquals(3, user.getEnableFriendRequestsUpdatesCalled()); - } - - @RetryingTest(5) - public void testLoading() { - List ids = new ArrayList<>(); - try (Statement statement = getConnection().createStatement()) { - for (int i = 0; i < 10; i++) { - UUID id = UUID.randomUUID(); - ids.add(id); - statement.executeUpdate("insert into discord.users (id, name) values ('" + id + "', 'User " + i + "')"); - statement.executeUpdate("insert into discord.user_meta (id) values ('" + id + "')"); - statement.executeUpdate("insert into discord.user_settings (user_id) values ('" + id + "')"); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - - MockEnvironment environment = createMockEnvironment(); - getMockEnvironments().add(environment); - - DataManager dataManager = environment.dataManager(); - dataManager.loadAll(DiscordUser.class); - - for (UUID id : ids) { - DiscordUser user = dataManager.get(DiscordUser.class, id); - assertEquals("User " + ids.indexOf(id), user.getName()); - } - } - - @RetryingTest(5) - public void testTaskQueue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "0", UUID.randomUUID()); - for (int i = 0; i < 30; i++) { - int name = Integer.parseInt(user.getName()); - user.setName(String.valueOf(name + 1)); - } - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); - resultSet.next(); - assertEquals("30", resultSet.getString("name")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testUpdateInterval() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - DiscordUser user = DiscordUser.createSync(dataManager, "0", UUID.randomUUID()); - user.name.updateInterval(getWaitForDataPropagationTime() * 2); - for (int i = 0; i < 30; i++) { - int name = Integer.parseInt(user.getName()); - user.setName(String.valueOf(name + 1)); - } - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); - resultSet.next(); - assertEquals("0", resultSet.getString("name")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("select name from discord.users where id = '" + user.getId() + "'"); - resultSet.next(); - assertEquals("30", resultSet.getString("name")); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - //todo: test straight up deleting an fpv. ideally a data does not exist exception should be thrown when calling get on a deleted fpv -} \ No newline at end of file diff --git a/oldsrc/ogtest/java/net/staticstudios/data/PostgresListenerTest.java b/oldsrc/ogtest/java/net/staticstudios/data/PostgresListenerTest.java deleted file mode 100644 index d052e48a..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/PostgresListenerTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.impl.pg.PostgresListener; -import net.staticstudios.data.impl.pg.PostgresNotification; -import net.staticstudios.data.impl.pg.PostgresOperation; -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; -import org.junit.jupiter.api.BeforeEach; -import org.junitpioneer.jupiter.RetryingTest; - -import java.sql.SQLException; -import java.sql.Statement; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -import static org.junit.jupiter.api.Assertions.*; - -public class PostgresListenerTest extends DataTest { - @BeforeEach - public void init() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - drop schema if exists test cascade; - create schema if not exists test; - create table if not exists test.test ( - id uuid primary primaryKey, - value int not null - ); - """); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - @RetryingTest(5) - public void testInsertUpdateDelete() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - PostgresListener pgListener = dataManager.getPostgresListener(); - - CompletableFuture insertFuture = new CompletableFuture<>(); - CompletableFuture updateFuture = new CompletableFuture<>(); - CompletableFuture deleteFuture = new CompletableFuture<>(); - - pgListener.ensureTableHasTrigger(getConnection(), "test.test"); - - pgListener.addHandler(notification -> { - if (notification.getSchema().equals("test")) { - if (notification.getOperation() == PostgresOperation.INSERT) { - insertFuture.complete(notification); - } else if (notification.getOperation() == PostgresOperation.UPDATE) { - updateFuture.complete(notification); - } else if (notification.getOperation() == PostgresOperation.DELETE) { - deleteFuture.complete(notification); - } - } - }); - - UUID id = UUID.randomUUID(); - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("insert into test.test (id, value) values ('" + id + "', 1)"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - PostgresNotification insertNotification = insertFuture.join(); - assertEquals(id, UUID.fromString(insertNotification.getData().newDataValueMap().get("id"))); - assertTrue(insertNotification.getData().oldDataValueMap().isEmpty()); - assertEquals(1, Integer.valueOf(insertNotification.getData().newDataValueMap().get("value"))); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update test.test set value = 2 where id = '" + id + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - PostgresNotification updateNotification = updateFuture.join(); - assertEquals(id, UUID.fromString(updateNotification.getData().newDataValueMap().get("id"))); - assertEquals(1, Integer.valueOf(updateNotification.getData().oldDataValueMap().get("value"))); - assertEquals(2, Integer.valueOf(updateNotification.getData().newDataValueMap().get("value"))); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("delete from test.test where id = '" + id + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - PostgresNotification deleteNotification = deleteFuture.join(); - assertEquals(id, UUID.fromString(deleteNotification.getData().oldDataValueMap().get("id"))); - assertTrue(deleteNotification.getData().newDataValueMap().isEmpty()); - assertEquals(2, Integer.valueOf(deleteNotification.getData().oldDataValueMap().get("value"))); - } - - @RetryingTest(5) - public void testReConnect() throws InterruptedException, SQLException { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - PostgresListener pgListener = dataManager.getPostgresListener(); - - assertFalse(pgListener.pgConnection.isClosed()); - pgListener.pgConnection.close(); - assertTrue(pgListener.pgConnection.isClosed()); - - Thread.sleep(2000); - - assertFalse(pgListener.pgConnection.isClosed()); - } -} \ No newline at end of file diff --git a/oldsrc/ogtest/java/net/staticstudios/data/PrimitivesTest.java b/oldsrc/ogtest/java/net/staticstudios/data/PrimitivesTest.java deleted file mode 100644 index 1560c677..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/PrimitivesTest.java +++ /dev/null @@ -1,415 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; -import net.staticstudios.data.mock.primative.*; -import net.staticstudios.data.primative.Primitives; -import org.junit.jupiter.api.BeforeEach; -import org.junitpioneer.jupiter.RetryingTest; - -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -public class PrimitivesTest extends DataTest { - - @BeforeEach - public void init() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - drop schema if exists primitive cascade; - create schema if not exists primitive; - create table if not exists primitive.string_test ( - id uuid primary primaryKey, - value text - ); - - create table if not exists primitive.character_test ( - id uuid primary primaryKey, - value char(1) not null - ); - - create table if not exists primitive.byte_test ( - id uuid primary primaryKey, - value smallint not null - ); - - create table if not exists primitive.short_test ( - id uuid primary primaryKey, - value smallint not null - ); - - create table if not exists primitive.integer_test ( - id uuid primary primaryKey, - value integer not null - ); - - create table if not exists primitive.long_test ( - id uuid primary primaryKey, - value bigint not null - ); - - create table if not exists primitive.float_test ( - id uuid primary primaryKey, - value real not null - ); - - create table if not exists primitive.double_test ( - id uuid primary primaryKey, - value double precision not null - ); - - create table if not exists primitive.boolean_test ( - id uuid primary primaryKey, - value boolean not null - ); - - create table if not exists primitive.uuid_test ( - id uuid primary primaryKey, - value uuid - ); - - create table if not exists primitive.timestamp_test ( - id uuid primary primaryKey, - value timestamp - ); - - create table if not exists primitive.byte_array_test ( - id uuid primary primaryKey, - value bytea - ); - """); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - getMockEnvironments().forEach(env -> { - DataManager dataManager = env.dataManager(); - dataManager.loadAll(StringPrimitiveTestObject.class); - dataManager.loadAll(CharacterPrimitiveTestObject.class); - dataManager.loadAll(BytePrimitiveTestObject.class); - dataManager.loadAll(ShortPrimitiveTestObject.class); - dataManager.loadAll(IntegerPrimitiveTestObject.class); - dataManager.loadAll(LongPrimitiveTestObject.class); - dataManager.loadAll(FloatPrimitiveTestObject.class); - dataManager.loadAll(DoublePrimitiveTestObject.class); - dataManager.loadAll(BooleanPrimitiveTestObject.class); - dataManager.loadAll(UUIDPrimitiveTestObject.class); - dataManager.loadAll(TimestampPrimitiveTestObject.class); - dataManager.loadAll(ByteArrayPrimitiveTestObject.class); - }); - } - - //-----Test nullability START----- - - @RetryingTest(5) - public void testStringPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are allowed in strings, so this should be fine. - StringPrimitiveTestObject obj = StringPrimitiveTestObject.createSync(dataManager, "Hello, World!"); - StringPrimitiveTestObject.createSync(dataManager, null); - - obj.setValue(null); //This should not throw an NPE - } - - @RetryingTest(5) - public void testCharacterPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are not allowed in characters, so this should throw an exception. - CharacterPrimitiveTestObject obj = CharacterPrimitiveTestObject.createSync(dataManager, 'a'); - assertThrows(NullPointerException.class, () -> CharacterPrimitiveTestObject.createSync(dataManager, null)); - - assertThrows(NullPointerException.class, () -> obj.setValue(null)); - } - - @RetryingTest(5) - public void testBytePersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are not allowed in bytes, so this should throw an exception. - BytePrimitiveTestObject obj = BytePrimitiveTestObject.createSync(dataManager, (byte) 1); - assertThrows(NullPointerException.class, () -> BytePrimitiveTestObject.createSync(dataManager, null)); - - assertThrows(NullPointerException.class, () -> obj.setValue(null)); - } - - @RetryingTest(5) - public void testShortPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are not allowed in shorts, so this should throw an exception. - ShortPrimitiveTestObject obj = ShortPrimitiveTestObject.createSync(dataManager, (short) 1); - assertThrows(NullPointerException.class, () -> ShortPrimitiveTestObject.createSync(dataManager, null)); - - assertThrows(NullPointerException.class, () -> obj.setValue(null)); - } - - @RetryingTest(5) - public void testIntegerPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are not allowed in integers, so this should throw an exception. - IntegerPrimitiveTestObject obj = IntegerPrimitiveTestObject.createSync(dataManager, 1); - assertThrows(NullPointerException.class, () -> IntegerPrimitiveTestObject.createSync(dataManager, null)); - - assertThrows(NullPointerException.class, () -> obj.setValue(null)); - } - - @RetryingTest(5) - public void testLongPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are not allowed in longs, so this should throw an exception. - LongPrimitiveTestObject obj = LongPrimitiveTestObject.createSync(dataManager, 1L); - assertThrows(NullPointerException.class, () -> LongPrimitiveTestObject.createSync(dataManager, null)); - - assertThrows(NullPointerException.class, () -> obj.setValue(null)); - } - - @RetryingTest(5) - public void testFloatPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are not allowed in floats, so this should throw an exception. - FloatPrimitiveTestObject obj = FloatPrimitiveTestObject.createSync(dataManager, 1.0f); - assertThrows(NullPointerException.class, () -> FloatPrimitiveTestObject.createSync(dataManager, null)); - - assertThrows(NullPointerException.class, () -> obj.setValue(null)); - } - - @RetryingTest(5) - public void testDoublePersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are not allowed in doubles, so this should throw an exception. - DoublePrimitiveTestObject obj = DoublePrimitiveTestObject.createSync(dataManager, 1.0); - assertThrows(NullPointerException.class, () -> DoublePrimitiveTestObject.createSync(dataManager, null)); - - assertThrows(NullPointerException.class, () -> obj.setValue(null)); - } - - @RetryingTest(5) - public void testBooleanPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are not allowed in booleans, so this should throw an exception. - BooleanPrimitiveTestObject obj = BooleanPrimitiveTestObject.createSync(dataManager, true); - assertThrows(NullPointerException.class, () -> BooleanPrimitiveTestObject.createSync(dataManager, null)); - - assertThrows(NullPointerException.class, () -> obj.setValue(null)); - } - - @RetryingTest(5) - public void testUUIDPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are allowed in UUIDs, so this should be fine. - UUIDPrimitiveTestObject obj = UUIDPrimitiveTestObject.createSync(dataManager, UUID.randomUUID()); - UUIDPrimitiveTestObject.createSync(dataManager, null); - - obj.setValue(null); //This should not throw an NPE - } - - @RetryingTest(5) - public void testTimestampPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are allowed in timestamps, so this should be fine. - TimestampPrimitiveTestObject obj = TimestampPrimitiveTestObject.createSync(dataManager, Timestamp.from(Instant.now())); - TimestampPrimitiveTestObject.createSync(dataManager, null); - - obj.setValue(null); //This should not throw an NPE - } - - @RetryingTest(5) - public void testByteArrayPersistentValue() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - //Null values are allowed in byte arrays, so this should be fine. - ByteArrayPrimitiveTestObject obj = ByteArrayPrimitiveTestObject.createSync(dataManager, new byte[]{1, 2, 3}); - ByteArrayPrimitiveTestObject.createSync(dataManager, null); - - obj.setValue(null); //This should not throw an NPE - } - - //-----Test nullability END----- - - //-----Test encoders and decoders START----- - - @RetryingTest(5) - public void testStringEncodersAndDecoders() { - String value = "Hello, World"; - String encoded = Primitives.STRING.encode(value); - String decoded = Primitives.STRING.decode(encoded); - - assertEquals("Hello, World", encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testStringEncodersAndDecodersWithNull() { - String value = null; - String encoded = Primitives.STRING.encode(value); - String decoded = Primitives.STRING.decode(encoded); - - assertNull(encoded); - assertNull(decoded); - } - - @RetryingTest(5) - public void testCharacterEncodersAndDecoders() { - char value = 'a'; - String encoded = Primitives.CHARACTER.encode(value); - char decoded = Primitives.CHARACTER.decode(encoded); - - assertEquals("a", encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testByteEncodersAndDecoders() { - byte value = 1; - String encoded = Primitives.BYTE.encode(value); - byte decoded = Primitives.BYTE.decode(encoded); - - assertEquals("1", encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testShortEncodersAndDecoders() { - short value = 1; - String encoded = Primitives.SHORT.encode(value); - short decoded = Primitives.SHORT.decode(encoded); - - assertEquals("1", encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testIntegerEncodersAndDecoders() { - int value = 1; - String encoded = Primitives.INTEGER.encode(value); - int decoded = Primitives.INTEGER.decode(encoded); - - assertEquals("1", encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testLongEncodersAndDecoders() { - long value = 1; - String encoded = Primitives.LONG.encode(value); - long decoded = Primitives.LONG.decode(encoded); - - assertEquals("1", encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testFloatEncodersAndDecoders() { - float value = 1.0f; - String encoded = Primitives.FLOAT.encode(value); - float decoded = Primitives.FLOAT.decode(encoded); - - assertEquals("1.0", encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testDoubleEncodersAndDecoders() { - double value = 1.0; - String encoded = Primitives.DOUBLE.encode(value); - double decoded = Primitives.DOUBLE.decode(encoded); - - assertEquals("1.0", encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testBooleanEncodersAndDecoders() { - boolean value = true; - String encoded = Primitives.BOOLEAN.encode(value); - boolean decoded = Primitives.BOOLEAN.decode(encoded); - - assertEquals("true", encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testUUIDEncodersAndDecoders() { - UUID value = UUID.randomUUID(); - String encoded = Primitives.UUID.encode(value); - UUID decoded = Primitives.UUID.decode(encoded); - - assertEquals(value.toString(), encoded); - assertEquals(value, decoded); - } - - @RetryingTest(5) - public void testUUIDEncodersAndDecodersWithNull() { - UUID value = null; - String encoded = Primitives.UUID.encode(value); - UUID decoded = Primitives.UUID.decode(encoded); - - assertNull(encoded); - assertNull(decoded); - } - - @RetryingTest(5) - public void testTimestampEncodersAndDecoders() { - String encoded = "1970-01-01T00:00:00+00:00"; //We get timezones in the PostgresListener in ISO-8601 format - Timestamp decoded = Primitives.TIMESTAMP.decode(encoded); - - assertEquals(Timestamp.from(Instant.EPOCH), decoded); - assertEquals(encoded, Primitives.TIMESTAMP.encode(decoded)); - } - - @RetryingTest(5) - public void testTimestampEncodersAndDecodersWithNull() { - Timestamp value = null; - String encoded = Primitives.TIMESTAMP.encode(value); - Timestamp decoded = Primitives.TIMESTAMP.decode(encoded); - - assertNull(encoded); - assertNull(decoded); - } - - @RetryingTest(5) - public void testByteArrayEncodersAndDecoders() { - byte[] value = "Hello, World".getBytes(); - String encoded = Primitives.BYTE_ARRAY.encode(value); - byte[] decoded = Primitives.BYTE_ARRAY.decode(encoded); - - assertEquals("\\x48656c6c6f2c20576f726c64", encoded); - assertArrayEquals(value, decoded); - } - - @RetryingTest(5) - public void testByteArrayEncodersAndDecodersWithNull() { - byte[] value = null; - String encoded = Primitives.BYTE_ARRAY.encode(value); - byte[] decoded = Primitives.BYTE_ARRAY.decode(encoded); - - assertNull(encoded); - assertNull(decoded); - } -} \ No newline at end of file diff --git a/oldsrc/ogtest/java/net/staticstudios/data/ReferenceTest.java b/oldsrc/ogtest/java/net/staticstudios/data/ReferenceTest.java deleted file mode 100644 index 22376ff2..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/ReferenceTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.misc.DataTest; -import net.staticstudios.data.misc.MockEnvironment; -import net.staticstudios.data.mock.reference.SnapchatUser; -import net.staticstudios.data.mock.reference.SnapchatUserSettings; -import org.junit.jupiter.api.BeforeEach; -import org.junitpioneer.jupiter.RetryingTest; - -import java.sql.SQLException; -import java.sql.Statement; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -public class ReferenceTest extends DataTest { - @BeforeEach - public void init() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - drop schema if exists snapchat cascade; - create schema if not exists snapchat; - create table if not exists snapchat.users ( - id uuid primary primaryKey, - favorite_user_id uuid - ); - create table if not exists snapchat.user_meta ( - id uuid primary primaryKey, - update_called integer - ); - create table if not exists snapchat.user_settings ( - user_id uuid primary primaryKey, - enable_friend_requests boolean not null - ); - """); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - getMockEnvironments().forEach(env -> { - DataManager dataManager = env.dataManager(); - dataManager.loadAll(SnapchatUser.class); - }); - } - - @RetryingTest(5) - public void testSimpleReference() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - SnapchatUser user = SnapchatUser.createSync(dataManager); - - assertTrue(user.getSettings().getEnableFriendRequests()); - - user.getSettings().setEnableFriendRequests(false); - assertFalse(user.getSettings().getEnableFriendRequests()); - assertEquals(user, user.getSettings().getUser()); - } - - @RetryingTest(5) - public void testLoadingReference() { - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate(""" - insert into snapchat.users (id) values ('00000000-0000-0000-0000-000000000001'); - insert into snapchat.user_settings (user_id, enable_friend_requests) values ('00000000-0000-0000-0000-000000000001', false); - """); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - MockEnvironment environment = createMockEnvironment(); - DataManager dataManager = environment.dataManager(); - dataManager.loadAll(SnapchatUser.class); - - SnapchatUserSettings settings = dataManager.get(SnapchatUserSettings.class, UUID.fromString("00000000-0000-0000-0000-000000000001")); - assertNotNull(settings); - SnapchatUser user = dataManager.get(SnapchatUser.class, UUID.fromString("00000000-0000-0000-0000-000000000001")); - assertFalse(user.getSettings().getEnableFriendRequests()); - } - - @RetryingTest(5) - public void testSetReference() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - SnapchatUser user = SnapchatUser.createSync(dataManager); - SnapchatUser favoriteUser = SnapchatUser.createSync(dataManager); - - assertNull(user.getFavoriteUser()); - user.setFavoriteUser(favoriteUser); - assertEquals(favoriteUser, user.getFavoriteUser()); - - user.setFavoriteUser(null); - assertNull(user.getFavoriteUser()); - - waitForDataPropagation(); - - try (Statement statement = getConnection().createStatement()) { - statement.executeUpdate("update snapchat.users set favorite_user_id = '" + favoriteUser.getId() + "' where id = '" + user.getId() + "'"); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - waitForDataPropagation(); - - assertEquals(favoriteUser, user.getFavoriteUser()); - } - - @RetryingTest(5) - public void testReferenceUpdateHandler() { - MockEnvironment environment = getMockEnvironments().getFirst(); - DataManager dataManager = environment.dataManager(); - - SnapchatUser user = SnapchatUser.createSync(dataManager); - SnapchatUser favoriteUser = SnapchatUser.createSync(dataManager); - - assertEquals(0, user.getUpdateCalled()); - user.setFavoriteUser(favoriteUser); - - waitForDataPropagation(); - assertEquals(1, user.getUpdateCalled()); - - user.setFavoriteUser(null); - waitForDataPropagation(); - assertEquals(2, user.getUpdateCalled()); - } -} \ No newline at end of file diff --git a/oldsrc/ogtest/java/net/staticstudios/data/misc/DataTest.java b/oldsrc/ogtest/java/net/staticstudios/data/misc/DataTest.java deleted file mode 100644 index 0891cc29..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/misc/DataTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package net.staticstudios.data.misc; - -import com.redis.testcontainers.RedisContainer; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.util.DataSourceConfig; -import net.staticstudios.utils.ThreadUtils; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.utility.DockerImageName; -import redis.clients.jedis.Jedis; - -import java.io.IOException; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; - -public class DataTest { - public static final int NUM_ENVIRONMENTS = 1; - public static RedisContainer redis; - public static PostgreSQLContainer postgres = new PostgreSQLContainer<>( - "postgres:16.2" - ); - public static DataSourceConfig dataSourceConfig; - private static Connection connection; - private static Jedis jedis; - private List mockEnvironments; - - @BeforeAll - static void initPostgres() throws IOException, SQLException, InterruptedException { - postgres.start(); - redis = new RedisContainer(DockerImageName.parse("redis:6.2.6")); - redis.start(); - - redis.execInContainer("redis-cli", "config", "set", "notify-keyspace-events", "KEA"); - - dataSourceConfig = new DataSourceConfig( - postgres.getHost(), - postgres.getFirstMappedPort(), - postgres.getDatabaseName(), - postgres.getUsername(), - postgres.getPassword(), - redis.getHost(), - redis.getRedisPort() - ); - - connection = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); - jedis = new Jedis(redis.getHost(), redis.getRedisPort()); - } - - @AfterAll - public static void cleanup() throws IOException { - postgres.stop(); - redis.stop(); - } - - public static Connection getConnection() { - return connection; - } - - public static Jedis getJedis() { - return jedis; - } - - @BeforeEach - public void setupMockEnvironments() { - mockEnvironments = new LinkedList<>(); - ThreadUtils.setProvider(new MockThreadProvider()); - for (int i = 0; i < NUM_ENVIRONMENTS; i++) { - mockEnvironments.add(createMockEnvironment()); - } - } - - protected MockEnvironment createMockEnvironment() { - DataManager dataManager = new DataManager(dataSourceConfig); - - MockEnvironment mockEnvironment = new MockEnvironment(dataSourceConfig, dataManager); - mockEnvironments.add(mockEnvironment); - return mockEnvironment; - } - - @AfterEach - public void teardownThreadUtils() { - ThreadUtils.shutdown(); - } - - public int getNumEnvironments() { - return mockEnvironments.size(); - } - - public List getMockEnvironments() { - return mockEnvironments; - } - - public int getWaitForDataPropagationTime() { - return 500 + (Objects.equals(System.getenv("GITHUB_ACTIONS"), "true") ? 1000 : 0); - } - - public void waitForDataPropagation() { - try { - Thread.sleep(getWaitForDataPropagationTime()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java b/oldsrc/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java deleted file mode 100644 index b40bff74..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/misc/MockThreadProvider.java +++ /dev/null @@ -1,125 +0,0 @@ -package net.staticstudios.data.misc; - -import net.staticstudios.utils.ShutdownStage; -import net.staticstudios.utils.ShutdownTask; -import net.staticstudios.utils.ThreadUtilProvider; -import net.staticstudios.utils.ThreadUtils; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; -import java.util.logging.Logger; - -public class MockThreadProvider implements ThreadUtilProvider { - private final ExecutorService mainThreadExecutorService; - private final List syncOnDisableTasksRunNext = Collections.synchronizedList(new ArrayList<>()); - private final List shutdownTasks = Collections.synchronizedList(new ArrayList<>()); - private final Logger logger = Logger.getLogger(MockThreadProvider.class.getName()); - private ExecutorService executorService; - private boolean isShuttingDown = false; - private boolean doneShuttingDown = false; - - - public MockThreadProvider() { - this.mainThreadExecutorService = Executors.newSingleThreadExecutor(); - this.executorService = Executors.newCachedThreadPool((r) -> new Thread(r, "MockThreadProvider")); - } - - @Override - public void submit(Runnable runnable) { - if (doneShuttingDown) { - throw new IllegalStateException("Cannot submit tasks after shutdown"); - } - executorService.submit(() -> { - try { - runnable.run(); - } catch (Exception e) { - e.printStackTrace(); - } - }); - } - - @Override - public void runSync(Runnable runnable) { - if (isShuttingDown) { - syncOnDisableTasksRunNext.add(runnable); - return; - } - - mainThreadExecutorService.submit(runnable); - } - - @Override - public void onShutdownRunSync(ShutdownStage shutdownStage, Runnable runnable) { - shutdownTasks.add(new ShutdownTask(shutdownStage, () -> { - ThreadUtils.safe(runnable); - return null; - }, true)); - } - - @Override - public void onShutdownRunAsync(ShutdownStage shutdownStage, Supplier> task) { - shutdownTasks.add(new ShutdownTask(shutdownStage, task, false)); - - } - - @Override - public boolean isShuttingDown() { - return isShuttingDown; - } - - public void shutdown() { - isShuttingDown = true; - - executorService.shutdown(); - try { - executorService.awaitTermination(30, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - Map> tasks = new HashMap<>(); - shutdownTasks.forEach(task -> tasks.computeIfAbsent(task.stage(), k -> new ArrayList<>()).add(task)); - - ShutdownStage.getStages() - .forEach(stage -> { - if (tasks.containsKey(stage)) { - getLogger().info("Running shutdown tasks for stage " + stage); - - List> asyncFutures = new ArrayList<>(); - List syncTasks = new ArrayList<>(); - - tasks.get(stage).forEach(task -> { - if (task.sync()) { - syncTasks.add(() -> task.task().get()); - } else { - asyncFutures.add(task.task().get()); - } - }); - - //Wait for all async tasks to finish - try { - CompletableFuture.allOf(asyncFutures.toArray(new CompletableFuture[0])).get(30, TimeUnit.SECONDS); - } catch (Exception e) { - getLogger().severe("Failed to wait for async tasks to finish during shutdown stage " + stage); - e.printStackTrace(); - } - - syncTasks.forEach(Runnable::run); - - syncOnDisableTasksRunNext.forEach(Runnable::run); - - syncOnDisableTasksRunNext.clear(); - } - }); - - doneShuttingDown = true; - } - - private Logger getLogger() { - return logger; - } -} \ No newline at end of file diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java deleted file mode 100644 index 1fb1c107..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java +++ /dev/null @@ -1,72 +0,0 @@ -package net.staticstudios.data.mock.cachedvalue; - -import net.staticstudios.data.CachedValue; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.UniqueData; - -import java.sql.Timestamp; -import java.util.UUID; - -public class RedditUser extends UniqueData { - public final CachedValue status_updates = CachedValue.of(this, Integer.class, "status_updates") - .withFallback(0); - public final CachedValue status = CachedValue.of(this, String.class, "status") - .onUpdate(update -> status_updates.set(status_updates.get() + 1)); - public final CachedValue lastLogin = CachedValue.of(this, Timestamp.class, "last_login"); - public final CachedValue suspended = CachedValue.of(this, Boolean.class, "suspended") - .withFallback(false) - .withExpiry(3); - public final CachedValue bio = CachedValue.of(this, String.class, "bio") - .withFallback("This user has not set a bio yet."); - - private RedditUser(DataManager dataManager, UUID id) { - super(dataManager, "reddit", "users", id); - } - - public static RedditUser createSync(DataManager dataManager) { - RedditUser user = new RedditUser(dataManager, UUID.randomUUID()); - dataManager.insert(user); - - return user; - } - - public String getStatus() { - return status.get(); - } - - public void setStatus(String status) { - this.status.set(status); - } - - public int getStatusUpdates() { - return status_updates.get(); - } - - public void setStatusUpdates(int statusUpdates) { - this.status_updates.set(statusUpdates); - } - - public Timestamp getLastLogin() { - return lastLogin.get(); - } - - public void setLastLogin(Timestamp lastLogin) { - this.lastLogin.set(lastLogin); - } - - public boolean isSuspended() { - return suspended.get(); - } - - public void setSuspended(boolean suspended) { - this.suspended.set(suspended); - } - - public String getBio() { - return bio.get(); - } - - public void setBio(String bio) { - this.bio.set(bio); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java deleted file mode 100644 index 306c2999..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftServer.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.staticstudios.data.mock.deletions; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class MinecraftServer extends UniqueData { - private final PersistentValue name = PersistentValue.of(this, String.class, "name"); - - private MinecraftServer(DataManager dataManager, UUID id) { - super(dataManager, "minecraft", "servers", id); - } - - public static MinecraftServer createSync(DataManager dataManager, String name) { - MinecraftServer server = new MinecraftServer(dataManager, UUID.randomUUID()); - dataManager.insert(server, server.name.initial(name)); - - return server; - } - - public String getName() { - return name.get(); - } - - public void setName(String name) { - this.name.set(name); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java deleted file mode 100644 index 86d55682..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.staticstudios.data.mock.deletions; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class MinecraftSkin extends UniqueData { - private final PersistentValue name = PersistentValue.of(this, String.class, "name"); - - private MinecraftSkin(DataManager dataManager, UUID id) { - super(dataManager, "minecraft", "skins", id); - } - - public static MinecraftSkin createSync(DataManager dataManager, String name) { - MinecraftSkin server = new MinecraftSkin(dataManager, UUID.randomUUID()); - dataManager.insert(server, server.name.initial(name)); - - return server; - } - - public String getName() { - return name.get(); - } - - public void setName(String name) { - this.name.set(name); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java deleted file mode 100644 index c8593a9e..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.staticstudios.data.mock.deletions; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class MinecraftUserStatistics extends UniqueData { - private MinecraftUserStatistics(DataManager dataManager, UUID id) { - super(dataManager, "minecraft", "user_stats", id); - } - - public static MinecraftUserStatistics createSync(DataManager dataManager, UUID userId) { - MinecraftUserStatistics stats = new MinecraftUserStatistics(dataManager, userId); - dataManager.insert(stats); - - return stats; - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java deleted file mode 100644 index 3acbcece..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.staticstudios.data.mock.deletions; - -import net.staticstudios.data.*; -import net.staticstudios.data.util.DeletionStrategy; - -import java.sql.Timestamp; -import java.time.Instant; -import java.util.UUID; - -public class MinecraftUserWithCascadeDeletionStrategy extends UniqueData { - public final CachedValue ipAddress = CachedValue.of(this, String.class, "ip") - .deletionStrategy(DeletionStrategy.CASCADE); - public final PersistentValue name = PersistentValue.of(this, String.class, "name"); - public final PersistentValue accountCreation = PersistentValue.foreign(this, Timestamp.class, "minecraft.user_meta.account_creation", "id") - .withDefault(Timestamp.from(Instant.now())) - .deletionStrategy(DeletionStrategy.CASCADE); - public final PersistentCollection servers = PersistentCollection.manyToMany(this, MinecraftServer.class, "minecraft", "user_servers", "user_id", "server_id") - .deletionStrategy(DeletionStrategy.CASCADE); - public final PersistentCollection skins = PersistentCollection.oneToMany(this, MinecraftSkin.class, "minecraft", "skins", "user_id") - .deletionStrategy(DeletionStrategy.CASCADE); - public final Reference statistics = Reference.of(this, MinecraftUserStatistics.class, "id") - .deletionStrategy(DeletionStrategy.CASCADE); - public final PersistentCollection worldNames = PersistentCollection.of(this, String.class, "minecraft", "worlds", "user_id", "name") - .deletionStrategy(DeletionStrategy.CASCADE); - - private MinecraftUserWithCascadeDeletionStrategy(DataManager dataManager, UUID id) { - super(dataManager, "minecraft", "users", id); - } - - public static MinecraftUserWithCascadeDeletionStrategy createSync(DataManager dataManager, String name, String ipAddress) { - MinecraftUserWithCascadeDeletionStrategy user = new MinecraftUserWithCascadeDeletionStrategy(dataManager, UUID.randomUUID()); - MinecraftUserStatistics stats = MinecraftUserStatistics.createSync(dataManager, user.getId()); - dataManager.insert(user, user.name.initial(name), user.ipAddress.initial(ipAddress), user.statistics.initial(stats)); - - return user; - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java deleted file mode 100644 index 98401a32..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.staticstudios.data.mock.deletions; - -import net.staticstudios.data.*; -import net.staticstudios.data.util.DeletionStrategy; - -import java.sql.Timestamp; -import java.time.Instant; -import java.util.UUID; - -public class MinecraftUserWithNoActionDeletionStrategy extends UniqueData { - public final CachedValue ipAddress = CachedValue.of(this, String.class, "ip") - .deletionStrategy(DeletionStrategy.NO_ACTION); - public final PersistentValue name = PersistentValue.of(this, String.class, "name"); - public final PersistentValue accountCreation = PersistentValue.foreign(this, Timestamp.class, "minecraft.user_meta.account_creation", "id") - .withDefault(Timestamp.from(Instant.now())) - .deletionStrategy(DeletionStrategy.NO_ACTION); - public final PersistentCollection servers = PersistentCollection.manyToMany(this, MinecraftServer.class, "minecraft", "user_servers", "user_id", "server_id") - .deletionStrategy(DeletionStrategy.NO_ACTION); - public final PersistentCollection skins = PersistentCollection.oneToMany(this, MinecraftSkin.class, "minecraft", "skins", "user_id") - .deletionStrategy(DeletionStrategy.NO_ACTION); - public final Reference statistics = Reference.of(this, MinecraftUserStatistics.class, "id") - .deletionStrategy(DeletionStrategy.NO_ACTION); - public final PersistentCollection worldNames = PersistentCollection.of(this, String.class, "minecraft", "worlds", "user_id", "name") - .deletionStrategy(DeletionStrategy.NO_ACTION); - - private MinecraftUserWithNoActionDeletionStrategy(DataManager dataManager, UUID id) { - super(dataManager, "minecraft", "users", id); - } - - public static MinecraftUserWithNoActionDeletionStrategy createSync(DataManager dataManager, String name, String ipAddress) { - MinecraftUserWithNoActionDeletionStrategy user = new MinecraftUserWithNoActionDeletionStrategy(dataManager, UUID.randomUUID()); - MinecraftUserStatistics stats = MinecraftUserStatistics.createSync(dataManager, user.getId()); - dataManager.insert(user, user.name.initial(name), user.ipAddress.initial(ipAddress), user.statistics.initial(stats)); - - return user; - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java deleted file mode 100644 index ebb08ab8..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.staticstudios.data.mock.deletions; - -import net.staticstudios.data.*; -import net.staticstudios.data.util.DeletionStrategy; - -import java.sql.Timestamp; -import java.time.Instant; -import java.util.UUID; - -public class MinecraftUserWithUnlinkDeletionStrategy extends UniqueData { - public final CachedValue ipAddress = CachedValue.of(this, String.class, "ip") - .deletionStrategy(DeletionStrategy.UNLINK); - public final PersistentValue name = PersistentValue.of(this, String.class, "name"); - public final PersistentValue accountCreation = PersistentValue.foreign(this, Timestamp.class, "minecraft.user_meta.account_creation", "id") - .withDefault(Timestamp.from(Instant.now())) - .deletionStrategy(DeletionStrategy.UNLINK); - public final PersistentCollection servers = PersistentCollection.manyToMany(this, MinecraftServer.class, "minecraft", "user_servers", "user_id", "server_id") - .deletionStrategy(DeletionStrategy.UNLINK); - public final PersistentCollection skins = PersistentCollection.oneToMany(this, MinecraftSkin.class, "minecraft", "skins", "user_id") - .deletionStrategy(DeletionStrategy.UNLINK); - public final Reference statistics = Reference.of(this, MinecraftUserStatistics.class, "id") - .deletionStrategy(DeletionStrategy.UNLINK); - public final PersistentCollection worldNames = PersistentCollection.of(this, String.class, "minecraft", "worlds", "user_id", "name") - .deletionStrategy(DeletionStrategy.UNLINK); - - private MinecraftUserWithUnlinkDeletionStrategy(DataManager dataManager, UUID id) { - super(dataManager, "minecraft", "users", id); - } - - public static MinecraftUserWithUnlinkDeletionStrategy createSync(DataManager dataManager, String name, String ipAddress) { - MinecraftUserWithUnlinkDeletionStrategy user = new MinecraftUserWithUnlinkDeletionStrategy(dataManager, UUID.randomUUID()); - MinecraftUserStatistics stats = MinecraftUserStatistics.createSync(dataManager, user.getId()); - dataManager.insert(user, user.name.initial(name), user.ipAddress.initial(ipAddress), user.statistics.initial(stats)); - - return user; - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java deleted file mode 100644 index df44c3dc..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java +++ /dev/null @@ -1,23 +0,0 @@ -package net.staticstudios.data.mock.insertions; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.Reference; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.util.BatchInsert; - -import java.util.UUID; - -public class TwitchChatMessage extends UniqueData { - public final Reference sender = Reference.of(this, TwitchUser.class, "sender_id"); - - private TwitchChatMessage(DataManager dataManager, UUID id) { - super(dataManager, "twitch", "chat_messages", id); - } - - public static TwitchChatMessage enqueueCreation(BatchInsert batchInsert, DataManager dataManager, TwitchUser sender) { - TwitchChatMessage message = new TwitchChatMessage(dataManager, UUID.randomUUID()); - batchInsert.add(message, message.sender.initial(sender)); - - return message; - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java deleted file mode 100644 index 378dc205..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/insertions/TwitchUser.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.staticstudios.data.mock.insertions; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentCollection; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.util.BatchInsert; - -import java.util.UUID; - -public class TwitchUser extends UniqueData { - public final PersistentValue name = PersistentValue.of(this, String.class, "name"); - public final PersistentCollection messages = PersistentCollection.oneToMany(this, TwitchChatMessage.class, "twitch", "chat_messages", "sender_id"); - - private TwitchUser(DataManager dataManager, UUID id) { - super(dataManager, "twitch", "users", id); - } - - public static TwitchUser enqueueCreation(BatchInsert batchInsert, DataManager dataManager, String name) { - TwitchUser user = new TwitchUser(dataManager, UUID.randomUUID()); - batchInsert.add(user, user.name.initial(name)); - - return user; - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java deleted file mode 100644 index f050be7c..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.staticstudios.data.mock.persistentcollection; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.Reference; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class FacebookPost extends UniqueData { - private final PersistentValue description = PersistentValue.of(this, String.class, "description"); - private final PersistentValue likes = PersistentValue.of(this, Integer.class, "likes") - .withDefault(0); - private final Reference user = Reference.of(this, FacebookUser.class, "user_id"); - - private FacebookPost(DataManager dataManager, UUID id) { - super(dataManager, "facebook", "posts", id); - } - - public static FacebookPost createSync(DataManager dataManager, String description, FacebookUser user) { - FacebookPost post = new FacebookPost(dataManager, UUID.randomUUID()); - - dataManager.insert(post, post.description.initial(description), post.user.initial(user)); - - return post; - } - - public String getDescription() { - return description.get(); - } - - public void setDescription(String description) { - this.description.set(description); - } - - public Integer getLikes() { - return likes.get(); - } - - public void setLikes(Integer likes) { - this.likes.set(likes); - } - - public FacebookUser getUser() { - return user.get(); - } - - public void setUser(FacebookUser user) { - this.user.set(user); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java deleted file mode 100644 index ecde9d6e..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.staticstudios.data.mock.persistentcollection; - -import net.staticstudios.data.CachedValue; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentCollection; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class FacebookUser extends UniqueData { - public final CachedValue followingAdditions = CachedValue.of(this, Integer.class, "following_adds").withFallback(0); - public final CachedValue postAdditions = CachedValue.of(this, Integer.class, "post_adds").withFallback(0); - public final CachedValue favoriteQuoteAdditions = CachedValue.of(this, Integer.class, "favorite_quote_adds").withFallback(0); - public final CachedValue followingRemovals = CachedValue.of(this, Integer.class, "following_removes").withFallback(0); - public final CachedValue postRemovals = CachedValue.of(this, Integer.class, "post_removes").withFallback(0); - public final CachedValue favoriteQuoteRemovals = CachedValue.of(this, Integer.class, "favorite_quote_removes").withFallback(0); - - public final PersistentCollection following = PersistentCollection.manyToMany(this, FacebookUser.class, "facebook", "user_following", "user_id", "following_id") - .onAdd(change -> followingAdditions.set(followingAdditions.get() + 1)) - .onRemove(change -> followingRemovals.set(followingRemovals.get() + 1)); - public final PersistentCollection liked = PersistentCollection.manyToMany(this, FacebookPost.class, "facebook", "user_liked_posts", "user_id", "post_id"); - public final PersistentCollection followers = PersistentCollection.manyToMany(this, FacebookUser.class, "facebook", "user_following", "following_id", "user_id"); - public final PersistentCollection posts = PersistentCollection.oneToMany(this, FacebookPost.class, "facebook", "posts", "user_id") - .onAdd(change -> postAdditions.set(postAdditions.get() + 1)) - .onRemove(change -> postRemovals.set(postRemovals.get() + 1)); - public final PersistentCollection favoriteQuotes = PersistentCollection.of(this, String.class, "facebook", "favorite_quotes", "user_id", "quote") - .onAdd(change -> favoriteQuoteAdditions.set(favoriteQuoteAdditions.get() + 1)) - .onRemove(change -> favoriteQuoteRemovals.set(favoriteQuoteRemovals.get() + 1)); - - private FacebookUser(DataManager dataManager, UUID id) { - super(dataManager, "facebook", "users", id); - } - - public static FacebookUser createSync(DataManager dataManager) { - FacebookUser user = new FacebookUser(dataManager, UUID.randomUUID()); - dataManager.insert(user); - - return user; - } - - public PersistentCollection getPosts() { - return posts; - } - - public PersistentCollection getFavoriteQuotes() { - return favoriteQuotes; - } - - public PersistentCollection getFollowing() { - return following; - } - - public PersistentCollection getFollowers() { - return followers; - } - - public PersistentCollection getLiked() { - return liked; - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java deleted file mode 100644 index efd19698..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java +++ /dev/null @@ -1,66 +0,0 @@ -package net.staticstudios.data.mock.persistentvalue; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.util.InsertionStrategy; - -import java.util.UUID; - -public class DiscordUser extends UniqueData { - public final PersistentValue nameUpdatesCalled = PersistentValue.foreign(this, Integer.class, "discord.user_meta.name_updates_called", "id") - .withDefault(0); - public final PersistentValue enableFriendRequestsUpdatesCalled = PersistentValue.foreign(this, Integer.class, "discord.user_meta.enable_friend_requests_updates_called", "id") - .withDefault(0); - public final PersistentValue name = PersistentValue.of(this, String.class, "name") - .onUpdate(update -> { - if (update.oldValue() == null) { - return; - } - nameUpdatesCalled.set(nameUpdatesCalled.get() + 1); - }); - public final PersistentValue enableFriendRequests = PersistentValue.foreign(this, Boolean.class, "discord", "user_settings", "enable_friend_requests", "user_id") - .withDefault(true) - .insertionStrategy(InsertionStrategy.PREFER_EXISTING) - .onUpdate(update -> { - if (update.oldValue() == null) { - return; - } - enableFriendRequestsUpdatesCalled.set(enableFriendRequestsUpdatesCalled.get() + 1); - }); - - private DiscordUser(DataManager dataManager, UUID id) { - super(dataManager, "discord", "users", id); - } - - public static DiscordUser createSync(DataManager dataManager, String name, UUID id) { - DiscordUser user = new DiscordUser(dataManager, id); - dataManager.insert(user, user.name.initial(name)); - - return user; - } - - public String getName() { - return name.get(); - } - - public void setName(String name) { - this.name.set(name); - } - - public boolean getEnableFriendRequests() { - return enableFriendRequests.get(); - } - - public void setEnableFriendRequests(boolean enableFriendRequests) { - this.enableFriendRequests.set(enableFriendRequests); - } - - public int getNameUpdatesCalled() { - return nameUpdatesCalled.get(); - } - - public int getEnableFriendRequestsUpdatesCalled() { - return enableFriendRequestsUpdatesCalled.get(); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java deleted file mode 100644 index 645eb2e8..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.staticstudios.data.mock.persistentvalue; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class DiscordUserSettings extends UniqueData { - private final PersistentValue enableFriendRequests = PersistentValue.of(this, Boolean.class, "enable_friend_requests") - .withDefault(true); - - private DiscordUserSettings(DataManager dataManager, UUID id) { - super(dataManager, "discord", "user_settings", "user_id", id); - } - - public static DiscordUserSettings createSync(DataManager dataManager, String name) { - DiscordUserSettings user = new DiscordUserSettings(dataManager, UUID.randomUUID()); - dataManager.insert(user); - - return user; - } - - public boolean getEnableFriendRequests() { - return enableFriendRequests.get(); - } - - public void setEnableFriendRequests(boolean enableFriendRequests) { - this.enableFriendRequests.set(enableFriendRequests); - } - -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java deleted file mode 100644 index 10dc6f66..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class BooleanPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, Boolean.class, "value"); - - private BooleanPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "boolean_test", id); - } - - public static BooleanPrimitiveTestObject createSync(DataManager dataManager, Boolean initialValue) { - BooleanPrimitiveTestObject obj = new BooleanPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(Boolean value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java deleted file mode 100644 index 7fa01bc4..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class ByteArrayPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, byte[].class, "value"); - - private ByteArrayPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "byte_array_test", id); - } - - public static ByteArrayPrimitiveTestObject createSync(DataManager dataManager, byte[] initialValue) { - ByteArrayPrimitiveTestObject obj = new ByteArrayPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(byte[] value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java deleted file mode 100644 index 5821011e..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class BytePrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, Byte.class, "value"); - - private BytePrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "byte_test", id); - } - - public static BytePrimitiveTestObject createSync(DataManager dataManager, Byte initialValue) { - BytePrimitiveTestObject obj = new BytePrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(Byte value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java deleted file mode 100644 index 43a162c0..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class CharacterPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, Character.class, "value"); - - private CharacterPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "character_test", id); - } - - public static CharacterPrimitiveTestObject createSync(DataManager dataManager, Character initialValue) { - CharacterPrimitiveTestObject obj = new CharacterPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(Character value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java deleted file mode 100644 index 92852b3b..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class DoublePrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, Double.class, "value"); - - private DoublePrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "double_test", id); - } - - public static DoublePrimitiveTestObject createSync(DataManager dataManager, Double initialValue) { - DoublePrimitiveTestObject obj = new DoublePrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(Double value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java deleted file mode 100644 index f6e6b5c2..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class FloatPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, Float.class, "value"); - - private FloatPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "float_test", id); - } - - public static FloatPrimitiveTestObject createSync(DataManager dataManager, Float initialValue) { - FloatPrimitiveTestObject obj = new FloatPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(Float value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java deleted file mode 100644 index 1bf29c99..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class IntegerPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, Integer.class, "value"); - - private IntegerPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "integer_test", id); - } - - public static IntegerPrimitiveTestObject createSync(DataManager dataManager, Integer initialValue) { - IntegerPrimitiveTestObject obj = new IntegerPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(Integer value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java deleted file mode 100644 index dbf567d0..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class LongPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, Long.class, "value"); - - private LongPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "long_test", id); - } - - public static LongPrimitiveTestObject createSync(DataManager dataManager, Long initialValue) { - LongPrimitiveTestObject obj = new LongPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(Long value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java deleted file mode 100644 index f2993ecf..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class ShortPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, Short.class, "value"); - - private ShortPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "short_test", id); - } - - public static ShortPrimitiveTestObject createSync(DataManager dataManager, Short initialValue) { - ShortPrimitiveTestObject obj = new ShortPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(Short value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java deleted file mode 100644 index 0ccd310f..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class StringPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, String.class, "value"); - - private StringPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "string_test", id); - } - - public static StringPrimitiveTestObject createSync(DataManager dataManager, String initialValue) { - StringPrimitiveTestObject obj = new StringPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(String value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java deleted file mode 100644 index 75201457..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.sql.Timestamp; -import java.util.UUID; - -public class TimestampPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, Timestamp.class, "value"); - - private TimestampPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "timestamp_test", id); - } - - public static TimestampPrimitiveTestObject createSync(DataManager dataManager, Timestamp initialValue) { - TimestampPrimitiveTestObject obj = new TimestampPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(Timestamp value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java deleted file mode 100644 index 93644074..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.mock.primative; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class UUIDPrimitiveTestObject extends UniqueData { - private final PersistentValue value = PersistentValue.of(this, UUID.class, "value"); - - private UUIDPrimitiveTestObject(DataManager dataManager, UUID id) { - super(dataManager, "primitive", "uuid_test", id); - } - - public static UUIDPrimitiveTestObject createSync(DataManager dataManager, UUID initialValue) { - UUIDPrimitiveTestObject obj = new UUIDPrimitiveTestObject(dataManager, UUID.randomUUID()); - dataManager.insert(obj, obj.value.initial(initialValue)); - - return obj; - } - - public void setValue(UUID value) { - this.value.set(value); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java deleted file mode 100644 index ebe58e45..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUser.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.staticstudios.data.mock.reference; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.Reference; -import net.staticstudios.data.UniqueData; -import org.jetbrains.annotations.Nullable; - -import java.util.UUID; - -public class SnapchatUser extends UniqueData { - private final PersistentValue updateCalled = PersistentValue.foreign(this, Integer.class, "snapchat.user_meta.update_called", "id") - .withDefault(0); - private final Reference settings = Reference.of(this, SnapchatUserSettings.class, "id"); - private final Reference favoriteUser = Reference.of(this, SnapchatUser.class, "favorite_user_id") - .onUpdate(update -> { - updateCalled.set(updateCalled.get() + 1); - }); - - private SnapchatUser(DataManager dataManager, UUID id) { - super(dataManager, "snapchat", "users", id); - } - - public static SnapchatUser createSync(DataManager dataManager) { - SnapchatUser user = new SnapchatUser(dataManager, UUID.randomUUID()); - SnapchatUserSettings settings = SnapchatUserSettings.createSync(dataManager, user.getId()); - dataManager.insert(user, user.settings.initial(settings)); - - return user; - } - - public SnapchatUserSettings getSettings() { - return settings.get(); - } - - public @Nullable SnapchatUser getFavoriteUser() { - return favoriteUser.get(); - } - - public void setFavoriteUser(@Nullable SnapchatUser favoriteUser) { - this.favoriteUser.set(favoriteUser); - } - - public Integer getUpdateCalled() { - return updateCalled.get(); - } -} diff --git a/oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java b/oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java deleted file mode 100644 index 935d6efa..00000000 --- a/oldsrc/ogtest/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.staticstudios.data.mock.reference; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.PersistentValue; -import net.staticstudios.data.Reference; -import net.staticstudios.data.UniqueData; - -import java.util.UUID; - -public class SnapchatUserSettings extends UniqueData { - private final Reference user = Reference.of(this, SnapchatUser.class, "user_id"); - private final PersistentValue enableFriendRequests = PersistentValue.of(this, Boolean.class, "enable_friend_requests") - .withDefault(true); - - private SnapchatUserSettings(DataManager dataManager, UUID id) { - super(dataManager, "snapchat", "user_settings", "user_id", id); - } - - public static SnapchatUserSettings createSync(DataManager dataManager, UUID id) { - SnapchatUserSettings settings = new SnapchatUserSettings(dataManager, id); - dataManager.insert(settings); - - return settings; - } - - public boolean getEnableFriendRequests() { - return enableFriendRequests.get(); - } - - public void setEnableFriendRequests(boolean enableFriendRequests) { - this.enableFriendRequests.set(enableFriendRequests); - } - - public SnapchatUser getUser() { - return user.get(); - } -} diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index dfbdc99b..67b1a19e 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -20,7 +20,7 @@ @SupportedAnnotationTypes("net.staticstudios.data.Data") @SupportedSourceVersion(SourceVersion.RELEASE_21) -public class DataProcessor extends AbstractProcessor { //todo: this seems to be in the classpath of the main project. address this. +public class DataProcessor extends AbstractProcessor { //TODO: delete this class since annotation processing is no longer used. @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { for (Element annotated : roundEnv.getElementsAnnotatedWith(Data.class)) { diff --git a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java index 37652f32..d2fcd11c 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java +++ b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java @@ -273,7 +273,7 @@ private void makeOrderByClause(TypeSpec.Builder builderType, PersistentValueMeta MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName) .addModifiers(Modifier.PUBLIC) .returns(returnType) - .addParameter(ClassName.get("net.staticstudios.data.query", "Order"), "order"); + .addParameter(ClassName.get("net.staticstudios.data", "Order"), "order"); handleForeignPersistentValue(builder, persistentValueMetadata); diff --git a/settings.gradle b/settings.gradle index c5274d1a..76985d10 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,8 @@ rootProject.name = 'static-data' include 'annotations' -include 'processor' \ No newline at end of file +include 'processor' +include 'benchmark' +include 'core' +include 'javac-plugin' +include 'intellij-plugin' \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresData.java b/src/main/java/net/staticstudios/data/impl/pg/PostgresData.java deleted file mode 100644 index 09950eee..00000000 --- a/src/main/java/net/staticstudios/data/impl/pg/PostgresData.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.staticstudios.data.impl.pg; - -import com.google.gson.annotations.SerializedName; - -import java.util.Map; - -public record PostgresData(@SerializedName("new") Map newDataValueMap, - @SerializedName("old") Map oldDataValueMap) { -} diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java b/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java deleted file mode 100644 index 0571efc9..00000000 --- a/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java +++ /dev/null @@ -1,50 +0,0 @@ -package net.staticstudios.data.impl.pg; - -import java.time.Instant; - -public class PostgresNotification { - private final Instant instant; - private final String schema; - private final String table; - private final PostgresOperation operation; - private final PostgresData data; - - public PostgresNotification(Instant instant, String schema, String table, PostgresOperation operation, PostgresData data) { - this.instant = instant; - this.schema = schema; - this.table = table; - this.operation = operation; - this.data = data; - } - - public Instant getInstant() { - return instant; - } - - public String getSchema() { - return schema; - } - - public String getTable() { - return table; - } - - public PostgresOperation getOperation() { - return operation; - } - - public PostgresData getData() { - return data; - } - - @Override - public String toString() { - return "PostgresNotification{" + - "instant=" + instant + - ", schema='" + schema + '\'' + - ", table='" + table + '\'' + - ", operation=" + operation + - ", data=" + data + - '}'; - } -} diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java b/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java deleted file mode 100644 index c45ffe95..00000000 --- a/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.staticstudios.data.impl.pg; - -public enum PostgresOperation { - INSERT, - UPDATE, - DELETE -} diff --git a/src/main/java/net/staticstudios/data/util/ConnectionConsumer.java b/src/main/java/net/staticstudios/data/util/ConnectionConsumer.java deleted file mode 100644 index 9beefca2..00000000 --- a/src/main/java/net/staticstudios/data/util/ConnectionConsumer.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.staticstudios.data.util; - -import java.sql.Connection; -import java.sql.SQLException; - -public interface ConnectionConsumer { - void accept(Connection connection) throws SQLException; -} diff --git a/src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java b/src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java deleted file mode 100644 index 8f6f209c..00000000 --- a/src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java +++ /dev/null @@ -1,10 +0,0 @@ -package net.staticstudios.data.util; - -import redis.clients.jedis.Jedis; - -import java.sql.Connection; -import java.sql.SQLException; - -public interface ConnectionJedisConsumer { - void accept(Connection connection, Jedis jedis) throws SQLException; -} diff --git a/src/main/java/net/staticstudios/data/util/DataSourceConfig.java b/src/main/java/net/staticstudios/data/util/DataSourceConfig.java deleted file mode 100644 index ee8adf5a..00000000 --- a/src/main/java/net/staticstudios/data/util/DataSourceConfig.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.staticstudios.data.util; - -public record DataSourceConfig( - String databaseHost, - int databasePort, - String databaseName, - String databaseUsername, - String databasePassword, - String redisHost, - int redisPort -) { -} diff --git a/src/main/java/net/staticstudios/data/util/PostgresUtils.java b/src/main/java/net/staticstudios/data/util/PostgresUtils.java deleted file mode 100644 index 89813c45..00000000 --- a/src/main/java/net/staticstudios/data/util/PostgresUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.staticstudios.data.util; - -public class PostgresUtils { - public static byte[] toBytes(String hex) { - hex = hex.substring(2); // Remove the \x prefix - byte[] bytes = new byte[hex.length() / 2]; - for (int i = 0; i < hex.length(); i += 2) { - bytes[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16)); - } - - return bytes; - } - - public static String toHex(byte[] bytes) { - StringBuilder sb = new StringBuilder("\\x"); - for (byte b : bytes) { - sb.append(String.format("%02x", b)); - } - - return sb.toString(); - } -} diff --git a/src/main/java/net/staticstudios/data/util/TaskQueue.java b/src/main/java/net/staticstudios/data/util/TaskQueue.java deleted file mode 100644 index e81d618a..00000000 --- a/src/main/java/net/staticstudios/data/util/TaskQueue.java +++ /dev/null @@ -1,111 +0,0 @@ -package net.staticstudios.data.util; - -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.pool.HikariPool; -import net.staticstudios.utils.ShutdownStage; -import net.staticstudios.utils.ThreadUtils; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; - -import java.sql.Connection; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; - -public class TaskQueue { - private final BlockingDeque taskQueue = new LinkedBlockingDeque<>(); - private final AtomicBoolean isShutdown = new AtomicBoolean(false); - private final ExecutorService executor; - private final HikariPool connectionPool; - private final JedisPool jedisPool; - - public TaskQueue(DataSourceConfig config, String applicationName) { - HikariConfig poolConfig = new HikariConfig(); - poolConfig.setDataSourceClassName("com.impossibl.postgres.jdbc.PGDataSource"); - poolConfig.addDataSourceProperty("serverName", config.databaseHost()); - poolConfig.addDataSourceProperty("portNumber", config.databasePort()); - poolConfig.addDataSourceProperty("user", config.databaseUsername()); - poolConfig.addDataSourceProperty("password", config.databasePassword()); - poolConfig.addDataSourceProperty("databaseName", config.databaseName()); - poolConfig.addDataSourceProperty("ApplicationName", applicationName); - poolConfig.setLeakDetectionThreshold(10000); - poolConfig.setMaximumPoolSize(1); - - this.connectionPool = new HikariPool(poolConfig); - this.jedisPool = new JedisPool(config.redisHost(), config.redisPort()); - this.jedisPool.setMaxTotal(1); - - executor = Executors.newSingleThreadExecutor(r -> { - Thread thread = new Thread(r); - thread.setName("SQLTaskQueue"); - thread.setDaemon(true); - return thread; - }); - - start(); - - ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, this::shutdown); - } - - public CompletableFuture submitTask(ConnectionConsumer task) { - return submitTask((connection, jedis) -> task.accept(connection)); - } - - public CompletableFuture submitTask(ConnectionJedisConsumer task) { - CompletableFuture future = new CompletableFuture<>(); - taskQueue.addLast((connection, jedis) -> { - try { - task.accept(connection, jedis); - future.complete(null); - } catch (Exception e) { - future.completeExceptionally(e); - } - }); - return future; - } - - private void start() { - executor.submit(() -> { - while (!(isShutdown.get() && taskQueue.isEmpty())) { - ConnectionJedisConsumer task; - try { - task = taskQueue.takeFirst(); - } catch (InterruptedException e) { - // We're shutting down - break; - } - - try ( - Connection connection = connectionPool.getConnection(); - Jedis jedis = jedisPool.getResource() - ) { - task.accept(connection, jedis); - - if (!connection.getAutoCommit()) { - connection.setAutoCommit(true); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - private void shutdown() { - if (!isShutdown.compareAndSet(false, true)) { - return; - } - executor.shutdown(); - - if (taskQueue.isEmpty()) { - executor.shutdownNow(); - } - - try { - if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { - executor.shutdownNow(); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/net/staticstudios/data/util/ValueUpdate.java b/src/main/java/net/staticstudios/data/util/ValueUpdate.java deleted file mode 100644 index aa715c43..00000000 --- a/src/main/java/net/staticstudios/data/util/ValueUpdate.java +++ /dev/null @@ -1,6 +0,0 @@ -package net.staticstudios.data.util; - -import org.jetbrains.annotations.Nullable; - -public record ValueUpdate(@Nullable T oldValue, @Nullable T newValue) { -} diff --git a/src/test/java/net/staticstudios/data/misc/MockEnvironment.java b/src/test/java/net/staticstudios/data/misc/MockEnvironment.java deleted file mode 100644 index a5b37ed1..00000000 --- a/src/test/java/net/staticstudios/data/misc/MockEnvironment.java +++ /dev/null @@ -1,10 +0,0 @@ -package net.staticstudios.data.misc; - -import net.staticstudios.data.DataManager; -import net.staticstudios.data.util.DataSourceConfig; - -public record MockEnvironment( - DataSourceConfig dataSourceConfig, - DataManager dataManager -) { -} diff --git a/src/test/java/net/staticstudios/data/misc/TestUtils.java b/src/test/java/net/staticstudios/data/misc/TestUtils.java deleted file mode 100644 index eeb5a95a..00000000 --- a/src/test/java/net/staticstudios/data/misc/TestUtils.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.staticstudios.data.misc; - -import java.sql.ResultSet; -import java.sql.SQLException; - -public class TestUtils { - public static int getResultCount(ResultSet rs) throws SQLException { - if (rs.getType() == ResultSet.TYPE_FORWARD_ONLY) { - int count = rs.getRow(); - while (rs.next()) { - count++; - } - return count; - } - - int currentRow = rs.getRow(); - try { - rs.last(); - int rowCount = rs.getRow(); - rs.absolute(currentRow); - return rowCount; - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} From 70a45a6fb47aa224735aff1d45dd55c4e5885bc5 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Thu, 23 Oct 2025 12:07:34 -0400 Subject: [PATCH 36/75] update .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f35ca9eb..1dfc2d8a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,6 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +.intellijPlatform \ No newline at end of file From c16e3717a487a5fab3d5d5e54a2991f8dca9b8bc Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Thu, 23 Oct 2025 12:11:25 -0400 Subject: [PATCH 37/75] added .builder(DataManager) and .query(DataManager) to intellij plugin --- .../ide/intellij/DataPsiAugmentProvider.java | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) 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 9926fd37..1c7f02e9 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 @@ -52,7 +52,7 @@ public class DataPsiAugmentProvider extends PsiAugmentProvider { } if (type.isAssignableFrom(PsiMethod.class)) { - return List.of(type.cast(getBuilderMethod(psiClass)), type.cast(getQueryMethod(psiClass))); + return List.of(type.cast(getBuilderMethod(psiClass)), type.cast(getBuilderMethod2(psiClass)), type.cast(getQueryMethod(psiClass)), type.cast(getQueryMethod2(psiClass))); } return Collections.emptyList(); @@ -97,6 +97,27 @@ private PsiMethod getBuilderMethod(PsiClass parent) { return builderMethod; } + 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; + } + private PsiMethod getQueryMethod(PsiClass parent) { // return CachedValuesManager.getCachedValue(parent, () -> { // PsiClass builderClass = getBuilderClass(parent); @@ -115,6 +136,27 @@ private PsiMethod getQueryMethod(PsiClass parent) { return builderMethod; } + 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; + } + private SyntheticBuilderClass createBuilderBuilderClass(PsiClass parentClass) { SyntheticBuilderClass builderClass = new SyntheticBuilderClass(parentClass, "Builder"); PsiType builderType = JavaPsiFacade.getElementFactory(parentClass.getProject()) From 2cdbc8fc12dbf13aa1215cb4452b0dff331b0519 Mon Sep 17 00:00:00 2001 From: Noah <59799222+Leguan16@users.noreply.github.com> Date: Thu, 23 Oct 2025 20:36:17 +0200 Subject: [PATCH 38/75] Add dependencySupport entry (#23) --- intellij-plugin/src/main/resources/META-INF/plugin.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml index 3bd0b421..b0d0b746 100644 --- a/intellij-plugin/src/main/resources/META-INF/plugin.xml +++ b/intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -11,5 +11,7 @@ + + From d339f9242185fbb076a98a5e48260b22701767d1 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 28 Oct 2025 23:25:13 -0400 Subject: [PATCH 39/75] javac builder implementation --- .../java/net/staticstudios/data/OneToOne.java | 3 + .../data/benchmark/StaticDataBenchmark.java | 1 + core/build.gradle | 91 +-- .../net/staticstudios/data/DataManager.java | 40 +- .../net/staticstudios/data/UniqueData.java | 11 +- .../PersistentManyToManyCollectionImpl.java | 98 +-- .../PersistentOneToManyCollectionImpl.java | 28 +- .../data/impl/data/PersistentValueImpl.java | 9 +- .../data/impl/data/ReferenceImpl.java | 16 +- .../H2DeleteStrategyCascadeTrigger.java | 8 +- .../data/insert/InsertContext.java | 2 +- .../staticstudios/data/parse/ForeignKey.java | 7 +- .../staticstudios/data/parse/SQLBuilder.java | 46 +- .../data/parse/SQLDeleteStrategyTrigger.java | 5 +- .../util/ForeignPersistentValueMetadata.java | 8 +- ...PersistentOneToManyCollectionMetadata.java | 8 +- .../data/util/ReferenceMetadata.java | 8 +- .../staticstudios/data/util/StringUtils.java | 9 - .../staticstudios/data/util/ValueUtils.java | 11 - .../data/PersistentValueTest.java | 32 +- .../data/mock/user/MockUser.java | 1 + intellij-plugin/build.gradle | 3 +- .../ide/intellij/DataPsiAugmentProvider.java | 44 +- .../{Utils.java => IntelliJPluginUtils.java} | 15 +- .../ide/intellij/query/NumericClause.java | 4 +- .../ide/intellij/query/QueryBuilderUtils.java | 6 +- .../intellij/query/clause/IsLikeClause.java | 4 +- .../query/clause/IsNotLikeClause.java | 4 +- .../src/main/resources/META-INF/plugin.xml | 2 +- javac-plugin/build.gradle | 5 +- .../data/compiler/javac/BuilderProcessor.java | 641 ++++++++++++++++++ .../data/compiler/javac/JavaCPluginUtils.java | 358 ++++++++++ .../data/compiler/javac/ParsedAnnotation.java | 14 + .../javac/ParsedColumnAnnotation.java | 61 ++ .../compiler/javac/ParsedDataAnnotation.java | 37 + .../javac/ParsedForeignPersistentValue.java | 28 + .../compiler/javac/ParsedPersistentValue.java | 131 ++++ .../data/compiler/javac/ParsedReference.java | 99 +++ .../compiler/javac/StaticDataJavacPlugin.java | 65 +- processor/build.gradle | 1 + .../data/processor/DataProcessor.java | 18 +- settings.gradle | 3 +- utils/build.gradle | 18 + .../staticstudios/data/utils}/Constants.java | 7 +- .../net/staticstudios/data/utils/Link.java | 31 + .../staticstudios/data/utils/StringUtils.java | 0 46 files changed, 1728 insertions(+), 313 deletions(-) delete mode 100644 core/src/main/java/net/staticstudios/data/util/StringUtils.java rename intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/{Utils.java => IntelliJPluginUtils.java} (80%) create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedAnnotation.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedColumnAnnotation.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedDataAnnotation.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java create mode 100644 utils/build.gradle rename {intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij => utils/src/main/java/net/staticstudios/data/utils}/Constants.java (73%) create mode 100644 utils/src/main/java/net/staticstudios/data/utils/Link.java rename {annotations => utils}/src/main/java/net/staticstudios/data/utils/StringUtils.java (100%) diff --git a/annotations/src/main/java/net/staticstudios/data/OneToOne.java b/annotations/src/main/java/net/staticstudios/data/OneToOne.java index 4526df50..4a2cfe45 100644 --- a/annotations/src/main/java/net/staticstudios/data/OneToOne.java +++ b/annotations/src/main/java/net/staticstudios/data/OneToOne.java @@ -16,4 +16,7 @@ * @return The link format */ String link(); + + //todo: option to force not null? + } diff --git a/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java index 95b94a59..9ea9e842 100644 --- a/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java +++ b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java @@ -22,6 +22,7 @@ public void sampleBenchmark(StaticDataBenchmarkState state) { @Benchmark public void testPersistentValueRead(StaticDataBenchmarkState state) { + } @Benchmark diff --git a/core/build.gradle b/core/build.gradle index 77f5f521..9b2f333f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -16,6 +16,7 @@ repositories { } dependencies { + implementation(project(":utils")) implementation 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.9' implementation 'com.zaxxer:HikariCP:5.1.0' implementation 'redis.clients:jedis:5.1.2' @@ -43,53 +44,53 @@ dependencies { testImplementation("org.testcontainers:postgresql:1.19.8") testImplementation("com.redis:testcontainers-redis:2.2.2") testImplementation("org.slf4j:slf4j-log4j12:2.0.16") -// compileOnly project(':javac-plugin') //TODO: java-c -// annotationProcessor project(':javac-plugin') -// testCompileOnly project(':javac-plugin') -// testAnnotationProcessor project(':javac-plugin') + compileOnly project(':javac-plugin') //TODO: java-c + annotationProcessor project(':javac-plugin') + testCompileOnly project(':javac-plugin') + testAnnotationProcessor project(':javac-plugin') } -//tasks.named('compileJava').configure { -// options.fork = true -// options.forkOptions.jvmArgs += [ -// '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', -// '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' -// ] -// -// options.compilerArgs += [ -// '-Xplugin:StaticDataJavacPlugin' -// ] -//} -// -//tasks.named('compileTestJava').configure { -// options.fork = true -// options.forkOptions.jvmArgs += [ -// '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', -// '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', -// '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' -// ] -// -// options.compilerArgs += [ -// '-Xplugin:StaticDataJavacPlugin' -// ] -//} +tasks.named('compileJava').configure { + options.fork = true + options.forkOptions.jvmArgs += [ + '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' + ] + + options.compilerArgs += [ + '-Xplugin:StaticDataJavacPlugin' + ] +} + +tasks.named('compileTestJava').configure { + options.fork = true + options.forkOptions.jvmArgs += [ + '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' + ] + + options.compilerArgs += [ + '-Xplugin:StaticDataJavacPlugin' + ] +} tasks.named('build') { diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index b761ccd9..f13adba6 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -13,6 +13,7 @@ import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; +import net.staticstudios.data.utils.Link; import net.staticstudios.utils.ThreadUtils; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; @@ -462,7 +463,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { for (ForeignKey fKey : table.getForeignKeys()) { SQLSchema referencedSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getReferencedSchema())); SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(fKey.getReferencedTable())); - for (ForeignKey.Link link : fKey.getLinkingColumns()) { + for (Link link : fKey.getLinkingColumns()) { String myColumnName = link.columnInReferringTable(); String otherColumnName = link.columnInReferencedTable(); SQLColumn otherColumn = Objects.requireNonNull(referencedTable.getColumn(otherColumnName)); @@ -493,7 +494,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { boolean addDependency = true; // if one of the linking columns is not present in the insert context, we can't add the dependency - for (ForeignKey.Link link : fKey.getLinkingColumns()) { + for (Link link : fKey.getLinkingColumns()) { Object value = insertContext.getEntries().entrySet().stream() .filter(entry -> { SimpleColumnMetadata key = entry.getKey(); @@ -643,12 +644,12 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { } } - public T get(String schema, String table, String column, ColumnValuePairs idColumns, List idColumnLinks, Class dataType) { + 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(); - for (ForeignKey.Link link : idColumnLinks) { + for (Link link : idColumnLinks) { if (link.columnInReferringTable().equals(columnValuePair.column())) { name = link.columnInReferencedTable(); break; @@ -671,7 +672,7 @@ public T get(String schema, String table, String column, ColumnValuePairs id } @ApiStatus.Internal - public void set(String schema, String table, String column, ColumnValuePairs idColumns, List idColumnLinks, Object value, int delay) { + public void set(String schema, String table, String column, ColumnValuePairs idColumns, List idColumnLinks, Object value, int delay) { StringBuilder sqlBuilder; if (idColumnLinks.isEmpty()) { sqlBuilder = new StringBuilder().append("UPDATE \"").append(schema).append("\".\"").append(table).append("\" SET \"").append(column).append("\" = ? WHERE "); @@ -686,7 +687,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC sqlBuilder.append(")) AS source (\"").append(column).append("\""); for (ColumnValuePair columnValuePair : idColumns) { String name = columnValuePair.column(); - for (ForeignKey.Link link : idColumnLinks) { + for (Link link : idColumnLinks) { if (link.columnInReferringTable().equals(columnValuePair.column())) { name = link.columnInReferencedTable(); break; @@ -697,7 +698,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC sqlBuilder.append(") ON "); for (ColumnValuePair columnValuePair : idColumns) { String name = columnValuePair.column(); - for (ForeignKey.Link link : idColumnLinks) { + for (Link link : idColumnLinks) { if (link.columnInReferringTable().equals(columnValuePair.column())) { name = link.columnInReferencedTable(); break; @@ -709,7 +710,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC sqlBuilder.append(" WHEN MATCHED THEN UPDATE SET \"").append(column).append("\" = source.\"").append(column).append("\" WHEN NOT MATCHED THEN INSERT (\"").append(column).append("\""); for (ColumnValuePair columnValuePair : idColumns) { String name = columnValuePair.column(); - for (ForeignKey.Link link : idColumnLinks) { + for (Link link : idColumnLinks) { if (link.columnInReferringTable().equals(columnValuePair.column())) { name = link.columnInReferencedTable(); break; @@ -720,7 +721,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC sqlBuilder.append(") VALUES (source.\"").append(column).append("\""); for (ColumnValuePair columnValuePair : idColumns) { String name = columnValuePair.column(); - for (ForeignKey.Link link : idColumnLinks) { + for (Link link : idColumnLinks) { if (link.columnInReferringTable().equals(columnValuePair.column())) { name = link.columnInReferencedTable(); break; @@ -829,4 +830,25 @@ public Class getSerializedType(Class clazz) { ValueSerializer serializer = getValueSerializer(clazz); return serializer.getSerializedType(); } + +// /** +// * For internal use only. A dummy instance has no DataManager, no id columns, and is marked as deleted. +// * +// * @param clazz The UniqueData class to create a dummy instance of. +// * @param The type of UniqueData. +// */ +// @ApiStatus.Internal +// public T createDummyInstance(Class clazz) { +// T instance; +// try { +// Constructor constructor = clazz.getDeclaredConstructor(); +// constructor.setAccessible(true); +// instance = constructor.newInstance(); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// +// instance.markDeleted(); +// return instance; +// } } diff --git a/core/src/main/java/net/staticstudios/data/UniqueData.java b/core/src/main/java/net/staticstudios/data/UniqueData.java index 6f6171e0..edb86bdb 100644 --- a/core/src/main/java/net/staticstudios/data/UniqueData.java +++ b/core/src/main/java/net/staticstudios/data/UniqueData.java @@ -68,12 +68,15 @@ public synchronized void delete() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append(this.getClass().getSimpleName()).append("{"); - for (ColumnValuePair idColumn : idColumns) { - sb.append(idColumn.column()).append("=").append(idColumn.value()).append(", "); + if (this.idColumns != null) { + for (ColumnValuePair idColumn : idColumns) { + sb.append(idColumn.column()).append("=").append(idColumn.value()).append(", "); + } } - if (!idColumns.isEmpty()) { - sb.setLength(sb.length() - 2); + if (isDeleted) { + sb.append("deleted=true, "); } + sb.append("dataManager=").append(dataManager); sb.append("}"); return sb.toString(); } 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 4b30956d..d5132e6f 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 @@ -2,9 +2,9 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.*; -import net.staticstudios.data.parse.ForeignKey; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; +import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,8 +21,8 @@ public class PersistentManyToManyCollectionImpl implements private final String parsedJoinTableSchema; private final String parsedJoinTableName; private final String links; // since we need information about the column prefixes in the join table, we have to compute these at runtime - private @Nullable List cachedJoinTableToDataTableLinks = null; - private @Nullable List cachedJoinTableToReferencedTableLinks = null; + private @Nullable List cachedJoinTableToDataTableLinks = null; + private @Nullable List cachedJoinTableToReferencedTableLinks = null; public PersistentManyToManyCollectionImpl(UniqueData holder, Class type, String parsedJoinTableSchema, String parsedJoinTableName, String links) { this.holder = holder; @@ -106,26 +106,26 @@ public static String getReferencedTableColumnPrefix(String dataTable, String ref return ""; } - public static List getJoinTableToDataTableLinks(String dataTable, String links) { - List joinTableToDataTableLinks = new ArrayList<>(); + public static List getJoinTableToDataTableLinks(String dataTable, String links) { + List joinTableToDataTableLinks = new ArrayList<>(); String dataTableColumnPrefix = getDataTableColumnPrefix(dataTable); - for (ForeignKey.Link link : SQLBuilder.parseLinks(links)) { + for (Link link : SQLBuilder.parseLinks(links)) { String columnInDataTable = link.columnInReferringTable(); String dataColumnInJoinTable = dataTableColumnPrefix + "_" + columnInDataTable; - joinTableToDataTableLinks.add(new ForeignKey.Link(columnInDataTable, dataColumnInJoinTable)); + joinTableToDataTableLinks.add(new Link(columnInDataTable, dataColumnInJoinTable)); } return joinTableToDataTableLinks; } - public static List getJoinTableToReferencedTableLinks(String dataTable, String referencedTable, String links) { - List joinTableToReferencedTableLinks = new ArrayList<>(); + public static List getJoinTableToReferencedTableLinks(String dataTable, String referencedTable, String links) { + List joinTableToReferencedTableLinks = new ArrayList<>(); String referencedTableColumnPrefix = getReferencedTableColumnPrefix(dataTable, referencedTable); - for (ForeignKey.Link link : SQLBuilder.parseLinks(links)) { + for (Link link : SQLBuilder.parseLinks(links)) { String columnInReferencedTable = link.columnInReferencedTable(); String referencedColumnInJoinTable = referencedTableColumnPrefix + "_" + columnInReferencedTable; - joinTableToReferencedTableLinks.add(new ForeignKey.Link(columnInReferencedTable, referencedColumnInJoinTable)); + joinTableToReferencedTableLinks.add(new Link(columnInReferencedTable, referencedColumnInJoinTable)); } return joinTableToReferencedTableLinks; } @@ -241,8 +241,8 @@ public boolean addAll(@NotNull Collection c) { SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); SQLTransaction.Statement selectReferencedIdsStatement = buildSelectReferencedIdsStatement(); SQLTransaction.Statement updateStatement = buildUpdateStatement(); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); List holderLinkingValues = new ArrayList<>(joinTableToDataTableLinks.size()); List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); @@ -251,7 +251,7 @@ public boolean addAll(@NotNull Collection c) { transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { try { Preconditions.checkState(rs.next(), "Could not find holder row in database"); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String dataColumn = entry.columnInReferencedTable(); Object value = rs.getObject(dataColumn); holderLinkingValues.add(value); @@ -268,7 +268,7 @@ public boolean addAll(@NotNull Collection c) { transaction.query(selectReferencedIdsStatement, () -> referencedIdValues, rs -> { try { Preconditions.checkState(rs.next(), "Could not find referenced row in database"); - for (ForeignKey.Link _entry : joinTableToReferencedTableLinks) { + for (Link _entry : joinTableToReferencedTableLinks) { String referencedColumn = _entry.columnInReferencedTable(); Object value = rs.getObject(referencedColumn); referencedLinkingValues.add(value); @@ -349,7 +349,7 @@ public void clear() { DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); SQLTransaction.Statement clearStatement = buildClearStatement(); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); List holderLinkingValues = new ArrayList<>(joinTableToDataTableLinks.size()); List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); @@ -358,7 +358,7 @@ public void clear() { transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { try { Preconditions.checkState(rs.next(), "Could not find holder row in database"); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String dataColumn = entry.columnInReferencedTable(); Object value = rs.getObject(dataColumn); holderLinkingValues.add(value); @@ -387,8 +387,8 @@ public boolean removeIds(List idsToRemove) { SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); SQLTransaction.Statement selectReferencedIdsStatement = buildSelectReferencedIdsStatement(); SQLTransaction.Statement removeStatement = buildRemoveStatement(); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); List holderLinkingValues = new ArrayList<>(joinTableToDataTableLinks.size()); List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); @@ -397,7 +397,7 @@ public boolean removeIds(List idsToRemove) { transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { try { Preconditions.checkState(rs.next(), "Could not find holder row in database"); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String dataColumn = entry.columnInReferencedTable(); Object value = rs.getObject(dataColumn); holderLinkingValues.add(value); @@ -414,7 +414,7 @@ public boolean removeIds(List idsToRemove) { transaction.query(selectReferencedIdsStatement, () -> referencedIdValues, rs -> { try { Preconditions.checkState(rs.next(), "Could not find referenced row in database"); - for (ForeignKey.Link _entry : joinTableToReferencedTableLinks) { + for (Link _entry : joinTableToReferencedTableLinks) { String referencedColumn = _entry.columnInReferencedTable(); Object value = rs.getObject(referencedColumn); referencedLinkingValues.add(value); @@ -456,8 +456,8 @@ private Set getIds() { String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), target.table()); - List joinTableToDataTableLinks = getJoinTableToDataTableLinks(holderMetadata.table(), links); - List joinTableToReferencedTableLinks = getJoinTableToReferencedTableLinks(holderMetadata.table(), target.table(), links); + List joinTableToDataTableLinks = getJoinTableToDataTableLinks(holderMetadata.table(), links); + List joinTableToReferencedTableLinks = getJoinTableToReferencedTableLinks(holderMetadata.table(), target.table(), links); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); @@ -467,14 +467,14 @@ private Set getIds() { 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 "); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String joinColumn = entry.columnInReferringTable(); String dataColumn = entry.columnInReferencedTable(); sqlBuilder.append("_data.\"").append(dataColumn).append("\" = \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\".\"").append(joinColumn).append("\" AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); sqlBuilder.append(" INNER JOIN \"").append(target.schema()).append("\".\"").append(target.table()).append("\" _target ON "); - for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + for (Link entry : joinTableToReferencedTableLinks) { String joinColumn = entry.columnInReferringTable(); String referencedColumn = entry.columnInReferencedTable(); sqlBuilder.append("_target.\"").append(referencedColumn).append("\" = \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\".\"").append(joinColumn).append("\" AND "); @@ -509,11 +509,11 @@ private Set getIds() { private SQLTransaction.Statement buildSelectDataIdsStatement() { UniqueDataMetadata holderMetadata = holder.getMetadata(); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String dataColumn = entry.columnInReferencedTable(); sqlBuilder.append("\"").append(dataColumn).append("\", "); } @@ -529,11 +529,11 @@ private SQLTransaction.Statement buildSelectDataIdsStatement() { private SQLTransaction.Statement buildSelectReferencedIdsStatement() { UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); - for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + for (Link entry : joinTableToReferencedTableLinks) { String referencedColumn = entry.columnInReferencedTable(); sqlBuilder.append("\"").append(referencedColumn).append("\", "); } @@ -551,8 +551,8 @@ private SQLTransaction.Statement buildSelectReferencedIdsStatement() { private SQLTransaction.Statement buildUpdateStatement() { UniqueDataMetadata holderMetadata = holder.getMetadata(); UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()); @@ -561,41 +561,41 @@ private SQLTransaction.Statement buildUpdateStatement() { sqlBuilder.append("?, ".repeat(Math.max(0, joinTableToDataTableLinks.size() + joinTableToReferencedTableLinks.size()))); sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(")) AS _source ("); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(joinColumn).append("\", "); } - for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + for (Link entry : joinTableToReferencedTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(joinColumn).append("\", "); } sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(") ON "); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("_target.\"").append(joinColumn).append("\" = _source.\"").append(joinColumn).append("\" AND "); } - for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + for (Link entry : joinTableToReferencedTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("_target.\"").append(joinColumn).append("\" = _source.\"").append(joinColumn).append("\" AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); sqlBuilder.append(" WHEN NOT MATCHED THEN INSERT ("); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(joinColumn).append("\", "); } - for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + for (Link entry : joinTableToReferencedTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(joinColumn).append("\", "); } sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(") VALUES ("); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("_source.\"").append(joinColumn).append("\", "); } - for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + for (Link entry : joinTableToReferencedTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("_source.\"").append(joinColumn).append("\", "); } @@ -605,11 +605,11 @@ private SQLTransaction.Statement buildUpdateStatement() { sqlBuilder.setLength(0); sqlBuilder.append("INSERT INTO \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" ("); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(joinColumn).append("\", "); } - for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + for (Link entry : joinTableToReferencedTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(joinColumn).append("\", "); } @@ -626,18 +626,18 @@ private SQLTransaction.Statement buildUpdateStatement() { private SQLTransaction.Statement buildRemoveStatement() { UniqueDataMetadata holderMetadata = holder.getMetadata(); UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("DELETE FROM \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" WHERE "); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(joinColumn).append("\" = ? AND "); } - for (ForeignKey.Link entry : joinTableToReferencedTableLinks) { + for (Link entry : joinTableToReferencedTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(joinColumn).append("\" = ? AND "); } @@ -649,13 +649,13 @@ private SQLTransaction.Statement buildRemoveStatement() { private SQLTransaction.Statement buildClearStatement() { UniqueDataMetadata holderMetadata = holder.getMetadata(); UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("DELETE FROM \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" WHERE "); - for (ForeignKey.Link entry : joinTableToDataTableLinks) { + for (Link entry : joinTableToDataTableLinks) { String joinColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(joinColumn).append("\" = ? AND "); } @@ -664,7 +664,7 @@ private SQLTransaction.Statement buildClearStatement() { return SQLTransaction.Statement.of(sql, sql); } - private List getCachedJoinTableToDataTableLinks() { + private List getCachedJoinTableToDataTableLinks() { if (cachedJoinTableToDataTableLinks == null) { UniqueDataMetadata holderMetadata = holder.getMetadata(); cachedJoinTableToDataTableLinks = getJoinTableToDataTableLinks(holderMetadata.table(), links); @@ -672,7 +672,7 @@ private List getCachedJoinTableToDataTableLinks() { return cachedJoinTableToDataTableLinks; } - private List getCachedJoinTableToReferencedTableLinks() { + private List getCachedJoinTableToReferencedTableLinks() { if (cachedJoinTableToReferencedTableLinks == null) { UniqueDataMetadata holderMetadata = holder.getMetadata(); UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); 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 a102d029..c469e1d9 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 @@ -2,9 +2,9 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.*; -import net.staticstudios.data.parse.ForeignKey; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; +import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -18,15 +18,15 @@ public class PersistentOneToManyCollectionImpl implements PersistentCollection { private final UniqueData holder; private final Class type; - private final List link; + private final List link; - public PersistentOneToManyCollectionImpl(UniqueData holder, Class type, List link) { + public PersistentOneToManyCollectionImpl(UniqueData holder, Class type, List link) { this.holder = holder; this.type = type; this.link = link; } - public static void createAndDelegate(PersistentCollection.ProxyPersistentCollection proxy, List link) { + public static void createAndDelegate(PersistentCollection.ProxyPersistentCollection proxy, List link) { PersistentOneToManyCollectionImpl delegate = new PersistentOneToManyCollectionImpl<>( proxy.getHolder(), proxy.getReferenceType(), @@ -35,7 +35,7 @@ public static void createAndDelegate(PersistentCollection proxy.setDelegate(delegate); } - public static PersistentOneToManyCollectionImpl create(UniqueData holder, Class type, List link) { + public static PersistentOneToManyCollectionImpl create(UniqueData holder, Class type, List link) { return new PersistentOneToManyCollectionImpl<>(holder, type, link); } @@ -192,7 +192,7 @@ public boolean addAll(@NotNull Collection c) { transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { try { Preconditions.checkState(rs.next(), "Could not find holder row in database"); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String dataColumn = entry.columnInReferringTable(); Object value = rs.getObject(dataColumn); holderLinkingValues.add(value); @@ -287,7 +287,7 @@ public void clear() { transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { try { Preconditions.checkState(rs.next(), "Could not find holder row in database"); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String dataColumn = entry.columnInReferringTable(); Object value = rs.getObject(dataColumn); holderLinkingValues.add(value); @@ -324,7 +324,7 @@ private void removeIds(List ids) { transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { try { Preconditions.checkState(rs.next(), "Could not find holder row in database"); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String dataColumn = entry.columnInReferringTable(); Object value = rs.getObject(dataColumn); holderLinkingValues.add(value); @@ -358,7 +358,7 @@ private SQLTransaction.Statement buildSelectDataIdsStatement() { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String dataColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(dataColumn).append("\", "); } @@ -377,7 +377,7 @@ private SQLTransaction.Statement buildUpdateStatement() { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String theirColumn = entry.columnInReferencedTable(); sqlBuilder.append("\"").append(theirColumn).append("\" = ?, "); } @@ -396,13 +396,13 @@ private SQLTransaction.Statement buildClearStatement() { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String theirColumn = entry.columnInReferencedTable(); sqlBuilder.append("\"").append(theirColumn).append("\" = NULL, "); } sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(" WHERE "); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String theirColumn = entry.columnInReferencedTable(); sqlBuilder.append("\"").append(theirColumn).append("\" = ? AND "); } @@ -429,7 +429,7 @@ private Set getIds() { sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(" FROM \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" "); sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" AS _source ON "); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String myColumn = entry.columnInReferringTable(); String theirColumn = entry.columnInReferencedTable(); sqlBuilder.append("\"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\".\"").append(theirColumn).append("\" = _source.\"").append(myColumn).append("\" AND "); @@ -437,7 +437,7 @@ private Set getIds() { sqlBuilder.setLength(sqlBuilder.length() - 5); sqlBuilder.append(" WHERE "); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String theirColumn = entry.columnInReferencedTable(); sqlBuilder.append("\"").append(theirColumn).append("\" = _source.\"").append(entry.columnInReferringTable()).append("\" AND "); } 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 75ad3c9a..9c627bd3 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 @@ -2,8 +2,9 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.*; -import net.staticstudios.data.parse.ForeignKey; import net.staticstudios.data.util.*; +import net.staticstudios.data.utils.Link; +import net.staticstudios.data.utils.StringUtils; import org.jetbrains.annotations.Nullable; import java.lang.reflect.Field; @@ -103,12 +104,12 @@ public static PersistentValueMetadata extractMetadata(Str foreignColumn.index(), defaultValue ); - List idColumnLinks = new LinkedList<>(); + List idColumnLinks = new LinkedList<>(); List links = StringUtils.parseCommaSeperatedList(foreignColumn.link()); for (String link : links) { String[] parts = link.split("="); Preconditions.checkArgument(parts.length == 2, "ForeignColumn link must be in the format localColumn=foreignColumn, got: %s", link); - idColumnLinks.add(new ForeignKey.Link(ValueUtils.parseValue(parts[1]), ValueUtils.parseValue(parts[0]))); + idColumnLinks.add(new Link(ValueUtils.parseValue(parts[1]), ValueUtils.parseValue(parts[0]))); } return new ForeignPersistentValueMetadata(clazz, columnMetadata, updateInterval, idColumnLinks); } @@ -143,7 +144,7 @@ public void set(T value) { holder.getDataManager().set(metadata.getSchema(), metadata.getTable(), metadata.getColumn(), holder.getIdColumns(), getIdColumnLinks(), value, metadata.getUpdateInterval()); } - private List getIdColumnLinks() { + private List getIdColumnLinks() { if (metadata instanceof ForeignPersistentValueMetadata foreignMetadata) { return foreignMetadata.getLinks(); } 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 41918b8b..e5725396 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 @@ -5,9 +5,9 @@ import net.staticstudios.data.OneToOne; import net.staticstudios.data.Reference; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.parse.ForeignKey; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; +import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; @@ -22,15 +22,15 @@ public class ReferenceImpl implements Reference { private final UniqueData holder; private final Class type; - private final List link; + private final List link; - public ReferenceImpl(UniqueData holder, Class type, List link) { + public ReferenceImpl(UniqueData holder, Class type, List link) { this.holder = holder; this.type = type; this.link = link; } - public static void createAndDelegate(Reference.ProxyReference proxy, List link) { + public static void createAndDelegate(Reference.ProxyReference proxy, List link) { ReferenceImpl delegate = new ReferenceImpl<>( proxy.getHolder(), proxy.getReferenceType(), @@ -39,7 +39,7 @@ public static void createAndDelegate(Reference.ProxyRefer proxy.setDelegate(delegate); } - public static ReferenceImpl create(UniqueData holder, Class type, List link) { + public static ReferenceImpl create(UniqueData holder, Class type, List link) { return new ReferenceImpl<>(holder, type, link); } @@ -93,7 +93,7 @@ public Class getReferenceType() { DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String myColumn = entry.columnInReferringTable(); sqlBuilder.append("\"").append(myColumn).append("\", "); } @@ -111,7 +111,7 @@ public Class getReferenceType() { return null; } - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String myColumn = entry.columnInReferringTable(); String theirColumn = entry.columnInReferencedTable(); if (rs.getObject(myColumn) == null) { @@ -133,7 +133,7 @@ public void set(@Nullable T value) { List values = new ArrayList<>(); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("UPDATE \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" SET "); - for (ForeignKey.Link entry : link) { + for (Link entry : link) { String myColumn = entry.columnInReferringTable(); if (value == null) { sqlBuilder.append("\"").append(myColumn).append("\" = NULL, "); diff --git a/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java index ced12049..98b22fd6 100644 --- a/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java +++ b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java @@ -1,6 +1,6 @@ package net.staticstudios.data.impl.h2.trigger; -import net.staticstudios.data.parse.ForeignKey; +import net.staticstudios.data.utils.Link; import org.h2.api.Trigger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,7 +15,7 @@ public class H2DeleteStrategyCascadeTrigger implements Trigger { private final Logger logger = LoggerFactory.getLogger(H2DeleteStrategyCascadeTrigger.class); private final List columnNames = new ArrayList<>(); - private List links; + private List links; private String parentSchema; private String parentTable; private String targetSchema; @@ -56,7 +56,7 @@ public void init(Connection conn, String schemaName, String triggerName, String } this.links = new ArrayList<>(); for (int i = 0; i < links.size(); i += 2) { - this.links.add(new ForeignKey.Link(links.get(i + 1), links.get(i))); + this.links.add(new Link(links.get(i + 1), links.get(i))); } } @@ -87,7 +87,7 @@ public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws private void handleDelete(Connection connection, Object[] oldRow) throws SQLException { StringBuilder sb = new StringBuilder("DELETE FROM \"").append(targetSchema).append("\".\"").append(targetTable).append("\" WHERE "); List values = new ArrayList<>(); - for (ForeignKey.Link link : links) { + for (Link link : links) { sb.append("\"").append(link.columnInReferencedTable()).append("\" = ? AND "); int index = columnNames.indexOf(link.columnInReferringTable()); values.add(oldRow[index]); diff --git a/core/src/main/java/net/staticstudios/data/insert/InsertContext.java b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java index 0a1e1723..29bdb402 100644 --- a/core/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -104,4 +104,4 @@ public T get(Class holderClass) { } return dataManager.getInstance(holderClass, idColumnValues); } -} +} \ No newline at end of file diff --git a/core/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/core/src/main/java/net/staticstudios/data/parse/ForeignKey.java index eea2b51a..1fdcfe41 100644 --- a/core/src/main/java/net/staticstudios/data/parse/ForeignKey.java +++ b/core/src/main/java/net/staticstudios/data/parse/ForeignKey.java @@ -2,6 +2,7 @@ import net.staticstudios.data.util.OnDelete; import net.staticstudios.data.util.OnUpdate; +import net.staticstudios.data.utils.Link; import java.util.Collections; import java.util.LinkedHashSet; @@ -61,10 +62,10 @@ public OnUpdate getOnUpdate() { public String getName() { return "fk_" // + referringSchema + "_" + referringTable + "_" - + String.join("_", links.stream().map(ForeignKey.Link::columnInReferringTable).toList()) + + String.join("_", links.stream().map(Link::columnInReferringTable).toList()) + "_to_" // + referencedSchema + "_" + referencedTable + "_" - + String.join("_", links.stream().map(ForeignKey.Link::columnInReferencedTable).toList()); + + String.join("_", links.stream().map(Link::columnInReferencedTable).toList()); } @Override @@ -98,6 +99,4 @@ public String toString() { '}'; } - public record Link(String columnInReferencedTable, String columnInReferringTable) { - } } 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 1083db9d..f9984dde 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,8 @@ import net.staticstudios.data.*; import net.staticstudios.data.impl.data.PersistentManyToManyCollectionImpl; import net.staticstudios.data.util.*; +import net.staticstudios.data.utils.Link; +import net.staticstudios.data.utils.StringUtils; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -28,33 +30,29 @@ public SQLBuilder(DataManager dataManager) { } public static void parseLinks(ForeignKey foreignKey, String links) { - for (ForeignKey.Link link : parseLinks(links)) { + for (Link link : parseLinks(links)) { foreignKey.addLink(link); } } public static void parseLinksReversed(ForeignKey foreignKey, String links) { - for (ForeignKey.Link link : parseLinksReversed(links)) { + for (Link link : parseLinksReversed(links)) { foreignKey.addLink(link); } } - public static List parseLinksReversed(String links) { - List mappings = new ArrayList<>(); - for (String link : StringUtils.parseCommaSeperatedList(links)) { - String[] parts = link.split("="); - Preconditions.checkArgument(parts.length == 2, "Invalid link format! Expected format: localColumn=foreignColumn, got: " + link); - mappings.add(new ForeignKey.Link(ValueUtils.parseValue(parts[0].trim()), ValueUtils.parseValue(parts[1].trim()))); + public static List parseLinksReversed(String links) { + List mappings = new ArrayList<>(); + for (Link rawLink : Link.parseRawLinksReversed(links)) { + mappings.add(new Link(ValueUtils.parseValue(rawLink.columnInReferencedTable()), ValueUtils.parseValue(rawLink.columnInReferringTable()))); } return mappings; } - public static List parseLinks(String links) { - List mappings = new ArrayList<>(); - for (String link : StringUtils.parseCommaSeperatedList(links)) { - String[] parts = link.split("="); - Preconditions.checkArgument(parts.length == 2, "Invalid link format! Expected format: localColumn=foreignColumn, got: " + link); - mappings.add(new ForeignKey.Link(ValueUtils.parseValue(parts[1].trim()), ValueUtils.parseValue(parts[0].trim()))); + public static List parseLinks(String links) { + List mappings = new ArrayList<>(); + for (Link rawLink : Link.parseRawLinks(links)) { + mappings.add(new Link(ValueUtils.parseValue(rawLink.columnInReferencedTable()), ValueUtils.parseValue(rawLink.columnInReferringTable()))); } return mappings; } @@ -183,13 +181,13 @@ private List getDefs(Collection schemas) { sb.append("ALTER TABLE \"").append(foreignKey.getReferringSchema()).append("\".\"").append(foreignKey.getReferringTable()).append("\" "); sb.append("ADD CONSTRAINT IF NOT EXISTS ").append(fKeyName).append(" "); sb.append("FOREIGN KEY ("); - for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { + for (Link link : foreignKey.getLinkingColumns()) { sb.append("\"").append(link.columnInReferringTable()).append("\", "); } sb.setLength(sb.length() - 2); sb.append(") "); sb.append("REFERENCES \"").append(foreignKey.getReferencedSchema()).append("\".\"").append(foreignKey.getReferencedTable()).append("\" ("); - for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { + for (Link link : foreignKey.getLinkingColumns()) { sb.append("\"").append(link.columnInReferencedTable()).append("\", "); } sb.setLength(sb.length() - 2); @@ -204,13 +202,13 @@ private List getDefs(Collection schemas) { sb.append("ALTER TABLE \"").append(foreignKey.getReferringSchema()).append("\".\"").append(foreignKey.getReferringTable()).append("\" "); sb.append("ADD CONSTRAINT ").append(fKeyName).append(" "); sb.append("FOREIGN KEY ("); - for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { + for (Link link : foreignKey.getLinkingColumns()) { sb.append("\"").append(link.columnInReferringTable()).append("\", "); } sb.setLength(sb.length() - 2); sb.append(") "); sb.append("REFERENCES \"").append(foreignKey.getReferencedSchema()).append("\".\"").append(foreignKey.getReferencedTable()).append("\" ("); - for (ForeignKey.Link link : foreignKey.getLinkingColumns()) { + for (Link link : foreignKey.getLinkingColumns()) { sb.append("\"").append(link.columnInReferencedTable()).append("\", "); } sb.setLength(sb.length() - 2); @@ -387,8 +385,8 @@ private void parseColumn(Class clazz, Map links = parseLinks(foreignColumn.link()); - for (ForeignKey.Link link : links) { + List links = parseLinks(foreignColumn.link()); + for (Link link : links) { ColumnMetadata found = null; for (ColumnMetadata idCol : metadata.idColumns()) { if (idCol.name().equals(link.columnInReferringTable())) { @@ -566,8 +564,8 @@ private void parseManyToManyPersistentCollection(ManyToMany manyToMany, Class joinTableToDataTableLinks; - List joinTableToReferencedTableLinks; + List joinTableToDataTableLinks; + List joinTableToReferencedTableLinks; try { joinTableToDataTableLinks = PersistentManyToManyCollectionImpl.getJoinTableToDataTableLinks(dataTable, manyToMany.link()); @@ -583,14 +581,14 @@ private void parseManyToManyPersistentCollection(ManyToMany manyToMany, Class joinTableIdColumns = new ArrayList<>(); - for (ForeignKey.Link dataLink : joinTableToDataTableLinks) { + for (Link dataLink : joinTableToDataTableLinks) { ColumnMetadata columnMetadata = metadata.idColumns().stream() .filter(c -> c.name().equals(dataLink.columnInReferencedTable())) .findFirst() .orElseThrow(() -> new IllegalArgumentException("Column not found in data table! " + dataLink.columnInReferringTable())); joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, dataTableColumnPrefix + "_" + columnMetadata.name(), columnMetadata.type(), false, false, "")); } - for (ForeignKey.Link referencedLink : joinTableToReferencedTableLinks) { + for (Link referencedLink : joinTableToReferencedTableLinks) { ColumnMetadata columnMetadata = referencedMetadata.idColumns().stream() .filter(c -> c.name().equals(referencedLink.columnInReferencedTable())) .findFirst() diff --git a/core/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java b/core/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java index b56fb1f2..472623ce 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java @@ -2,6 +2,7 @@ import net.staticstudios.data.DeleteStrategy; import net.staticstudios.data.impl.h2.trigger.H2DeleteStrategyCascadeTrigger; +import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import java.util.Set; @@ -12,9 +13,9 @@ public class SQLDeleteStrategyTrigger implements SQLTrigger { private final String targetSchema; private final String targetTable; private final DeleteStrategy deleteStrategy; - private final Set links; + private final Set links; - public SQLDeleteStrategyTrigger(String parentSchema, String parentTable, String targetSchema, String targetTable, DeleteStrategy deleteStrategy, Set links) { + public SQLDeleteStrategyTrigger(String parentSchema, String parentTable, String targetSchema, String targetTable, DeleteStrategy deleteStrategy, Set links) { this.parentSchema = parentSchema; this.parentTable = parentTable; this.targetSchema = targetSchema; diff --git a/core/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java b/core/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java index a08048cb..55f486fd 100644 --- a/core/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java @@ -1,20 +1,20 @@ package net.staticstudios.data.util; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.parse.ForeignKey; +import net.staticstudios.data.utils.Link; import java.util.List; import java.util.Objects; public class ForeignPersistentValueMetadata extends PersistentValueMetadata { - private final List links; + private final List links; - public ForeignPersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata, int updateInterval, List links) { + public ForeignPersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata, int updateInterval, List links) { super(holderClass, columnMetadata, updateInterval); this.links = links; } - public List getLinks() { + public List getLinks() { return links; } 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 e7c19f19..b87c3cee 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java @@ -1,16 +1,16 @@ package net.staticstudios.data.util; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.parse.ForeignKey; +import net.staticstudios.data.utils.Link; import java.util.List; import java.util.Objects; public class PersistentOneToManyCollectionMetadata implements PersistentCollectionMetadata { private final Class dataType; - private final List links; + private final List links; - public PersistentOneToManyCollectionMetadata(Class dataType, List links) { + public PersistentOneToManyCollectionMetadata(Class dataType, List links) { this.dataType = dataType; this.links = links; } @@ -19,7 +19,7 @@ public Class getDataType() { return dataType; } - public List getLinks() { + public List getLinks() { return links; } 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 2863ff0b..cef210c7 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java @@ -1,16 +1,16 @@ package net.staticstudios.data.util; import net.staticstudios.data.UniqueData; -import net.staticstudios.data.parse.ForeignKey; +import net.staticstudios.data.utils.Link; import java.util.List; import java.util.Objects; public class ReferenceMetadata { private final Class referencedClass; - private final List links; + private final List links; - public ReferenceMetadata(Class referencedClass, List links) { + public ReferenceMetadata(Class referencedClass, List links) { this.referencedClass = referencedClass; this.links = links; } @@ -19,7 +19,7 @@ public Class getReferencedClass() { return referencedClass; } - public List getLinks() { + public List getLinks() { return links; } diff --git a/core/src/main/java/net/staticstudios/data/util/StringUtils.java b/core/src/main/java/net/staticstudios/data/util/StringUtils.java deleted file mode 100644 index d10dbaf3..00000000 --- a/core/src/main/java/net/staticstudios/data/util/StringUtils.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.staticstudios.data.util; - -import java.util.List; - -public class StringUtils { - public static List parseCommaSeperatedList(String input) { - return List.of(input.split(",")); - } -} diff --git a/core/src/main/java/net/staticstudios/data/util/ValueUtils.java b/core/src/main/java/net/staticstudios/data/util/ValueUtils.java index 93af28f3..c9751234 100644 --- a/core/src/main/java/net/staticstudios/data/util/ValueUtils.java +++ b/core/src/main/java/net/staticstudios/data/util/ValueUtils.java @@ -3,8 +3,6 @@ import com.google.common.base.Preconditions; import org.jetbrains.annotations.VisibleForTesting; -import java.util.ArrayList; -import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,13 +24,4 @@ public static String parseValue(String encoded) { matcher.appendTail(sb); return sb.toString(); } - - public static List parseCommaSeperatedList(String encoded) { - List strings = new ArrayList<>(); - for (String s : StringUtils.parseCommaSeperatedList(encoded)) { - strings.add(parseValue(s)); - } - - return strings; - } } diff --git a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java index ec56fda2..b84e2375 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -3,7 +3,6 @@ import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.misc.MockEnvironment; import net.staticstudios.data.mock.user.MockUser; -import net.staticstudios.data.mock.user.MockUserFactory; import net.staticstudios.data.util.ColumnValuePair; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -30,7 +29,7 @@ public void testReadData() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); for (UUID id : userIds) { - MockUserFactory.builder(dataManager) + MockUser.builder(dataManager) .id(id) .name("user " + id) .insert(InsertMode.SYNC); @@ -43,7 +42,6 @@ public void testReadData() throws SQLException { } waitForDataPropagation(); - MockEnvironment environment2 = createMockEnvironment(); DataManager dataManager2 = environment2.dataManager(); dataManager2.load(MockUser.class); @@ -55,11 +53,11 @@ public void testReadData() throws SQLException { } @Test - public void test() throws SQLException { + public void test() throws SQLException { //todo: this test throws an exception from pg DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - MockUser mockUser = MockUserFactory.builder(dataManager) + MockUser mockUser = MockUser.builder(dataManager) .id(id) .name("test user") .nameUpdates(0) @@ -108,7 +106,7 @@ public void testUpdateHandlerRegistration() { dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); assertEquals(0, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); - MockUser mockUser = MockUserFactory.builder(dataManager) + MockUser mockUser = MockUser.builder(dataManager) .id(id) .name("test user") .favoriteColor("orange") @@ -144,7 +142,7 @@ public void testReceiveUpdateFromPostgres() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - MockUser mockUser = MockUserFactory.builder(dataManager) + MockUser mockUser = MockUser.builder(dataManager) .id(id) .name("test user") .favoriteColor("orange") @@ -203,7 +201,7 @@ public void testReceiveDeleteFromPostgres() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - MockUser mockUser = MockUserFactory.builder(dataManager) + MockUser mockUser = MockUser.builder(dataManager) .id(id) .name("test user") .favoriteColor("orange") @@ -235,7 +233,7 @@ public void testChangeIdColumn() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - MockUser mockUser = MockUserFactory.builder(dataManager) + MockUser mockUser = MockUser.builder(dataManager) .id(id) .name("test user") .favoriteColor("orange") @@ -266,7 +264,7 @@ public void testChangeIdColumnInPostgres() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - MockUser mockUser = MockUserFactory.builder(dataManager) + MockUser mockUser = MockUser.builder(dataManager) .id(id) .name("test user") .favoriteColor("orange") @@ -308,7 +306,7 @@ public void testUpdateInterval() throws Exception { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - MockUser mockUser = MockUserFactory.builder(dataManager) + MockUser mockUser = MockUser.builder(dataManager) .id(id) .name("test user") .favoriteColor("orange") @@ -352,13 +350,13 @@ public void testInsertStrategyPreferExisting() throws SQLException { preparedStatement.executeUpdate(); } - MockUser user1 = MockUserFactory.builder(dataManager) + MockUser user1 = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("test user") .favoriteColor("red") .insert(InsertMode.SYNC); assertEquals("red", user1.favoriteColor.get()); - MockUser user2 = MockUserFactory.builder(dataManager) + MockUser user2 = MockUser.builder(dataManager) .id(id) .name("test user2") .favoriteColor("green") @@ -386,13 +384,13 @@ public void testInsertStrategyOverwriteExisting() throws SQLException { preparedStatement.setInt(2, 5); preparedStatement.executeUpdate(); } - MockUser user1 = MockUserFactory.builder(dataManager) + MockUser user1 = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("test user") .nameUpdates(10) .insert(InsertMode.SYNC); assertEquals(10, user1.nameUpdates.get()); - MockUser user2 = MockUserFactory.builder(dataManager) + MockUser user2 = MockUser.builder(dataManager) .id(id) .name("test user2") .nameUpdates(15) @@ -416,7 +414,7 @@ public void testDeleteStrategyCascade() throws SQLException { Connection h2Connection = getH2Connection(dataManager); Connection pgConnection = getConnection(); UUID id = UUID.randomUUID(); - MockUser user = MockUserFactory.builder(dataManager) + MockUser user = MockUser.builder(dataManager) .id(id) .name("test user") .favoriteColor("red") @@ -464,7 +462,7 @@ public void testDeleteStrategyNoAction() throws SQLException { Connection h2Connection = getH2Connection(dataManager); Connection pgConnection = getConnection(); UUID id = UUID.randomUUID(); - MockUser user = MockUserFactory.builder(dataManager) + MockUser user = MockUser.builder(dataManager) .id(id) .name("test user") .nameUpdates(10) diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index 4d10e541..5afe4bcc 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -8,6 +8,7 @@ // if the super class provides a data annotation, ignore it and use the child's annotation. it would be cool tho to allow the super class to use a @data annotation. the former is whats implemented now. if changed, update the processor. @Data(schema = "public", table = "users") public class MockUser extends UniqueData { + //todo: test inheritance properly //todo: cached values //todo: note - maybe PC's add and remove handlers can be implemented using update handlers @IdColumn(name = "id") diff --git a/intellij-plugin/build.gradle b/intellij-plugin/build.gradle index fd196e89..92790c1c 100644 --- a/intellij-plugin/build.gradle +++ b/intellij-plugin/build.gradle @@ -12,8 +12,9 @@ repositories { } dependencies { + implementation(project(":utils")) intellijPlatform { - intellijIdeaCommunity('2023.3') + intellijIdeaCommunity('2025.2') bundledPlugin("com.intellij.java") } } 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 1c7f02e9..828a4719 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 @@ -10,6 +10,7 @@ import com.intellij.psi.util.CachedValuesManager; import net.staticstudios.data.ide.intellij.query.QueryBuilderUtils; import net.staticstudios.data.ide.intellij.query.QueryClause; +import net.staticstudios.data.utils.Constants; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,11 +40,11 @@ public class DataPsiAugmentProvider extends PsiAugmentProvider { return Collections.emptyList(); } - if (!Utils.extendsClass(psiClass, Constants.UNIQUE_DATA_FQN)) { + if (!IntelliJPluginUtils.extendsClass(psiClass, Constants.UNIQUE_DATA_FQN)) { return Collections.emptyList(); } - if (!Utils.hasAnnotation(psiClass, Constants.DATA_ANNOTATION_FQN)) { + if (!IntelliJPluginUtils.hasAnnotation(psiClass, Constants.DATA_ANNOTATION_FQN)) { return Collections.emptyList(); } @@ -163,15 +164,18 @@ private SyntheticBuilderClass createBuilderBuilderClass(PsiClass parentClass) { .createType(builderClass, PsiSubstitutor.EMPTY); for (PsiField psiField : parentClass.getAllFields()) { PsiType type = psiField.getType(); - PsiType innerType = Utils.getGenericParameter((PsiClassType) type, parentClass.getManager()); - if (Utils.isValidPersistentValue(psiField)) { + if (!(type instanceof PsiClassType psiClassType)) { + continue; + } + PsiType innerType = IntelliJPluginUtils.getGenericParameter(psiClassType, parentClass.getManager()); + if (IntelliJPluginUtils.isValidPersistentValue(psiField)) { SyntheticMethod setterMethod = new SyntheticMethod(parentClass, builderClass, psiField.getName(), builderType); setterMethod.addParameter(psiField.getName(), innerType); setterMethod.addModifier(PsiModifier.PUBLIC); setterMethod.addModifier(PsiModifier.FINAL); builderClass.addMethod(setterMethod); - } else if (Utils.isValidReference(psiField)) { + } else if (IntelliJPluginUtils.isValidReference(psiField)) { SyntheticMethod setterMethod = new SyntheticMethod(parentClass, builderClass, psiField.getName(), builderType); setterMethod.addParameter(psiField.getName(), innerType); setterMethod.addModifier(PsiModifier.PUBLIC); @@ -182,6 +186,25 @@ private SyntheticBuilderClass createBuilderBuilderClass(PsiClass parentClass) { //todo: support CachedValues, similar to PVs } + PsiType parentType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createType(parentClass, PsiSubstitutor.EMPTY); + + SyntheticMethod insertModeMethod = new SyntheticMethod(parentClass, builderClass, "insert", parentType); + PsiType insertModeType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createTypeFromText(Constants.INSERT_MODE_FQN, parentClass); + insertModeMethod.addParameter("insertMode", insertModeType); + insertModeMethod.addModifier(PsiModifier.PUBLIC); + insertModeMethod.addModifier(PsiModifier.FINAL); + builderClass.addMethod(insertModeMethod); + + SyntheticMethod insertContextMethod = new SyntheticMethod(parentClass, builderClass, "insert", null); + PsiType insertContextType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createTypeFromText(Constants.INSERT_CONTEXT_FQN, parentClass); + insertContextMethod.addParameter("ctx", insertContextType); + insertContextMethod.addModifier(PsiModifier.PUBLIC); + insertContextMethod.addModifier(PsiModifier.FINAL); + builderClass.addMethod(insertContextMethod); + return builderClass; } @@ -221,7 +244,7 @@ private SyntheticBuilderClass createQueryBuilderClass(PsiClass parentClass) { .createType(listClass, substitutor); for (PsiField psiField : parentClass.getAllFields()) { - if (Utils.isValidPersistentValue(psiField)) { + if (IntelliJPluginUtils.isValidPersistentValue(psiField)) { SyntheticMethod orderByMethod = new SyntheticMethod(parentClass, queryClass, "orderBy" + StringUtil.capitalize(psiField.getName()), queryType); orderByMethod.addParameter("order", orderType); orderByMethod.addModifier(PsiModifier.PUBLIC); @@ -291,12 +314,15 @@ private SyntheticBuilderClass createQueryWhereBuilderClass(PsiClass parentClass) for (PsiField psiField : parentClass.getAllFields()) { PsiType type = psiField.getType(); boolean isValidReference = false; - if (!Utils.isValidPersistentValue(psiField) && !(isValidReference = Utils.isValidReference(psiField))) { + if (!IntelliJPluginUtils.isValidPersistentValue(psiField) && !(isValidReference = IntelliJPluginUtils.isValidReference(psiField))) { continue; //non-supported field type } - PsiType innerType = Utils.getGenericParameter((PsiClassType) type, parentClass.getManager()); + if (!(type instanceof PsiClassType psiClassType)) { + continue; + } + PsiType innerType = IntelliJPluginUtils.getGenericParameter(psiClassType, parentClass.getManager()); - List clauses = QueryBuilderUtils.getClausesForType(psiField, isValidReference || Utils.isNullable(psiField, type)); + List clauses = QueryBuilderUtils.getClausesForType(psiField, isValidReference || IntelliJPluginUtils.isNullable(psiField, type)); for (QueryClause clause : clauses) { String methodName = clause.getMethodName(psiField.getName()); List parameterTypes = clause.getMethodParamTypes(parentClass.getManager(), innerType); diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Utils.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/IntelliJPluginUtils.java similarity index 80% rename from intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Utils.java rename to intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/IntelliJPluginUtils.java index ea702a44..292eefb9 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/Utils.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/IntelliJPluginUtils.java @@ -1,10 +1,11 @@ package net.staticstudios.data.ide.intellij; import com.intellij.psi.*; +import net.staticstudios.data.utils.Constants; import java.util.Objects; -public class Utils { +public class IntelliJPluginUtils { public static boolean is(PsiType type, String classFqn) { if (!(type instanceof PsiClassType psiClassType)) { return false; @@ -68,14 +69,14 @@ public static boolean isNullable(PsiField psiField, PsiType fieldType) { } public static boolean isValidPersistentValue(PsiField psiField) { - return Utils.is(psiField.getType(), Constants.PERSISTENT_VALUE_FQN) && ( - Utils.hasAnnotation(psiField, Constants.COLUMN_ANNOTATION_FQN) || - Utils.hasAnnotation(psiField, Constants.FOREIGN_COLUMN_ANNOTATION_FQN) || - Utils.hasAnnotation(psiField, Constants.ID_COLUMN_ANNOTATION_FQN)); + return IntelliJPluginUtils.is(psiField.getType(), Constants.PERSISTENT_VALUE_FQN) && ( + IntelliJPluginUtils.hasAnnotation(psiField, Constants.COLUMN_ANNOTATION_FQN) || + IntelliJPluginUtils.hasAnnotation(psiField, Constants.FOREIGN_COLUMN_ANNOTATION_FQN) || + IntelliJPluginUtils.hasAnnotation(psiField, Constants.ID_COLUMN_ANNOTATION_FQN)); } public static boolean isValidReference(PsiField psiField) { - return Utils.is(psiField.getType(), Constants.REFERENCE_FQN) && - Utils.hasAnnotation(psiField, Constants.ONE_TO_ONE_ANNOTATION_FQN); + return IntelliJPluginUtils.is(psiField.getType(), Constants.REFERENCE_FQN) && + IntelliJPluginUtils.hasAnnotation(psiField, Constants.ONE_TO_ONE_ANNOTATION_FQN); } } diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/NumericClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/NumericClause.java index b41543f3..2604625d 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/NumericClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/NumericClause.java @@ -2,13 +2,13 @@ import com.intellij.psi.PsiClassType; import com.intellij.psi.PsiField; -import net.staticstudios.data.ide.intellij.Utils; +import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; public interface NumericClause extends QueryClause { @Override default boolean matches(PsiField psiField, boolean nullable) { if (!(psiField.getType() instanceof PsiClassType psiClassType)) return false; - return QueryBuilderUtils.isNumeric(Utils.getGenericParameter(psiClassType, psiField.getManager())); + return QueryBuilderUtils.isNumeric(IntelliJPluginUtils.getGenericParameter(psiClassType, psiField.getManager())); } } 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 451b5442..4a856c51 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 @@ -2,7 +2,7 @@ import com.intellij.psi.PsiField; import com.intellij.psi.PsiType; -import net.staticstudios.data.ide.intellij.Utils; +import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; import net.staticstudios.data.ide.intellij.query.clause.*; import java.sql.Timestamp; @@ -39,7 +39,7 @@ public class QueryBuilderUtils { } public static List getClausesForType(PsiField psiField, boolean nullable) { - if (Utils.isValidPersistentValue(psiField)) { + if (IntelliJPluginUtils.isValidPersistentValue(psiField)) { List applicableClauses = new ArrayList<>(); for (QueryClause clause : pvClauses) { if (clause.matches(psiField, nullable)) { @@ -48,7 +48,7 @@ public static List getClausesForType(PsiField psiField, boolean nul } return applicableClauses; } - if (Utils.isValidReference(psiField)) { + if (IntelliJPluginUtils.isValidReference(psiField)) { List applicableClauses = new ArrayList<>(); for (QueryClause clause : referenceClauses) { if (clause.matches(psiField, nullable)) { diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java index 1cada5ff..0213336e 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java @@ -3,7 +3,7 @@ import com.intellij.psi.PsiField; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiType; -import net.staticstudios.data.ide.intellij.Utils; +import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; import net.staticstudios.data.ide.intellij.query.QueryClause; import java.util.List; @@ -12,7 +12,7 @@ public class IsLikeClause implements QueryClause { @Override public boolean matches(PsiField psiField, boolean nullable) { - return Utils.is(psiField.getType(), String.class.getName()); + return IntelliJPluginUtils.is(psiField.getType(), String.class.getName()); } @Override diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java index 7e489366..9447a5ff 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java @@ -3,7 +3,7 @@ import com.intellij.psi.PsiField; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiType; -import net.staticstudios.data.ide.intellij.Utils; +import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; import net.staticstudios.data.ide.intellij.query.QueryClause; import java.util.List; @@ -12,7 +12,7 @@ public class IsNotLikeClause implements QueryClause { @Override public boolean matches(PsiField psiField, boolean nullable) { - return Utils.is(psiField.getType(), String.class.getName()); + return IntelliJPluginUtils.is(psiField.getType(), String.class.getName()); } @Override diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml index b0d0b746..a46e8026 100644 --- a/intellij-plugin/src/main/resources/META-INF/plugin.xml +++ b/intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -8,7 +8,7 @@ provide a better developer experience, with strict type safety. This plugin makes IntelliJ aware of these generated classes and/or methods. - + diff --git a/javac-plugin/build.gradle b/javac-plugin/build.gradle index c1ee0d4e..be573b91 100644 --- a/javac-plugin/build.gradle +++ b/javac-plugin/build.gradle @@ -7,7 +7,9 @@ repositories { } dependencies { - implementation(project(":annotations")) + implementation(project(":utils")) + implementation 'org.jetbrains:annotations:24.0.1' + implementation("com.google.guava:guava:33.5.0-jre") } tasks.withType(JavaCompile).configureEach { @@ -24,7 +26,6 @@ tasks.withType(JavaCompile).configureEach { ] } - java { toolchain { languageVersion = JavaLanguageVersion.of(21) diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java new file mode 100644 index 00000000..7921a5cc --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java @@ -0,0 +1,641 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.TypeTag; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.Names; + +import java.util.ArrayList; +import java.util.Collection; + +public class BuilderProcessor { //todo: abstract processor which has utility methods maybe + + private final JCTree.JCCompilationUnit compilationUnit; + private final TreeMaker treeMaker; + private final Names names; + private final JCTree.JCClassDecl dataClassDecl; + private final ParsedDataAnnotation dataAnnotation; + private JCTree.JCClassDecl builderClassDecl; + + public BuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation) { + this.compilationUnit = compilationUnit; + this.treeMaker = treeMaker; + this.names = names; + this.dataClassDecl = dataClassDecl; + this.dataAnnotation = dataAnnotation; + } + + public static boolean hasProcessed(JCTree.JCClassDecl classDecl) { + return classDecl.defs.stream() + .anyMatch(def -> def instanceof JCTree.JCClassDecl && + ((JCTree.JCClassDecl) def).name.toString().equals(classDecl.name + "Builder")); + } + + + public void process() { + if (hasProcessed(dataClassDecl)) { + return; + } + + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "DataManager"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "ValueUtils"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.insert", "InsertContext"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "InsertMode"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "InsertStrategy"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "UniqueDataMetadata"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "ColumnValuePair"); + + makeBuilderClass(); + makeBuilderMethod(); + makeParameterizedBuilderMethod(); + + Collection persistentValues = ParsedPersistentValue.extractPersistentValues(dataClassDecl, dataAnnotation, treeMaker, names); + for (ParsedPersistentValue pv : persistentValues) { + processValue(pv); + } + + Collection references = ParsedReference.extractReferences(dataClassDecl, dataAnnotation, treeMaker, names); + for (ParsedReference ref : references) { + processReference(ref); + } + + makeInsertContextMethod(persistentValues, references); + makeInsertModeMethod(); + } + + + private void makeBuilderClass() { + builderClassDecl = treeMaker.ClassDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString(getBuilderClassName()), + List.nil(), + null, + List.nil(), + List.nil() + ); + + JCTree.JCVariableDecl dataManagerField = treeMaker.VarDef( + treeMaker.Modifiers(Flags.PRIVATE | Flags.FINAL), + names.fromString("dataManager"), + treeMaker.Ident(names.fromString("DataManager")), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(dataManagerField); + + JCTree.JCMethodDecl constructor = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC), + names.fromString(""), + null, + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("dataManager"), + treeMaker.Ident(names.fromString("DataManager")), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString("dataManager") + ), + treeMaker.Ident(names.fromString("dataManager")) + ) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(constructor); + + dataClassDecl.defs = dataClassDecl.defs.append(builderClassDecl); + } + + + private void makeParameterizedBuilderMethod() { + JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString("builder"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Return( + treeMaker.Apply( + List.nil(), + treeMaker.Ident(names.fromString("builder")), + List.of( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("DataManager")), + names.fromString("getInstance") + ), + List.nil() + ) + ) + ) + ) + )), + null + ); + dataClassDecl.defs = dataClassDecl.defs.append(builderMethod); + } + + private void makeBuilderMethod() { + JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString("builder"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("dataManager"), + treeMaker.Ident(names.fromString("DataManager")), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Return( + treeMaker.NewClass(null, List.nil(), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.of( + treeMaker.Ident(names.fromString("dataManager")) + ), + null + ) + ) + )), + null + ); + dataClassDecl.defs = dataClassDecl.defs.append(builderMethod); + } + + private String getBuilderClassName() { + return dataClassDecl.name.toString() + "Builder"; + } + + + private void processValue(ParsedPersistentValue pv) { + String schemaFieldName = pv.getFieldName() + "$schema"; + String tableFieldName = pv.getFieldName() + "$table"; + String columnFieldName = pv.getFieldName() + "$column"; + + JCTree.JCExpression stringType = treeMaker.Ident(names.fromString("String")); + JCTree.JCExpression schemaInit = treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("ValueUtils")), + names.fromString("parseValue") + ), + List.of( + treeMaker.Literal(pv.getSchema()) + ) + ); + JCTree.JCExpression tableInit = treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("ValueUtils")), + names.fromString("parseValue") + ), + List.of( + treeMaker.Literal(pv.getTable()) + ) + ); + JCTree.JCExpression columnInit = treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("ValueUtils")), + names.fromString("parseValue") + ), + List.of( + treeMaker.Literal(pv.getColumn()) + ) + ); + + JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, schemaFieldName, stringType, schemaInit); + JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, tableFieldName, stringType, tableInit); + JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, columnFieldName, stringType, columnInit); + + JCTree.JCExpression nullInit = treeMaker.Literal(TypeTag.BOT, null); + + JavaCPluginUtils.generatePrivateMemberField(treeMaker, names, builderClassDecl, pv.getFieldName(), pv.getType(), nullInit); + + JCTree.JCMethodDecl setterMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName()), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + pv.getType(), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(pv.getFieldName()) + ), + treeMaker.Ident(names.fromString(pv.getFieldName())) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + + builderClassDecl.defs = builderClassDecl.defs.append(setterMethod); + } + + private void processReference(ParsedReference ref) { + String idColumnValuePairsFieldName = ref.getFieldName() + "_reference$idColumnValuePairs"; + String schemaFieldName = ref.getFieldName() + "_reference$schema"; + String tableFieldName = ref.getFieldName() + "_reference$table"; + + JCTree.JCExpression arrayType = treeMaker.TypeArray(treeMaker.Ident(names.fromString("ColumnValuePair"))); + JCTree.JCExpression stringType = treeMaker.Ident(names.fromString("String")); + + JCTree.JCExpression nullInit = treeMaker.Literal(TypeTag.BOT, null); + + JavaCPluginUtils.generatePrivateMemberField(treeMaker, names, builderClassDecl, idColumnValuePairsFieldName, arrayType, nullInit); + JavaCPluginUtils.generatePrivateMemberField(treeMaker, names, builderClassDecl, schemaFieldName, stringType, nullInit); + JavaCPluginUtils.generatePrivateMemberField(treeMaker, names, builderClassDecl, tableFieldName, stringType, nullInit); + + var handleNotNull = treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(idColumnValuePairsFieldName) + ), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString(ref.getFieldName())), + names.fromString("getIdColumns") + ), + List.nil() + ), + names.fromString("getPairs") + ), + List.nil() + ) + ) + ), + treeMaker.VarDef( + treeMaker.Modifiers(0), + names.fromString("__$metadata"), + treeMaker.Ident(names.fromString("UniqueDataMetadata")), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString(ref.getFieldName())), + names.fromString("getMetadata") + ), + List.nil() + ) + ), + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(schemaFieldName) + ), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("__$metadata")), + names.fromString("schema") + ), + List.nil() + ) + ) + ), + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(tableFieldName) + ), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("__$metadata")), + names.fromString("table") + ), + List.nil() + ) + ) + ) + )); + + var handleNull = treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(idColumnValuePairsFieldName) + ), + treeMaker.Literal(TypeTag.BOT, null) + ) + ), + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(schemaFieldName) + ), + treeMaker.Literal(TypeTag.BOT, null) + ) + ), + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(tableFieldName) + ), + treeMaker.Literal(TypeTag.BOT, null) + ) + ) + )); + + JCTree.JCMethodDecl setterMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(ref.getFieldName()), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(ref.getFieldName()), + ref.getType(), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.If( + treeMaker.Binary( + JCTree.Tag.NE, + treeMaker.Ident(names.fromString(ref.getFieldName())), + treeMaker.Literal(TypeTag.BOT, null) + ), + handleNotNull, + handleNull + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(setterMethod); + } + + private void makeInsertContextMethod(Collection parsedPersistentValues, Collection parsedReferences) { + java.util.List bodyStatements = new ArrayList<>(); + + for (ParsedPersistentValue pv : parsedPersistentValues) { + JCTree.JCExpression schemaFieldAccess = treeMaker.Ident(names.fromString(pv.getFieldName() + "$schema")); + JCTree.JCExpression tableFieldAccess = treeMaker.Ident(names.fromString(pv.getFieldName() + "$table")); + JCTree.JCExpression columnFieldAccess = treeMaker.Ident(names.fromString(pv.getFieldName() + "$column")); + + JCTree.JCExpression fieldAccess = treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(pv.getFieldName()) + ); + + String insertStrategy = null; + if (pv instanceof ParsedForeignPersistentValue foreignPv) { + insertStrategy = foreignPv.getInsertStrategy(); + } + + JCTree.JCExpression insertStatement = treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("ctx")), + names.fromString("set") + ), + List.of( + schemaFieldAccess, + tableFieldAccess, + columnFieldAccess, + fieldAccess, + insertStrategy != null ? + treeMaker.Select( + treeMaker.Ident(names.fromString("InsertStrategy")), + names.fromString(insertStrategy) + ) + : + treeMaker.Literal(TypeTag.BOT, null) + ) + ); + + bodyStatements.add(treeMaker.Exec(insertStatement)); + } + + for (ParsedReference ref : parsedReferences) { + String idColumnValuePairsFieldName = ref.getFieldName() + "_reference$idColumnValuePairs"; + String schemaFieldName = ref.getFieldName() + "_reference$schema"; + String tableFieldName = ref.getFieldName() + "_reference$table"; + + + bodyStatements.add( + treeMaker.If( + treeMaker.Binary( + JCTree.Tag.NE, + treeMaker.Ident(names.fromString(idColumnValuePairsFieldName)), + treeMaker.Literal(TypeTag.BOT, null) + ), + treeMaker.ForLoop( + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(0), + names.fromString("i"), + treeMaker.TypeIdent(TypeTag.INT), + treeMaker.Literal(0) + ) + ), + treeMaker.Binary( + JCTree.Tag.LT, + treeMaker.Ident(names.fromString("i")), + treeMaker.Select( + treeMaker.Ident(names.fromString(idColumnValuePairsFieldName)), + names.fromString("length") + ) + ), + List.of( + treeMaker.Exec( + treeMaker.Unary( + JCTree.Tag.POSTINC, + treeMaker.Ident(names.fromString("i")) + ) + ) + ), + treeMaker.Block( + 0, + List.of( + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("ctx")), + names.fromString("set") + ), + List.of( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(schemaFieldName) + ), + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString(tableFieldName) + ), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Indexed( + treeMaker.Ident(names.fromString(idColumnValuePairsFieldName)), + treeMaker.Ident(names.fromString("i")) + ), + names.fromString("column") + ), + List.nil() + ), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Indexed( + treeMaker.Ident(names.fromString(idColumnValuePairsFieldName)), + treeMaker.Ident(names.fromString("i")) + ), + names.fromString("value") + ), + List.nil() + ), + treeMaker.Select( + treeMaker.Ident(names.fromString("InsertStrategy")), + names.fromString("OVERWRITE_EXISTING") + ) + ) + ) + ) + + ) + ) + ), + null + ) + ); + } + + JCTree.JCMethodDecl insertMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("insert"), + treeMaker.TypeIdent(TypeTag.VOID), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("ctx"), + treeMaker.Ident(names.fromString("InsertContext")), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.from(bodyStatements)), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(insertMethod); + } + + public void makeInsertModeMethod() { + JCTree.JCMethodDecl insertMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("insert"), + treeMaker.Ident(dataClassDecl.name), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("mode"), + treeMaker.Ident(names.fromString("InsertMode")), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.VarDef( + treeMaker.Modifiers(0), + names.fromString("ctx"), + treeMaker.Ident(names.fromString("InsertContext")), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("dataManager")), + names.fromString("createInsertContext") + ), + List.nil() + ) + ), + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString("insert") + ), + List.of( + treeMaker.Ident(names.fromString("ctx")) + ) + ) + ), + treeMaker.Return( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("ctx")), + names.fromString("insert") + ), + List.of( + treeMaker.Ident(names.fromString("mode")) + ) + ), + names.fromString("get") + ), + List.of( + treeMaker.Select( + treeMaker.Ident(dataClassDecl.name), + names.fromString("class") + ) + ) + ) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(insertMethod); + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java new file mode 100644 index 00000000..70aeaadc --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java @@ -0,0 +1,358 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.tools.javac.code.Attribute; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.Name; +import com.sun.tools.javac.util.Names; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; + +public class JavaCPluginUtils { + public static boolean isAnnotation(@NotNull JCTree.JCAnnotation annotation, @NotNull String targetFqn) { + JCTree annotationType = annotation.getAnnotationType(); + return isFQN(annotationType, targetFqn); + } + + public static boolean isAnnotation(@NotNull Attribute.Compound annotation, @NotNull String targetFqn) { + Symbol.TypeSymbol typeSymbol = annotation.type.tsym; + if (typeSymbol == null) { + return false; + } + Name qualifiedName = typeSymbol.getQualifiedName(); + return qualifiedName.contentEquals(targetFqn); + } + + public static boolean isFQN(@Nullable JCTree tree, @NotNull String targetFqn) { + if (tree == null) { + return false; + } + Type type = tree.type; + if (type == null) { + return false; + } + String fqn = type.toString(); + return fqn.equals(targetFqn); + } + + public static boolean isFQN(@Nullable Symbol.VarSymbol varSymbol, @NotNull String targetFqn) { + if (varSymbol == null) { + return false; + } + + Symbol typeSymbol = varSymbol.type != null ? varSymbol.type.tsym : null; + if (typeSymbol == null) { + return false; + } + + return typeSymbol.getQualifiedName().contentEquals(targetFqn); + } + + public static @Nullable JCTree.JCAnnotation extractAnnotation(JCTree.JCClassDecl classDecl, String targetFqn) { + for (JCTree.JCAnnotation annotation : classDecl.getModifiers().getAnnotations()) { + if (isAnnotation(annotation, targetFqn)) { + return annotation; + } + } + return null; + } + + /** + * Get a string annotation value, treating empty strings as null + */ + public static @Nullable String getStringAnnotationValue(@NotNull JCTree.JCAnnotation annotation, @NotNull String key) { + String value = getAnnotationValue(annotation, String.class, key); + if (value != null && !value.isEmpty()) { + return value; + } + return null; // Treat empty strings as null + } + + public static @Nullable T getAnnotationValue(@NotNull JCTree.JCAnnotation annotation, @NotNull Class type, @NotNull String key) { + List args = annotation.args; + if (args == null) { + return null; + } + for (JCTree.JCExpression arg : args) { + if (arg instanceof JCTree.JCAssign assign) { + String propertyName = assign.lhs.toString(); + if (propertyName.equals(key)) { + if (assign.rhs instanceof JCTree.JCLiteral rhs) { + if (type.isInstance(rhs.value)) { + return type.cast(rhs.value); + } + } else { + throw new UnsupportedOperationException("Cannot handle non-literal annotation values yet"); + } + + return null; + } + } else if ("value".equals(key)) { + if (arg instanceof JCTree.JCLiteral literal) { + if (type.isInstance(literal.value)) { + return type.cast(literal.value); + } + } else { + throw new UnsupportedOperationException("Cannot handle non-literal annotation values yet"); + } + + return null; + } + } + return null; + } + + /** + * Get a string annotation value, treating empty strings as null + */ + public static @Nullable String getStringAnnotationValue(@NotNull Attribute.Compound annotation, @NotNull String key) { + String value = getAnnotationValue(annotation, String.class, key); + if (value != null && !value.isEmpty()) { + return value; + } + return null; // Treat empty strings as null + } + + public static @Nullable T getAnnotationValue(@NotNull Attribute.Compound annotation, + @NotNull Class type, + @NotNull String key) { + for (var pair : annotation.values) { + String elementName = pair.fst.getSimpleName().toString(); + if (!elementName.equals(key)) continue; + + Object constValue = extractAnnotationValue(pair.snd); + if (type.isInstance(constValue)) { + return type.cast(constValue); + } + return null; + } + + if ("value".equals(key) && annotation.values.isEmpty()) { + Object defaultVal = getDefaultAnnotationValue(annotation, key); + if (type.isInstance(defaultVal)) { + return type.cast(defaultVal); + } + } + + return null; + } + + public static @Nullable String getStringAnnotationValue(java.util.List annotations, + @NotNull String targetFqn, + @NotNull String key) { + String value = getAnnotationValue(annotations, String.class, targetFqn, key); + if (value != null && !value.isEmpty()) { + return value; + } + return null; // Treat empty strings as null + } + + public static @Nullable T getAnnotationValue(java.util.List annotations, + @NotNull Class type, + @NotNull String targetFqn, + @NotNull String key) { + for (Attribute.Compound annotation : annotations) { + if (isAnnotation(annotation, targetFqn)) { + return getAnnotationValue(annotation, type, key); + } + } + return null; + } + + public static JCTree.JCExpression makeFqnIdent(@NotNull TreeMaker treeMaker, @NotNull Names names, @NotNull String fqn) { + String[] parts = fqn.split("\\."); + JCTree.JCExpression expression = treeMaker.Ident(names.fromString(parts[0])); + for (int i = 1; i < parts.length; i++) { + expression = treeMaker.Select(expression, names.fromString(parts[i])); + } + return expression; + } + + public static void importClass(@NotNull JCTree.JCCompilationUnit compilationUnit, @NotNull TreeMaker treeMaker, @NotNull Names names, String packageName, String className) { + // Build a package expression that supports dot-qualified names (uses makeFqnIdent) + JCTree.JCExpression pkgExpr = makeFqnIdent(treeMaker, names, packageName); + JCTree.JCImport jcImport = treeMaker.Import( + treeMaker.Select( + pkgExpr, + names.fromString(className) + ), + false + ); + + // Don't add duplicate imports: compare the string form of existing imports + for (JCTree def : compilationUnit.defs) { + if (def instanceof JCTree.JCImport) { + if (def.toString().equals(jcImport.toString())) { + return; // already imported + } + } + } + + // Insert the import before the first top-level class declaration. This keeps + // imports after package/imports and avoids placing them before the package. + List oldDefs = compilationUnit.defs; + List newDefs = List.nil(); + boolean inserted = false; + for (JCTree def : oldDefs) { + if (!inserted && def instanceof JCTree.JCClassDecl) { + newDefs = newDefs.append(jcImport); + inserted = true; + } + newDefs = newDefs.append(def); + } + if (!inserted) { + // no class declarations found; append the import at the end + newDefs = newDefs.append(jcImport); + } + compilationUnit.defs = newDefs; + } + + public static void generatePrivateStaticField( + @NotNull TreeMaker treeMaker, + @NotNull Names names, + @NotNull JCTree.JCClassDecl classDecl, + @NotNull String name, + @NotNull JCTree.JCExpression type, + @Nullable JCTree.JCExpression init + ) { + JCTree.JCVariableDecl fieldDef = treeMaker.VarDef( + treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL), + names.fromString(name), + type, + init + ); + classDecl.defs = classDecl.defs.append(fieldDef); + } + + public static void generatePrivateMemberField( + @NotNull TreeMaker treeMaker, + @NotNull Names names, + @NotNull JCTree.JCClassDecl classDecl, + @NotNull String name, + @NotNull JCTree.JCExpression type, + @Nullable JCTree.JCExpression init + ) { + JCTree.JCVariableDecl fieldDef = treeMaker.VarDef( + treeMaker.Modifiers(Flags.PRIVATE), + names.fromString(name), + type, + init + ); + classDecl.defs = classDecl.defs.append(fieldDef); + } + + private static @Nullable Object extractAnnotationValue(@NotNull Attribute attribute) { + return switch (attribute) { + case Attribute.Constant c -> c.getValue(); + case Attribute.Enum e -> e.value.toString(); + case Attribute.Class c -> c.classType.tsym.getQualifiedName().toString(); + default -> null; + }; + } + + private static @Nullable Object getDefaultAnnotationValue(@NotNull Attribute.Compound annotation, @NotNull String key) { + Symbol.TypeSymbol typeSymbol = annotation.type.tsym; + if (!(typeSymbol instanceof Symbol.ClassSymbol classSym)) { + return null; + } + + for (Symbol member : classSym.members().getSymbols()) { + if (member instanceof Symbol.MethodSymbol method) { + Name name = method.name; + if (name != null && name.contentEquals(key)) { + Attribute defaultValue = method.getDefaultValue(); + if (defaultValue != null) { + return extractAnnotationValue(defaultValue); + } + } + } + } + return null; + } + + public static Collection getFields(@NotNull JCTree.JCClassDecl classDecl, @NotNull String targetFqn) { + java.util.List fieldList = new ArrayList<>(); + getFields(classDecl.sym, targetFqn, fieldList); + return fieldList; + } + + private static void getFields(@NotNull Symbol.ClassSymbol classSymbol, @NotNull String targetFqn, Collection fields) { + for (Symbol symbol : classSymbol.getEnclosedElements()) { + if (!(symbol instanceof Symbol.VarSymbol varSymbol)) { + continue; + } + + if (isFQN(varSymbol, targetFqn)) { + fields.add(varSymbol); + } + } + + Type superType = classSymbol.getSuperclass(); + if (superType != null && superType.tsym instanceof Symbol.ClassSymbol superClassSymbol) { + getFields(superClassSymbol, targetFqn, fields); + } + } + + public static @Nullable JCTree.JCExpression getGenericTypeExpression( + @NotNull TreeMaker treeMaker, + @NotNull Names names, + @NotNull Symbol.VarSymbol varSymbol, + int index + ) { + Type varType = varSymbol.type; + if (!(varType instanceof Type.ClassType classType)) { + return null; + } + com.sun.tools.javac.util.List typeArgs = classType.getTypeArguments(); + if (typeArgs == null || typeArgs.isEmpty() || index < 0 || index >= typeArgs.size()) { + return null; + } + return typeToExpression(treeMaker, names, typeArgs.get(index)); + } + + private static @Nullable JCTree.JCExpression typeToExpression(@NotNull TreeMaker treeMaker, + @NotNull Names names, + @Nullable Type type) { + if (type == null) return null; + + // Parameterized / class types + if (type.tsym != null) { + String qn = type.tsym.getQualifiedName().toString(); + JCTree.JCExpression base = makeFqnIdent(treeMaker, names, qn); + + if (type instanceof Type.ClassType classType) { + com.sun.tools.javac.util.List args = classType.getTypeArguments(); + if (args != null && !args.isEmpty()) { + com.sun.tools.javac.util.List jcArgs = List.nil(); + for (Type ta : args) { + JCTree.JCExpression expr = typeToExpression(treeMaker, names, ta); + jcArgs = jcArgs.append(expr != null ? expr : treeMaker.Ident(names.fromString(ta.toString()))); + } + return treeMaker.TypeApply(base, jcArgs); + } + } + return base; + } + + // Array types + if (type instanceof Type.ArrayType arr) { + JCTree.JCExpression elem = typeToExpression(treeMaker, names, arr.elemtype); + return elem != null ? treeMaker.TypeArray(elem) : null; + } + + // Fallback to a simple identifier (covers type variables / wildcards minimally) + return treeMaker.Ident(names.fromString(type.toString())); + } + + + //todo: need to be able to extract the generic type +} + + diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedAnnotation.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedAnnotation.java new file mode 100644 index 00000000..a6302df3 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedAnnotation.java @@ -0,0 +1,14 @@ +package net.staticstudios.data.compiler.javac; + +public abstract class ParsedAnnotation { + private final String annotationFQN; + + public ParsedAnnotation(String annotationFQN) { + this.annotationFQN = annotationFQN; + } + + public String getAnnotationFQN() { + return annotationFQN; + } + +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedColumnAnnotation.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedColumnAnnotation.java new file mode 100644 index 00000000..bbbc30bd --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedColumnAnnotation.java @@ -0,0 +1,61 @@ +package net.staticstudios.data.compiler.javac; + +import net.staticstudios.data.utils.Constants; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ParsedColumnAnnotation extends ParsedAnnotation { + private final @NotNull String name; + private final @Nullable String schema; + private final @Nullable String table; + private final boolean index; + private final boolean nullable; + private final boolean unique; + + public ParsedColumnAnnotation( + @NotNull String name, + @Nullable String schema, + @Nullable String table, + boolean index, + boolean nullable, + boolean unique + ) { + super(Constants.COLUMN_ANNOTATION_FQN); + this.name = name; + this.schema = schema; + this.table = table; + this.index = index; + this.nullable = nullable; + this.unique = unique; + } + + public @NotNull String getName() { + return name; + } + + public @NotNull String getSchema(@NotNull ParsedDataAnnotation dataAnnotation) { + if (schema != null) { + return schema; + } + return dataAnnotation.getSchema(); + } + + public @NotNull String getTable(@NotNull ParsedDataAnnotation dataAnnotation) { + if (table != null) { + return table; + } + return dataAnnotation.getTable(); + } + + public boolean createIndex() { + return index; + } + + public boolean isNullable() { + return nullable; + } + + public boolean isUnique() { + return unique; + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedDataAnnotation.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedDataAnnotation.java new file mode 100644 index 00000000..10507336 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedDataAnnotation.java @@ -0,0 +1,37 @@ +package net.staticstudios.data.compiler.javac; + +import com.google.common.base.Preconditions; +import com.sun.tools.javac.tree.JCTree; +import net.staticstudios.data.utils.Constants; +import org.jetbrains.annotations.NotNull; + +public class ParsedDataAnnotation extends ParsedAnnotation { + private final @NotNull String schema; + private final @NotNull String table; + + public ParsedDataAnnotation(@NotNull String schema, @NotNull String table) { + super(Constants.DATA_ANNOTATION_FQN); + this.schema = schema; + this.table = table; + } + + public static ParsedDataAnnotation extract(JCTree.JCClassDecl classDecl) { + JCTree.JCAnnotation dataAnnotation = JavaCPluginUtils.extractAnnotation(classDecl, Constants.DATA_ANNOTATION_FQN); + Preconditions.checkNotNull(dataAnnotation, "Data annotation not found on class: " + classDecl.getSimpleName()); + String schema = JavaCPluginUtils.getStringAnnotationValue(dataAnnotation, "schema"); + String table = JavaCPluginUtils.getStringAnnotationValue(dataAnnotation, "table"); + + Preconditions.checkNotNull(schema, "Data annotation 'schema' value cannot be null on class: " + classDecl.getSimpleName()); + Preconditions.checkNotNull(table, "Data annotation 'table' value cannot be null on class: " + classDecl.getSimpleName()); + + return new ParsedDataAnnotation(schema, table); + } + + public @NotNull String getSchema() { + return schema; + } + + public @NotNull String getTable() { + return table; + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java new file mode 100644 index 00000000..b9056eec --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java @@ -0,0 +1,28 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.tools.javac.tree.JCTree; + +class ParsedForeignPersistentValue extends ParsedPersistentValue { + private final String insertStrategy; + + public ParsedForeignPersistentValue(String fieldName, String schema, String table, String column, JCTree.JCExpression type, String insertStrategy) { + super(fieldName, schema, table, column, type); + this.insertStrategy = insertStrategy; + } + + public String getInsertStrategy() { + return insertStrategy; + } + + @Override + public String toString() { + return "ParsedForeignPersistentValue{" + + "fieldName='" + getFieldName() + '\'' + + ", schema='" + getSchema() + '\'' + + ", table='" + getTable() + '\'' + + ", column='" + getColumn() + '\'' + + ", type=" + getType() + + ", insertStrategy='" + insertStrategy + '\'' + + '}'; + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java new file mode 100644 index 00000000..0bb675e2 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java @@ -0,0 +1,131 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.tools.javac.code.Attribute; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.Names; +import net.staticstudios.data.utils.Constants; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +class ParsedPersistentValue { + private final String fieldName; + private final String schema; + private final String table; + private final String column; + private final JCTree.JCExpression type; + + public ParsedPersistentValue(String fieldName, String schema, String table, String column, JCTree.JCExpression type) { + this.fieldName = fieldName; + this.schema = schema; + this.table = table; + this.column = column; + this.type = type; + } + + public static Collection extractPersistentValues(@NotNull JCTree.JCClassDecl dataClassDecl, + @NotNull ParsedDataAnnotation dataAnnotation, + @NotNull TreeMaker treeMaker, + @NotNull Names names + + ) { + Collection persistentValues = new ArrayList<>(); + Collection fields = JavaCPluginUtils.getFields(dataClassDecl, Constants.PERSISTENT_VALUE_FQN); + for (Symbol.VarSymbol varSymbol : fields) { + List annotations = varSymbol.getAnnotationMirrors(); + for (Attribute.Compound annotation : annotations) { + boolean isIdColumnAnnotation = JavaCPluginUtils.isAnnotation(annotation, Constants.ID_COLUMN_ANNOTATION_FQN); + boolean isColumnAnnotation = JavaCPluginUtils.isAnnotation(annotation, Constants.COLUMN_ANNOTATION_FQN); + boolean isForeignColumnAnnotation = JavaCPluginUtils.isAnnotation(annotation, Constants.FOREIGN_COLUMN_ANNOTATION_FQN); + + if (!isColumnAnnotation && !isForeignColumnAnnotation && !isIdColumnAnnotation) { + continue; + } + String columnName = Objects.requireNonNull(JavaCPluginUtils.getStringAnnotationValue(annotation, "name")); + String schemaValue; + String tableValue; + + if (isIdColumnAnnotation) { + schemaValue = dataAnnotation.getSchema(); + tableValue = dataAnnotation.getTable(); + } else { + schemaValue = JavaCPluginUtils.getStringAnnotationValue(annotation, "schema"); + tableValue = JavaCPluginUtils.getStringAnnotationValue(annotation, "table"); + + if (schemaValue == null) { + schemaValue = dataAnnotation.getSchema(); + } + if (tableValue == null) { + tableValue = dataAnnotation.getTable(); + } + } + + JCTree.JCExpression typeExpression = JavaCPluginUtils.getGenericTypeExpression(treeMaker, names, varSymbol, 0); + ParsedPersistentValue parsedPersistentValue; + + if (isForeignColumnAnnotation) { + String insertStrategy = JavaCPluginUtils.getStringAnnotationValue(annotations, Constants.INSERT_ANNOTATION_FQN, "value"); + if (insertStrategy == null) { + insertStrategy = "PREFER_EXISTING"; + } + parsedPersistentValue = new ParsedForeignPersistentValue( + varSymbol.getSimpleName().toString(), + schemaValue, + tableValue, + columnName, + typeExpression, + insertStrategy + ); + } else { + parsedPersistentValue = new ParsedPersistentValue( + varSymbol.getSimpleName().toString(), + schemaValue, + tableValue, + columnName, + typeExpression + ); + } + persistentValues.add(parsedPersistentValue); + break; + } + } + + return persistentValues; + } + + public String getFieldName() { + return fieldName; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getColumn() { + return column; + } + + public JCTree.JCExpression getType() { + return type; + } + + @Override + public String toString() { + return "PersistentValue{" + + "fieldName='" + fieldName + '\'' + + ", schema='" + schema + '\'' + + ", table='" + table + '\'' + + ", column='" + column + '\'' + + ", type=" + type + + '}'; + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java new file mode 100644 index 00000000..b3e7ab36 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java @@ -0,0 +1,99 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.tools.javac.code.Attribute; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.Names; +import net.staticstudios.data.utils.Constants; +import net.staticstudios.data.utils.Link; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +class ParsedReference { + private final String fieldName; + private final List links; + private final JCTree.JCExpression type; + + public ParsedReference(String fieldName, List links, JCTree.JCExpression type) { + this.fieldName = fieldName; + this.links = links; + this.type = type; + } + + public static Collection extractReferences(@NotNull JCTree.JCClassDecl dataClassDecl, + @NotNull ParsedDataAnnotation dataAnnotation, + @NotNull TreeMaker treeMaker, + @NotNull Names names + + ) { + Collection references = new ArrayList<>(); + Collection fields = JavaCPluginUtils.getFields(dataClassDecl, Constants.REFERENCE_FQN); + for (Symbol.VarSymbol varSymbol : fields) { + List annotations = varSymbol.getAnnotationMirrors(); + for (Attribute.Compound annotation : annotations) { + boolean isOneToOneAnnotation = JavaCPluginUtils.isAnnotation(annotation, Constants.ONE_TO_ONE_ANNOTATION_FQN); + + if (!isOneToOneAnnotation) { + continue; + } + + List links = Link.parseRawLinks(JavaCPluginUtils.getStringAnnotationValue(annotation, "link")); + + JCTree.JCExpression typeExpression = JavaCPluginUtils.getGenericTypeExpression(treeMaker, names, varSymbol, 0); + ParsedReference parsedReference = new ParsedReference( + varSymbol.getSimpleName().toString(), + links, + typeExpression + ); + references.add(parsedReference); + break; + } + } + + return references; + } + + public String getFieldName() { + return fieldName; + } + + public List getLinks() { + return links; + } + + public JCTree.JCExpression getType() { + return type; + } + + @Override + public String toString() { + return "ParsedReference{" + + "fieldName='" + fieldName + '\'' + + ", links=" + links + + ", type=" + type + + '}'; + } + +// private void settings(That t) { +// //this method just needs to set the id column values. the column names will be known at compile time +// //then during insert() is where it get tricky, but create a dummy instance to get the runtime type and them set the values accordingly, in the proper table. table/schema is obtained from runtime type metadata +// +// +// //todo: if the referenced type is abstract, dont support setting in the builder. +// String schema; //lookup at runtime due to inheritance +// String table; //lookup at runtime due to inheritance +//// String[] linkingColumnsInReferringTable = referenceLinkingColumns_[fieldName]; //todo: we dont need this here but we will need it during insert() +// String[] linkingColumnsInReferencedTable = referenceLinkedColumns_[fieldName]; +// +// for (int i = 0; i < linkingColumnsInReferringTable.length; i++) { +//// String colInReferringTable = linkingColumnsInReferringTable[i]; +// String colInReferencedTable = linkingColumnsInReferencedTable[i]; +// this.refenceLinkingValues_[fieldName] = new Object[]... +// } +// } + //todo: when storing the links, we can have two cols. each a static string array storing the parsed columns. we know the linking columns, and the table schema/table from the reference itself. (what if the refernced data is abstract? +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java index 4eeaa51b..ef6aa693 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java @@ -1,17 +1,13 @@ package net.staticstudios.data.compiler.javac; import com.sun.source.tree.ClassTree; -import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.Tree; import com.sun.source.util.*; import com.sun.tools.javac.api.BasicJavacTask; -import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.Context; -import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.Names; -import net.staticstudios.data.Data; +import net.staticstudios.data.utils.Constants; public class StaticDataJavacPlugin implements Plugin { @@ -29,7 +25,6 @@ public void init(JavacTask task, String... args) { Context context = ((BasicJavacTask) task).getContext(); TreeMaker treeMaker = TreeMaker.instance(context); Names names = Names.instance(context); - Trees trees = Trees.instance(task); task.addTaskListener(new TaskListener() { @Override @@ -40,63 +35,21 @@ public void finished(TaskEvent e) { @Override public Void visitClass(ClassTree node, Void unused) { boolean hasDataAnnotation = node.getModifiers().getAnnotations().stream() - .anyMatch(a -> { - Tree type = a.getAnnotationType(); - if (type instanceof IdentifierTree) { //todo: handle qualified names - return type.toString().equals(Data.class.getSimpleName()); - } - return false; - }); + .anyMatch(a -> JavaCPluginUtils.isAnnotation((JCTree.JCAnnotation) a, Constants.DATA_ANNOTATION_FQN)); - if (!hasDataAnnotation) return super.visitClass(node, unused); + if (hasDataAnnotation) { + JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) node; - JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) node; - - boolean hasBuilderClass = classDecl.defs.stream() - .anyMatch(def -> def instanceof JCTree.JCClassDecl && - ((JCTree.JCClassDecl) def).name.toString().equals("Builder")); - boolean hasBuilderMethod = classDecl.defs.stream() - .anyMatch(def -> def instanceof JCTree.JCMethodDecl && - ((JCTree.JCMethodDecl) def).name.toString().equals("builder") && - (((JCTree.JCMethodDecl) def).mods.flags & Flags.STATIC) != 0); - - if (!hasBuilderClass) { - System.err.println("Adding Builder class to " + classDecl.name); - JCTree.JCClassDecl builderClass = treeMaker.ClassDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), - names.fromString("Builder"), - List.nil(), - null, - List.nil(), - List.nil() - ); - classDecl.defs = classDecl.defs.append(builderClass); - } - if (!hasBuilderMethod) { - System.err.println("Adding builder() method to " + classDecl.name); - JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), - names.fromString("builder"), - treeMaker.Ident(names.fromString("Builder")), - List.nil(), - List.nil(), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Return( - treeMaker.NewClass(null, List.nil(), - treeMaker.Ident(names.fromString("Builder")), - List.nil(), null) - ) - )), - null - ); - classDecl.defs = classDecl.defs.append(builderMethod); + if (!BuilderProcessor.hasProcessed(classDecl)) { + ParsedDataAnnotation dataAnnotation = ParsedDataAnnotation.extract(classDecl); + new BuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation).process(); + } } + return super.visitClass(node, unused); } }, null); } }); - } } diff --git a/processor/build.gradle b/processor/build.gradle index c72da7ad..e3103bab 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -18,6 +18,7 @@ java { } dependencies { + implementation project(":utils") implementation project(":annotations") implementation 'com.palantir.javapoet:javapoet:0.7.0' diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java index 67b1a19e..01434a55 100644 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java @@ -91,7 +91,7 @@ private void generateFactory(TypeElement entityType, Data dataAnnotation, List parseRawLinks(String links) { + List mappings = new ArrayList<>(); + for (String link : StringUtils.parseCommaSeperatedList(links)) { + String[] parts = link.split("="); + Preconditions.checkArgument(parts.length == 2, "Invalid link format! Expected format: localColumn=foreignColumn, got: " + link); + mappings.add(new Link(parts[1].trim(), parts[0].trim())); + } + + return mappings; + } + + public static List parseRawLinksReversed(String links) { + List mappings = new ArrayList<>(); + for (String link : StringUtils.parseCommaSeperatedList(links)) { + String[] parts = link.split("="); + Preconditions.checkArgument(parts.length == 2, "Invalid link format! Expected format: localColumn=foreignColumn, got: " + link); + mappings.add(new Link(parts[0].trim(), parts[1].trim())); + } + + return mappings; + } +} diff --git a/annotations/src/main/java/net/staticstudios/data/utils/StringUtils.java b/utils/src/main/java/net/staticstudios/data/utils/StringUtils.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/utils/StringUtils.java rename to utils/src/main/java/net/staticstudios/data/utils/StringUtils.java From 9293396dce1230768ca151d14bee00cd2eeb6941 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 29 Oct 2025 01:20:34 -0400 Subject: [PATCH 40/75] AbstractBuilderProcessor --- .../javac/AbstractBuilderProcessor.java | 164 ++++++++++++++++++ .../data/compiler/javac/BuilderProcessor.java | 149 +--------------- .../data/compiler/javac/JavaCPluginUtils.java | 3 - .../compiler/javac/QueryBuilderProcessor.java | 24 +++ .../compiler/javac/StaticDataJavacPlugin.java | 3 +- 5 files changed, 197 insertions(+), 146 deletions(-) create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java new file mode 100644 index 00000000..86ddcaca --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java @@ -0,0 +1,164 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.Names; + +public abstract class AbstractBuilderProcessor { + protected final JCTree.JCCompilationUnit compilationUnit; + protected final TreeMaker treeMaker; + protected final Names names; + protected final JCTree.JCClassDecl dataClassDecl; + protected final ParsedDataAnnotation dataAnnotation; + private final String builderClassSuffix; + private final String builderMethodName; + protected JCTree.JCClassDecl builderClassDecl; + + public AbstractBuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation, String builderClassSuffix, String builderMethodName) { + this.compilationUnit = compilationUnit; + this.treeMaker = treeMaker; + this.names = names; + this.dataClassDecl = dataClassDecl; + this.dataAnnotation = dataAnnotation; + this.builderClassSuffix = builderClassSuffix; + this.builderMethodName = builderMethodName; + } + + protected abstract void addImports(); + + protected abstract void process(); + + public void runProcessor() { + if (dataClassDecl.defs.stream() + .anyMatch(def -> def instanceof JCTree.JCClassDecl && + ((JCTree.JCClassDecl) def).name.toString().equals(getBuilderClassName()))) { + return; + } + + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "DataManager"); + + addImports(); + makeBuilderClass(); + makeBuilderMethod(); + makeParameterizedBuilderMethod(); + process(); + } + + private void makeBuilderClass() { + builderClassDecl = treeMaker.ClassDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString(getBuilderClassName()), + List.nil(), + null, + List.nil(), + List.nil() + ); + + JCTree.JCVariableDecl dataManagerField = treeMaker.VarDef( + treeMaker.Modifiers(Flags.PRIVATE | Flags.FINAL), + names.fromString("dataManager"), + treeMaker.Ident(names.fromString("DataManager")), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(dataManagerField); + + JCTree.JCMethodDecl constructor = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC), + names.fromString(""), + null, + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("dataManager"), + treeMaker.Ident(names.fromString("DataManager")), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString("dataManager") + ), + treeMaker.Ident(names.fromString("dataManager")) + ) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(constructor); + + dataClassDecl.defs = dataClassDecl.defs.append(builderClassDecl); + } + + private void makeParameterizedBuilderMethod() { + JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString(builderMethodName), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Return( + treeMaker.Apply( + List.nil(), + treeMaker.Ident(names.fromString(builderMethodName)), + List.of( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("DataManager")), + names.fromString("getInstance") + ), + List.nil() + ) + ) + ) + ) + )), + null + ); + dataClassDecl.defs = dataClassDecl.defs.append(builderMethod); + } + + private void makeBuilderMethod() { + JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString(builderMethodName), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("dataManager"), + treeMaker.Ident(names.fromString("DataManager")), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Return( + treeMaker.NewClass(null, List.nil(), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.of( + treeMaker.Ident(names.fromString("dataManager")) + ), + null + ) + ) + )), + null + ); + dataClassDecl.defs = dataClassDecl.defs.append(builderMethod); + } + + public String getBuilderClassName() { + return dataClassDecl.name.toString() + builderClassSuffix; + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java index 7921a5cc..b7aa895e 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java @@ -10,21 +10,10 @@ import java.util.ArrayList; import java.util.Collection; -public class BuilderProcessor { //todo: abstract processor which has utility methods maybe - - private final JCTree.JCCompilationUnit compilationUnit; - private final TreeMaker treeMaker; - private final Names names; - private final JCTree.JCClassDecl dataClassDecl; - private final ParsedDataAnnotation dataAnnotation; - private JCTree.JCClassDecl builderClassDecl; +public class BuilderProcessor extends AbstractBuilderProcessor { public BuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation) { - this.compilationUnit = compilationUnit; - this.treeMaker = treeMaker; - this.names = names; - this.dataClassDecl = dataClassDecl; - this.dataAnnotation = dataAnnotation; + super(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation, "Builder", "builder"); } public static boolean hasProcessed(JCTree.JCClassDecl classDecl) { @@ -33,24 +22,18 @@ public static boolean hasProcessed(JCTree.JCClassDecl classDecl) { ((JCTree.JCClassDecl) def).name.toString().equals(classDecl.name + "Builder")); } - - public void process() { - if (hasProcessed(dataClassDecl)) { - return; - } - - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "DataManager"); + @Override + protected void addImports() { JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "ValueUtils"); JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.insert", "InsertContext"); JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "InsertMode"); JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "InsertStrategy"); JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "UniqueDataMetadata"); JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "ColumnValuePair"); + } - makeBuilderClass(); - makeBuilderMethod(); - makeParameterizedBuilderMethod(); - + @Override + protected void process() { Collection persistentValues = ParsedPersistentValue.extractPersistentValues(dataClassDecl, dataAnnotation, treeMaker, names); for (ParsedPersistentValue pv : persistentValues) { processValue(pv); @@ -66,124 +49,6 @@ public void process() { } - private void makeBuilderClass() { - builderClassDecl = treeMaker.ClassDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), - names.fromString(getBuilderClassName()), - List.nil(), - null, - List.nil(), - List.nil() - ); - - JCTree.JCVariableDecl dataManagerField = treeMaker.VarDef( - treeMaker.Modifiers(Flags.PRIVATE | Flags.FINAL), - names.fromString("dataManager"), - treeMaker.Ident(names.fromString("DataManager")), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(dataManagerField); - - JCTree.JCMethodDecl constructor = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC), - names.fromString(""), - null, - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("dataManager"), - treeMaker.Ident(names.fromString("DataManager")), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), - names.fromString("dataManager") - ), - treeMaker.Ident(names.fromString("dataManager")) - ) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(constructor); - - dataClassDecl.defs = dataClassDecl.defs.append(builderClassDecl); - } - - - private void makeParameterizedBuilderMethod() { - JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), - names.fromString("builder"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.nil(), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Return( - treeMaker.Apply( - List.nil(), - treeMaker.Ident(names.fromString("builder")), - List.of( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("DataManager")), - names.fromString("getInstance") - ), - List.nil() - ) - ) - ) - ) - )), - null - ); - dataClassDecl.defs = dataClassDecl.defs.append(builderMethod); - } - - private void makeBuilderMethod() { - JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), - names.fromString("builder"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("dataManager"), - treeMaker.Ident(names.fromString("DataManager")), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Return( - treeMaker.NewClass(null, List.nil(), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.of( - treeMaker.Ident(names.fromString("dataManager")) - ), - null - ) - ) - )), - null - ); - dataClassDecl.defs = dataClassDecl.defs.append(builderMethod); - } - - private String getBuilderClassName() { - return dataClassDecl.name.toString() + "Builder"; - } - - private void processValue(ParsedPersistentValue pv) { String schemaFieldName = pv.getFieldName() + "$schema"; String tableFieldName = pv.getFieldName() + "$table"; diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java index 70aeaadc..c97d9f13 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java @@ -350,9 +350,6 @@ private static void getFields(@NotNull Symbol.ClassSymbol classSymbol, @NotNull // Fallback to a simple identifier (covers type variables / wildcards minimally) return treeMaker.Ident(names.fromString(type.toString())); } - - - //todo: need to be able to extract the generic type } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java new file mode 100644 index 00000000..a3f1b5c7 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java @@ -0,0 +1,24 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.Names; + +public class QueryBuilderProcessor extends AbstractBuilderProcessor { + + + public QueryBuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation) { + super(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation, "QueryBuilder", "query"); + } + + @Override + protected void addImports() { + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "ValueUtils"); + + } + + @Override + protected void process() { + //todo: impl + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java index ef6aa693..556ac399 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java @@ -42,7 +42,8 @@ public Void visitClass(ClassTree node, Void unused) { if (!BuilderProcessor.hasProcessed(classDecl)) { ParsedDataAnnotation dataAnnotation = ParsedDataAnnotation.extract(classDecl); - new BuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation).process(); + new BuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation).runProcessor(); + new QueryBuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation).runProcessor(); } } From 9655fd1976f26277337efa76cfcca90b343bd1c7 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 1 Nov 2025 05:36:47 -0400 Subject: [PATCH 41/75] java-c plugin impl --- annotations/build.gradle | 25 - benchmark/build.gradle | 23 - core/build.gradle | 16 +- .../java/net/staticstudios/data/Column.java | 4 - .../java/net/staticstudios/data/Data.java | 1 - .../net/staticstudios/data/DataManager.java | 24 +- .../net/staticstudios/data/DefaultValue.java | 0 .../java/net/staticstudios/data/Delete.java | 0 .../staticstudios/data/DeleteStrategy.java | 0 .../net/staticstudios/data/ForeignColumn.java | 0 .../java/net/staticstudios/data/IdColumn.java | 0 .../java/net/staticstudios/data/Insert.java | 0 .../net/staticstudios/data/InsertMode.java | 0 .../staticstudios/data/InsertStrategy.java | 0 .../net/staticstudios/data/ManyToMany.java | 0 .../net/staticstudios/data/OneToMany.java | 0 .../java/net/staticstudios/data/OneToOne.java | 0 .../staticstudios/data/PersistentValue.java | 2 +- .../staticstudios/data/UpdateInterval.java | 2 +- .../staticstudios/data/UpdateStrategy.java | 0 .../PersistentManyToManyCollectionImpl.java | 2 +- .../PersistentOneToManyCollectionImpl.java | 2 +- .../data/impl/data/PersistentValueImpl.java | 4 +- .../data/impl/h2/H2DataAccessor.java | 2 +- .../h2/trigger/H2UpdateHandlerTrigger.java | 2 +- .../data/impl/pg/PostgresListener.java | 8 +- .../data/impl/pg/PostgresNotification.java | 4 +- .../data/insert/InsertContext.java | 8 +- .../staticstudios/data/parse/SQLBuilder.java | 32 +- .../staticstudios/data/parse/SQLSchema.java | 4 +- .../staticstudios/data/parse/SQLTable.java | 4 +- .../query/AbstractConditionalBuilder.java | 105 -- .../data/query/AbstractQueryBuilder.java | 163 --- .../data/query/BaseQueryBuilder.java | 98 ++ .../data/query/BaseQueryWhere.java | 179 +++ .../staticstudios/data/query/InnerJoin.java | 19 +- .../data/query/clause/AndClause.java | 16 +- .../data/query/clause/BetweenClause.java | 8 +- .../data/query/clause/CompositeClause.java | 4 - .../data/query/clause/ConditionalClause.java | 4 + .../data/query/clause/GreaterThanClause.java | 6 +- .../clause/GreaterThanOrEqualToClause.java | 6 +- .../data/query/clause/InClause.java | 4 - .../data/query/clause/LessThanClause.java | 6 +- .../query/clause/LessThanOrEqualToClause.java | 6 +- .../data/query/clause/NotBetweenClause.java | 25 + .../data/query/clause/NotInClause.java | 4 - .../data/query/clause/OrClause.java | 16 +- .../data/query/clause/ParenthesisClause.java | 19 - .../staticstudios/data/CustomTypeTest.java | 31 +- .../PersistentManyToManyCollectionTest.java | 7 +- .../PersistentOneToManyCollectionTest.java | 8 +- .../data/PersistentValueTest.java | 3 +- .../net/staticstudios/data/QueryTest.java | 109 +- .../net/staticstudios/data/ReferenceTest.java | 22 +- .../net/staticstudios/data/SQLParseTest.java | 24 +- .../net/staticstudios/data/misc/DataTest.java | 1 - .../data/mock/post/MockPost.java | 2 +- .../data/mock/post/MockPostMetadata.java | 2 +- .../data/mock/user/MockUser.java | 2 +- .../ide/intellij/DataPsiAugmentProvider.java | 6 +- .../ide/intellij/IntelliJPluginUtils.java | 12 + .../data/ide/intellij/SyntheticMethod.java | 5 + .../ide/intellij/query/QueryBuilderUtils.java | 16 +- .../data/ide/intellij/query/QueryClause.java | 6 +- .../query/clause/IsBetweenClause.java | 10 +- .../ide/intellij/query/clause/IsClause.java | 9 +- .../query/clause/IsGreaterThanClause.java | 7 +- .../clause/IsGreaterThanOrEqualToClause.java | 7 +- .../query/clause/IsInArrayClause.java | 47 + .../query/clause/IsInCollectionClause.java | 34 + .../query/clause/IsLessThanClause.java | 7 +- .../clause/IsLessThanOrEqualToClause.java | 7 +- .../intellij/query/clause/IsLikeClause.java | 11 +- .../query/clause/IsNotBetweenClause.java | 10 +- .../intellij/query/clause/IsNotClause.java | 9 +- .../query/clause/IsNotInArrayClause.java | 47 + .../query/clause/IsNotInCollectionClause.java | 34 + .../query/clause/IsNotLikeClause.java | 11 +- .../query/clause/IsNotNullClause.java | 9 +- .../intellij/query/clause/IsNullClause.java | 9 +- javac-plugin/build.gradle | 6 +- .../javac/AbstractBuilderProcessor.java | 209 ++- .../data/compiler/javac/BuilderProcessor.java | 52 +- .../data/compiler/javac/DummyProcessor.java | 21 + .../data/compiler/javac/JavaCPluginUtils.java | 28 + .../javac/ParsedForeignPersistentValue.java | 15 +- .../compiler/javac/ParsedPersistentValue.java | 36 +- .../data/compiler/javac/ParsedReference.java | 21 +- .../compiler/javac/QueryBuilderProcessor.java | 1122 ++++++++++++++++- .../compiler/javac/StaticDataJavacPlugin.java | 11 +- .../data/compiler/javac/SuperClass.java | 7 + .../javax.annotation.processing.Processor | 1 + processor/build.gradle | 31 - .../data/processor/DataProcessor.java | 158 --- .../data/processor/ForeignLink.java | 4 - .../ForeignPersistentValueMetadata.java | 25 - .../data/processor/Metadata.java | 4 - .../data/processor/MetadataUtils.java | 151 --- .../processor/PersistentValueMetadata.java | 47 - .../data/processor/QueryFactory.java | 323 ----- .../processor/SchemaTableColumnStatics.java | 28 - .../gradle/incremental.annotation.processors | 2 - .../javax.annotation.processing.Processor | 1 - settings.gradle | 2 - .../staticstudios/data/utils/StringUtils.java | 7 + 106 files changed, 2166 insertions(+), 1527 deletions(-) delete mode 100644 annotations/build.gradle rename {annotations => core}/src/main/java/net/staticstudios/data/Column.java (86%) rename {annotations => core}/src/main/java/net/staticstudios/data/Data.java (73%) rename {annotations => core}/src/main/java/net/staticstudios/data/DefaultValue.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/Delete.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/DeleteStrategy.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/ForeignColumn.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/IdColumn.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/Insert.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/InsertMode.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/InsertStrategy.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/ManyToMany.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/OneToMany.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/OneToOne.java (100%) rename {annotations => core}/src/main/java/net/staticstudios/data/UpdateInterval.java (88%) rename {annotations => core}/src/main/java/net/staticstudios/data/UpdateStrategy.java (100%) delete mode 100644 core/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java delete mode 100644 core/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java create mode 100644 core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java create mode 100644 core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java delete mode 100644 core/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/ConditionalClause.java create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/NotBetweenClause.java delete mode 100644 core/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsInArrayClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsInCollectionClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotInArrayClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotInCollectionClause.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/DummyProcessor.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/SuperClass.java create mode 100644 javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor delete mode 100644 processor/build.gradle delete mode 100644 processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java delete mode 100644 processor/src/main/java/net/staticstudios/data/processor/ForeignLink.java delete mode 100644 processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java delete mode 100644 processor/src/main/java/net/staticstudios/data/processor/Metadata.java delete mode 100644 processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java delete mode 100644 processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java delete mode 100644 processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java delete mode 100644 processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java delete mode 100644 processor/src/main/resources/META-INF/gradle/incremental.annotation.processors delete mode 100644 processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/annotations/build.gradle b/annotations/build.gradle deleted file mode 100644 index 75526fba..00000000 --- a/annotations/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - id 'java' -} - -group = 'net.staticstudios' -version = '3.0.0-SNAPSHOT' - -repositories { - mavenCentral() -} - -dependencies { - testImplementation platform('org.junit:junit-bom:5.10.0') - testImplementation 'org.junit.jupiter:junit-jupiter' -} - -test { - useJUnitPlatform() -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} diff --git a/benchmark/build.gradle b/benchmark/build.gradle index 38b0b6b8..a2509730 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -19,29 +19,6 @@ dependencies { implementation("org.testcontainers:postgresql:1.19.8") implementation("com.redis:testcontainers-redis:2.2.2") implementation("org.slf4j:slf4j-log4j12:2.0.16") - - annotationProcessor project(":processor") - compileOnly project(":processor") -} - -def generatedSourcesDir = file("$buildDir/generated/sources/annotations") -sourceSets.main.java.srcDir(generatedSourcesDir) - - -tasks.register('runAnnotationProcessor', JavaCompile) { - group = 'build' - description = 'Run annotation processor manually (codegen only)' - - source = sourceSets.main.java - classpath = configurations.compileClasspath + configurations.annotationProcessor - destinationDir = file("$buildDir/tmp/classes/manual") // arbitrary, ignored with -proc:only - - options.annotationProcessorPath = configurations.annotationProcessor - options.compilerArgs += [ - '-proc:only', - '-processor', 'net.staticstudios.data.processor.DataProcessor', - '-s', generatedSourcesDir - ] } tasks.named('jmhRunBytecodeGenerator') { diff --git a/core/build.gradle b/core/build.gradle index 9b2f333f..18b423a7 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -23,14 +23,14 @@ dependencies { implementation 'net.staticstudios:static-utils:1.0.6-SNAPSHOT' implementation 'com.h2database:h2:2.3.232' implementation 'org.jetbrains:annotations:24.0.1' - api project(":annotations") - implementation project(":annotations") - annotationProcessor project(":processor") - compileOnly project(":processor") - - - testAnnotationProcessor project(":processor") - testCompileOnly project(":processor") +// api project(":annotations") +// implementation project(":annotations") +// annotationProcessor project(":processor") +// compileOnly project(":processor") + +// +// testAnnotationProcessor project(":processor") +// testCompileOnly project(":processor") // testImplementation(platform('org.junit:junit-bom:5.10.3')) // testImplementation('org.junit.jupiter:junit-jupiter') diff --git a/annotations/src/main/java/net/staticstudios/data/Column.java b/core/src/main/java/net/staticstudios/data/Column.java similarity index 86% rename from annotations/src/main/java/net/staticstudios/data/Column.java rename to core/src/main/java/net/staticstudios/data/Column.java index 07c90c20..6045f880 100644 --- a/annotations/src/main/java/net/staticstudios/data/Column.java +++ b/core/src/main/java/net/staticstudios/data/Column.java @@ -10,10 +10,6 @@ public @interface Column { String name(); - String schema() default ""; - - String table() default ""; - boolean index() default false; boolean nullable() default false; diff --git a/annotations/src/main/java/net/staticstudios/data/Data.java b/core/src/main/java/net/staticstudios/data/Data.java similarity index 73% rename from annotations/src/main/java/net/staticstudios/data/Data.java rename to core/src/main/java/net/staticstudios/data/Data.java index 457aeefc..834cb1e2 100644 --- a/annotations/src/main/java/net/staticstudios/data/Data.java +++ b/core/src/main/java/net/staticstudios/data/Data.java @@ -1,5 +1,4 @@ package net.staticstudios.data; -//todo: the annotations package can be removed and everything can be put back into core once the processor is gone import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index f13adba6..a78fd2f0 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -123,7 +123,7 @@ public void callUpdateHandlers(List columnNames, String schema, String t } } if (!found) { - throw new IllegalArgumentException("Not all ID columns were provided for UniqueData class " + holderClass.getName() + ". Required: " + metadata.idColumns() + ", Provided: " + columnNames); + throw new IllegalArgumentException("Not all ID columnsInReferringTable were provided for UniqueData class " + holderClass.getName() + ". Required: " + metadata.idColumns() + ", Provided: " + columnNames); } } UniqueData instance = getInstance(holderClass, idColumns); @@ -244,7 +244,7 @@ public void handleDelete(List columnNames, String schema, String table, break; } } - Preconditions.checkArgument(found, "Not all ID columns were provided for UniqueData class %s. Required: %s, Provided: %s", uniqueDataMetadata.clazz().getName(), uniqueDataMetadata.idColumns(), Arrays.toString(values)); + Preconditions.checkArgument(found, "Not all ID columnsInReferringTable were provided for UniqueData class %s. Required: %s, Provided: %s", uniqueDataMetadata.clazz().getName(), uniqueDataMetadata.idColumns(), Arrays.toString(values)); } UniqueData instance = uniqueDataInstanceCache.getOrDefault(uniqueDataMetadata.clazz(), Collections.emptyMap()).get(new ColumnValuePairs(idColumns)); @@ -286,10 +286,10 @@ public synchronized void updateIdColumns(List columnNames, String schema break; } } - Preconditions.checkArgument(found, "Not all ID columns were provided for UniqueData class %s. Required: %s, Provided: %s", uniqueDataMetadata.clazz().getName(), uniqueDataMetadata.idColumns(), Arrays.toString(oldValues)); + Preconditions.checkArgument(found, "Not all ID columnsInReferringTable were provided for UniqueData class %s. Required: %s, Provided: %s", uniqueDataMetadata.clazz().getName(), uniqueDataMetadata.idColumns(), Arrays.toString(oldValues)); } if (Arrays.equals(oldIdColumns, newIdColumns)) { - return; // no change to id columns here + return; // no change to id columnsInReferringTable here } ColumnValuePairs oldIdCols = new ColumnValuePairs(oldIdColumns); @@ -364,11 +364,11 @@ public T getInstance(Class clazz, ColumnValuePair... i Preconditions.checkNotNull(providedIdColumn.value(), "ID name value for name %s in UniqueData class %s cannot be null", providedIdColumn.column(), clazz.getName()); } - Preconditions.checkArgument(hasAllIdColumns, "Not all @IdColumn columns were provided for UniqueData class %s. Required: %s, Provided: %s", clazz.getName(), metadata.idColumns(), idColumns); + Preconditions.checkArgument(hasAllIdColumns, "Not all @IdColumn columnsInReferringTable were provided for UniqueData class %s. Required: %s, Provided: %s", clazz.getName(), metadata.idColumns(), idColumns); T instance; if (uniqueDataInstanceCache.containsKey(clazz) && uniqueDataInstanceCache.get(clazz).containsKey(idColumns)) { - logger.trace("Cache hit for UniqueData class {} with ID columns {}", clazz.getName(), idColumns); + logger.trace("Cache hit for UniqueData class {} with ID columnsInReferringTable {}", clazz.getName(), idColumns); instance = (T) uniqueDataInstanceCache.get(clazz).get(idColumns); if (instance.isDeleted()) { return null; @@ -414,7 +414,7 @@ public T getInstance(Class clazz, ColumnValuePair... i uniqueDataInstanceCache.computeIfAbsent(clazz, k -> new MapMaker().weakValues().makeMap()) .put(idColumns, instance); - logger.trace("Cache miss for UniqueData class {} with ID columns {}. Created new instance.", clazz.getName(), idColumns); + logger.trace("Cache miss for UniqueData class {} with ID columnsInReferringTable {}. Created new instance.", clazz.getName(), idColumns); return instance; } @@ -478,13 +478,13 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { } } - tables.clear(); // rebuild the table set in case we added any new tables from foreign keys + tables.clear(); // rebuild the referringTable set in case we added any new tables from foreign keys insertContext.getEntries().forEach((simpleColumnMetadata, o) -> { SQLTable table = Objects.requireNonNull(sqlBuilder.getSchema(simpleColumnMetadata.schema())).getTable(simpleColumnMetadata.table()); tables.add(table); }); - // Build dependency graph: table -> set of tables it depends on + // Build dependency graph: referringTable -> set of tables it depends on Map> dependencyGraph = new HashMap<>(); for (SQLTable table : tables) { Set dependsOn = new HashSet<>(); @@ -493,7 +493,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(fKey.getReferencedTable())); boolean addDependency = true; - // if one of the linking columns is not present in the insert context, we can't add the dependency + // if one of the linking columnsInReferringTable is not present in the insert context, we can't add the dependency for (Link link : fKey.getLinkingColumns()) { Object value = insertContext.getEntries().entrySet().stream() .filter(entry -> { @@ -525,7 +525,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { Set stack = new HashSet<>(); for (SQLTable table : tables) { if (hasCycle(table, dependencyGraph, visited, stack)) { - throw new IllegalStateException(String.format("Cycle detected in foreign key dependencies involving table %s.%s", table.getSchema().getName(), table.getName())); + throw new IllegalStateException(String.format("Cycle detected in foreign key dependencies involving referringTable %s.%s", table.getSchema().getName(), table.getName())); } } @@ -832,7 +832,7 @@ public Class getSerializedType(Class clazz) { } // /** -// * For internal use only. A dummy instance has no DataManager, no id columns, and is marked as deleted. +// * For internal use only. A dummy instance has no DataManager, no id columnsInReferringTable, and is marked as deleted. // * // * @param clazz The UniqueData class to create a dummy instance of. // * @param The type of UniqueData. diff --git a/annotations/src/main/java/net/staticstudios/data/DefaultValue.java b/core/src/main/java/net/staticstudios/data/DefaultValue.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/DefaultValue.java rename to core/src/main/java/net/staticstudios/data/DefaultValue.java diff --git a/annotations/src/main/java/net/staticstudios/data/Delete.java b/core/src/main/java/net/staticstudios/data/Delete.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/Delete.java rename to core/src/main/java/net/staticstudios/data/Delete.java diff --git a/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java b/core/src/main/java/net/staticstudios/data/DeleteStrategy.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java rename to core/src/main/java/net/staticstudios/data/DeleteStrategy.java diff --git a/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java b/core/src/main/java/net/staticstudios/data/ForeignColumn.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/ForeignColumn.java rename to core/src/main/java/net/staticstudios/data/ForeignColumn.java diff --git a/annotations/src/main/java/net/staticstudios/data/IdColumn.java b/core/src/main/java/net/staticstudios/data/IdColumn.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/IdColumn.java rename to core/src/main/java/net/staticstudios/data/IdColumn.java diff --git a/annotations/src/main/java/net/staticstudios/data/Insert.java b/core/src/main/java/net/staticstudios/data/Insert.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/Insert.java rename to core/src/main/java/net/staticstudios/data/Insert.java diff --git a/annotations/src/main/java/net/staticstudios/data/InsertMode.java b/core/src/main/java/net/staticstudios/data/InsertMode.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/InsertMode.java rename to core/src/main/java/net/staticstudios/data/InsertMode.java diff --git a/annotations/src/main/java/net/staticstudios/data/InsertStrategy.java b/core/src/main/java/net/staticstudios/data/InsertStrategy.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/InsertStrategy.java rename to core/src/main/java/net/staticstudios/data/InsertStrategy.java diff --git a/annotations/src/main/java/net/staticstudios/data/ManyToMany.java b/core/src/main/java/net/staticstudios/data/ManyToMany.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/ManyToMany.java rename to core/src/main/java/net/staticstudios/data/ManyToMany.java diff --git a/annotations/src/main/java/net/staticstudios/data/OneToMany.java b/core/src/main/java/net/staticstudios/data/OneToMany.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/OneToMany.java rename to core/src/main/java/net/staticstudios/data/OneToMany.java diff --git a/annotations/src/main/java/net/staticstudios/data/OneToOne.java b/core/src/main/java/net/staticstudios/data/OneToOne.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/OneToOne.java rename to core/src/main/java/net/staticstudios/data/OneToOne.java diff --git a/core/src/main/java/net/staticstudios/data/PersistentValue.java b/core/src/main/java/net/staticstudios/data/PersistentValue.java index 27cd54c6..0687bf0d 100644 --- a/core/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/core/src/main/java/net/staticstudios/data/PersistentValue.java @@ -11,7 +11,7 @@ import java.util.List; /** - * A persistent value represents a single cell in a database table. + * A persistent value represents a single cell in a database referringTable. * * @param */ diff --git a/annotations/src/main/java/net/staticstudios/data/UpdateInterval.java b/core/src/main/java/net/staticstudios/data/UpdateInterval.java similarity index 88% rename from annotations/src/main/java/net/staticstudios/data/UpdateInterval.java rename to core/src/main/java/net/staticstudios/data/UpdateInterval.java index ff10c23c..3908c178 100644 --- a/annotations/src/main/java/net/staticstudios/data/UpdateInterval.java +++ b/core/src/main/java/net/staticstudios/data/UpdateInterval.java @@ -6,7 +6,7 @@ import java.lang.annotation.Target; /** - * This annotation is only applicable to {@link net.staticstudios.data.PersistentValue}s. + * This annotation is only applicable to {@link PersistentValue}s. * Instead of propagating updates to the real database instantly, only the latest update will be made every Nms. * This is useful for values that update very frequently, since this could otherwise overwhelm the task queue. */ diff --git a/annotations/src/main/java/net/staticstudios/data/UpdateStrategy.java b/core/src/main/java/net/staticstudios/data/UpdateStrategy.java similarity index 100% rename from annotations/src/main/java/net/staticstudios/data/UpdateStrategy.java rename to core/src/main/java/net/staticstudios/data/UpdateStrategy.java 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 d5132e6f..77e698f4 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 @@ -20,7 +20,7 @@ public class PersistentManyToManyCollectionImpl implements private final Class type; private final String parsedJoinTableSchema; private final String parsedJoinTableName; - private final String links; // since we need information about the column prefixes in the join table, we have to compute these at runtime + private final String links; // since we need information about the column prefixes in the join referringTable, we have to compute these at runtime private @Nullable List cachedJoinTableToDataTableLinks = null; private @Nullable List cachedJoinTableToReferencedTableLinks = null; 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 c469e1d9..56476a3e 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 @@ -412,7 +412,7 @@ private SQLTransaction.Statement buildClearStatement() { } private Set getIds() { - // note: we need the join since we support linking on non-id columns + // 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(); 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 9c627bd3..e2d1e52a 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 @@ -84,8 +84,8 @@ public static PersistentValueMetadata extractMetadata(Str } if (columnAnnotation != null) { ColumnMetadata columnMetadata = new ColumnMetadata( - columnAnnotation.schema().isEmpty() ? schema : ValueUtils.parseValue(columnAnnotation.schema()), - columnAnnotation.table().isEmpty() ? table : ValueUtils.parseValue(columnAnnotation.table()), + schema, + table, ValueUtils.parseValue(columnAnnotation.name()), ReflectionUtils.getGenericType(field), columnAnnotation.nullable(), 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 bbcc584c..0144dae2 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 @@ -461,7 +461,7 @@ private synchronized void updateKnownTables() throws SQLException { currentTables.add(schema + "." + table); if (!knownTables.contains(schema + "." + table)) { - logger.trace("Discovered new table {}.{}", schema, table); + logger.trace("Discovered new referringTable {}.{}", schema, table); UUID randomId = UUID.randomUUID(); @Language("SQL") String sql = "CREATE TRIGGER IF NOT EXISTS \"trg_%s_%s\" AFTER INSERT, UPDATE, DELETE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; 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 314f0b60..a64f07ba 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 @@ -27,7 +27,7 @@ public static void registerDataManager(UUID id, DataManager 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 table name since it might be a copy for an internal table (very odd behavior i must say h2) + 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.schema = schemaName; } diff --git a/core/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java index c0cfffd2..59188a48 100644 --- a/core/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java +++ b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java @@ -167,11 +167,11 @@ public void addHandler(Consumer handler) { /** - * Whenever we see a new table, make sure the trigger is added to it + * Whenever we see a new referringTable, make sure the trigger is added to it * * @param connection the connection to the database - * @param schema the schema of the table - * @param table the table to ensure has the trigger + * @param schema the referringSchema of the referringTable + * @param table the referringTable to ensure has the trigger */ public void ensureTableHasTrigger(Connection connection, String schema, String table) { String schemaTable = schema + "." + table; @@ -180,7 +180,7 @@ public void ensureTableHasTrigger(Connection connection, String schema, String t } String sql = CREATE_TRIGGER.formatted(schemaTable, schemaTable); - logger.debug("Adding propagate_data_update_trigger to table: {}", schemaTable); + logger.debug("Adding propagate_data_update_trigger to referringTable: {}", schemaTable); try (Statement statement = connection.createStatement()) { statement.execute(sql); diff --git a/core/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java index 0571efc9..914e523e 100644 --- a/core/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java +++ b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java @@ -41,8 +41,8 @@ public PostgresData getData() { public String toString() { return "PostgresNotification{" + "instant=" + instant + - ", schema='" + schema + '\'' + - ", table='" + table + '\'' + + ", referringSchema='" + schema + '\'' + + ", referringTable='" + table + '\'' + ", operation=" + operation + ", data=" + data + '}'; diff --git a/core/src/main/java/net/staticstudios/data/insert/InsertContext.java b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java index 29bdb402..15e54549 100644 --- a/core/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -35,7 +35,7 @@ public InsertContext set(String schema, String table, String column, @Nullable O SQLTable sqlTable = sqlSchema.getTable(table); Preconditions.checkNotNull(sqlTable, "Table not found: " + table); SQLColumn sqlColumn = sqlTable.getColumn(column); - Preconditions.checkNotNull(sqlColumn, "Column not found: " + column + " in table: " + table + " schema: " + schema); + Preconditions.checkNotNull(sqlColumn, "Column not found: " + column + " in referringTable: " + table + " referringSchema: " + schema); SimpleColumnMetadata columnMetadata = new SimpleColumnMetadata( schema, @@ -54,7 +54,7 @@ public InsertContext set(String schema, String table, String column, @Nullable O insertStrategies.put(columnMetadata, insertStrategy); } - Preconditions.checkArgument(sqlColumn.getType().isAssignableFrom(dataManager.getSerializedType(value.getClass())), "Value type mismatch for name " + column + " in table " + table + " schema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); + Preconditions.checkArgument(sqlColumn.getType().isAssignableFrom(dataManager.getSerializedType(value.getClass())), "Value type mismatch for name " + column + " in referringTable " + table + " referringSchema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); entries.put(columnMetadata, dataManager.serialize(value)); return this; @@ -78,7 +78,7 @@ public InsertContext insert(InsertMode insertMode) { } /** - * Retrieves an instance of the specified UniqueData class based on the ID columns set in this InsertContext. + * Retrieves an instance of the specified UniqueData class based on the ID columnsInReferringTable set in this InsertContext. * * @param holderClass The class of the UniqueData to retrieve. * @param The type of UniqueData. @@ -97,7 +97,7 @@ public T get(Class holderClass) { Objects.equals(entry.table(), idColumn.table()) && Objects.equals(entry.name(), idColumn.name()))); - Preconditions.checkState(insertedAllIdColumns, "The requested class was not inserted. Class: " + holderClass.getName() + " is missing one or more ID name values. Required ID columns: " + metadata.idColumns()); + Preconditions.checkState(insertedAllIdColumns, "The requested class was not inserted. Class: " + holderClass.getName() + " is missing one or more ID name values. Required ID columnsInReferringTable: " + metadata.idColumns()); ColumnValuePair[] idColumnValues = new ColumnValuePair[metadata.idColumns().size()]; for (int i = 0; i < metadata.idColumns().size(); i++) { idColumnValues[i] = new ColumnValuePair(metadata.idColumns().get(i).name(), entries.get(new SimpleColumnMetadata(metadata.idColumns().get(i)))); 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 f9984dde..55b724fd 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -86,7 +86,7 @@ public List parse(Class clazz) { for (SQLColumn newColumn : newTable.getColumns()) { SQLColumn existingColumn = existingTable.getColumn(newColumn.getName()); if (existingColumn != null) { - Preconditions.checkState(existingColumn.equals(newColumn), "Column " + newColumn.getName() + " in table " + newTable.getName() + " has conflicting definitions! Existing: " + existingColumn + ", New: " + newColumn); + Preconditions.checkState(existingColumn.equals(newColumn), "Column " + newColumn.getName() + " in referringTable " + newTable.getName() + " has conflicting definitions! Existing: " + existingColumn + ", New: " + newColumn); continue; } existingTable.addColumn(newColumn); @@ -169,7 +169,7 @@ private List getDefs(Collection schemas) { } } - // define fkeys after table creation, to ensure all tables exist before adding fkeys + // define fkeys after referringTable creation, to ensure all tables exist before adding fkeys for (SQLSchema schema : schemas) { //todo: what if an fkey's on delete/ on cascade strategy has changed? for (SQLTable table : schema.getTables()) { for (ForeignKey foreignKey : table.getForeignKeys()) { @@ -256,7 +256,7 @@ private void walk(Class clazz, Set clazz, Map schemas) { - logger.trace("Parsing columns for class {}", clazz.getName()); + logger.trace("Parsing columnsInReferringTable 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"); @@ -318,8 +318,8 @@ private void parseColumn(Class clazz, Map clazz, Map clazz, Map clazz, Map clazz, Map clazz, Map c.name().equals(dataLink.columnInReferencedTable())) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Column not found in data table! " + dataLink.columnInReferringTable())); + .orElseThrow(() -> new IllegalArgumentException("Column not found in data referringTable! " + dataLink.columnInReferringTable())); joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, dataTableColumnPrefix + "_" + columnMetadata.name(), columnMetadata.type(), false, false, "")); } for (Link referencedLink : joinTableToReferencedTableLinks) { ColumnMetadata columnMetadata = referencedMetadata.idColumns().stream() .filter(c -> c.name().equals(referencedLink.columnInReferencedTable())) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Column not found in referenced table! " + referencedLink.columnInReferringTable())); + .orElseThrow(() -> new IllegalArgumentException("Column not found in referenced referringTable! " + referencedLink.columnInReferringTable())); joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, referencedTableColumnPrefix + "_" + columnMetadata.name(), columnMetadata.type(), false, false, "")); } joinTable = new SQLTable(joinSchema, joinTableName, joinTableIdColumns); @@ -603,7 +603,7 @@ private void parseManyToManyPersistentCollection(ManyToMany manyToMany, Class getTables() { public void addTable(SQLTable table) { if (table.getSchema() != this) { - throw new IllegalArgumentException("Table does not belong to this schema"); + throw new IllegalArgumentException("Table does not belong to this referringSchema"); } if (tables.containsKey(table.getName())) { - throw new IllegalArgumentException("Table with name " + table.getName() + " already exists in schema " + name); + throw new IllegalArgumentException("Table with name " + table.getName() + " already exists in referringSchema " + name); } tables.put(table.getName(), table); diff --git a/core/src/main/java/net/staticstudios/data/parse/SQLTable.java b/core/src/main/java/net/staticstudios/data/parse/SQLTable.java index c6c62dc0..25bb807b 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLTable.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -75,12 +75,12 @@ public List getIdColumns() { public void addColumn(SQLColumn column) { if (column.getTable() != this) { - throw new IllegalArgumentException("Column does not belong to this table"); + throw new IllegalArgumentException("Column does not belong to this referringTable"); } Preconditions.checkNotNull(column, "Column cannot be null"); SQLColumn existingColumn = columns.get(column.getName()); if (existingColumn != null && !Objects.equals(existingColumn, column)) { - throw new IllegalArgumentException("Column with name " + column.getName() + " already exists in table " + name + " in schema " + schema.getName() + " and is different from the one being added"); + throw new IllegalArgumentException("Column with name " + column.getName() + " already exists in referringTable " + name + " in referringSchema " + schema.getName() + " and is different from the one being added"); } columns.put(column.getName(), column); diff --git a/core/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java b/core/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java deleted file mode 100644 index 42ae745b..00000000 --- a/core/src/main/java/net/staticstudios/data/query/AbstractConditionalBuilder.java +++ /dev/null @@ -1,105 +0,0 @@ -package net.staticstudios.data.query; - -import com.google.common.base.Preconditions; -import net.staticstudios.data.Order; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.query.clause.AndClause; -import net.staticstudios.data.query.clause.OrClause; -import net.staticstudios.data.query.clause.ParenthesisClause; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.function.Consumer; - -public abstract class AbstractConditionalBuilder, C extends AbstractConditionalBuilder, T extends UniqueData> implements QueryLike { - protected final AbstractQueryBuilder queryBuilder; - - public AbstractConditionalBuilder(AbstractQueryBuilder queryBuilder) { - this.queryBuilder = queryBuilder; - } - - /** - * AND ... - */ - public Q and() { - queryBuilder.state = AbstractQueryBuilder.State.AND; - return queryBuilder.self(); - } - - /** - * OR ... - */ - public Q or() { - queryBuilder.state = AbstractQueryBuilder.State.OR; - return queryBuilder.self(); - } - - /** - * OR (...) - */ - public Q or(Consumer conditional) { - AbstractQueryBuilder inner = queryBuilder.createInstance(); - inner.temp = true; - conditional.accept(inner.self()); - Preconditions.checkState(inner.state == AbstractQueryBuilder.State.NONE, "Clause not completed"); - - queryBuilder.set(new OrClause(queryBuilder.clause, new ParenthesisClause(inner.clause))); - - return queryBuilder.self(); - } - - /** - * AND (...) - */ - public Q and(Consumer conditional) { - AbstractQueryBuilder inner = queryBuilder.createInstance(); - inner.temp = true; - conditional.accept(inner.self()); - Preconditions.checkState(inner.state == AbstractQueryBuilder.State.NONE, "Clause not completed"); - - queryBuilder.set(new AndClause(queryBuilder.clause, new ParenthesisClause(inner.clause))); - - return queryBuilder.self(); - } - - @SuppressWarnings("unchecked") - private C self() { - return (C) this; - } - - @Override - public C limit(int limit) { - queryBuilder.limit(limit); - return self(); - } - - @Override - public C offset(int offset) { - queryBuilder.offset(offset); - return self(); - } - - @Override - public @Nullable T findOne() { - return queryBuilder.findOne(); - } - - @Override - public @NotNull List findAll() { - return queryBuilder.findAll(); - } - - protected void orderBy(String schema, String table, String column, Order order) { - queryBuilder.orderBy(schema, table, column, order); - } - - protected void innerJoin(String schema, String table, String[] columns, String foreignSchema, String foreignTable, String[] foreignColumns) { - queryBuilder.innerJoin(schema, table, columns, foreignSchema, foreignTable, foreignColumns); - } - - @Override - public String toString() { - return queryBuilder.toString(); - } -} \ No newline at end of file diff --git a/core/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java b/core/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java deleted file mode 100644 index a7aae215..00000000 --- a/core/src/main/java/net/staticstudios/data/query/AbstractQueryBuilder.java +++ /dev/null @@ -1,163 +0,0 @@ -package net.staticstudios.data.query; - -import com.google.common.base.Preconditions; -import net.staticstudios.data.DataManager; -import net.staticstudios.data.Order; -import net.staticstudios.data.UniqueData; -import net.staticstudios.data.query.clause.AndClause; -import net.staticstudios.data.query.clause.Clause; -import net.staticstudios.data.query.clause.OrClause; -import net.staticstudios.data.query.clause.ValueClause; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public abstract class AbstractQueryBuilder, - C extends AbstractConditionalBuilder, - T extends UniqueData> - implements QueryLike { - protected final DataManager dataManager; - private final Class type; - protected boolean temp = false; - protected State state = State.NONE; - protected Clause clause = null; - private int limit = -1; - private int offset = -1; - private String orderBySchema; - private String orderByTable; - private String orderByColumn; - private Order order = Order.ASCENDING; - private Set innerJoins = new HashSet<>(); - - protected AbstractQueryBuilder(DataManager dataManager, Class type) { - this.dataManager = dataManager; - this.type = type; - } - //todo: there needs to be a way to support WHERE ( x or Y) ... - // right now you can only do WHERE x AND (y OR z) ... the first clause cannot be a parenthesis clause - - public Q and() { - state = State.AND; - return self(); - } - - public Q or() { - state = State.OR; - return self(); - } - - private ComputedClause compute(int limit, int offset) { - Preconditions.checkState(!temp, "Cannot call compute on a temporary query builder"); - Preconditions.checkNotNull(clause, "No clause defined"); - StringBuilder sb = new StringBuilder(); - for (InnerJoin join : innerJoins) { - sb.append("INNER JOIN \"").append(join.foreignSchema()).append("\".\"").append(join.foreignTable()).append("\" ON "); - for (int i = 0; i < join.columns().length; i++) { - sb.append("\"").append(join.schema()).append("\".\"").append(join.table()).append("\".\"").append(join.columns()[i]).append("\" = \"") - .append(join.foreignSchema()).append("\".\"").append(join.foreignTable()).append("\".\"").append(join.foreignColumns()[i]).append("\""); - if (i < join.columns().length - 1) { - sb.append(" AND "); - } - } - sb.append(" "); - } - sb.append("WHERE "); - List parameters = clause.append(sb); - if (limit > 0) { - sb.append(" LIMIT ").append(limit); - } - if (offset > 0) { - sb.append(" OFFSET ").append(offset); - } - if (orderByColumn != null) { - sb.append(" ORDER BY \"").append(orderBySchema).append("\".\"").append(orderByTable).append("\".\"").append(orderByColumn).append("\" ").append(order == Order.ASCENDING ? "ASC" : "DESC"); - } - return new ComputedClause(sb.toString(), parameters); - } - - public String toString() { - return compute(limit, offset).sql(); - } - - protected void orderBy(String schema, String table, String column, Order order) { - this.orderBySchema = schema; - this.orderByTable = table; - this.orderByColumn = column; - this.order = order; - } - - @SuppressWarnings("unchecked") - protected Q self() { - return (Q) this; - } - - @Override - public @Nullable T findOne() { - ComputedClause computed = compute(1, -1); - List result = dataManager.query(type, computed.sql(), computed.parameters()); - if (result.isEmpty()) { - return null; - } - return result.getFirst(); - } - - @Override - public @NotNull List findAll() { - ComputedClause computed = compute(limit, offset); - return dataManager.query(type, computed.sql(), computed.parameters()); - } - - @Override - public Q limit(int limit) { - this.limit = limit; - return self(); - } - - @Override - public Q offset(int offset) { - this.offset = offset; - return self(); - } - - protected void innerJoin(String schema, String table, String[] columns, String foreignSchema, String foreignTable, String[] foreignColumns) { - innerJoins.add(new InnerJoin(schema, table, columns, foreignSchema, foreignTable, foreignColumns)); - } - - protected C set(Clause clause) { - switch (state) { - case NONE -> this.clause = clause; - case AND -> { - if (!(clause instanceof ValueClause valueClause)) { - throw new IllegalStateException("AND clause must be a ValueClause"); - } - this.clause = new AndClause(this.clause, valueClause); - } - case OR -> { - if (!(clause instanceof ValueClause valueClause)) { - throw new IllegalStateException("OR clause must be a ValueClause"); - } - this.clause = new OrClause(this.clause, valueClause); - } - } - - state = State.NONE; - return createConditionalInstance(); - } - - protected abstract AbstractQueryBuilder createInstance(); - - protected abstract C createConditionalInstance(); - - protected enum State { - NONE, - AND, - OR - } - - record ComputedClause(String sql, List parameters) { - } - -} diff --git a/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java b/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java new file mode 100644 index 00000000..fb90b478 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java @@ -0,0 +1,98 @@ +package net.staticstudios.data.query; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.Order; +import net.staticstudios.data.UniqueData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("unused") +public abstract class BaseQueryBuilder { + protected final DataManager dataManager; + protected final Class type; + protected final W where; + private String orderBySchema = null; + private String orderByTable = null; + private String orderByColumn = null; + private Order order = null; + private int limit = -1; + private int offset = -1; + + protected BaseQueryBuilder(DataManager dataManager, Class type, W where) { + this.dataManager = dataManager; + this.type = type; + this.where = where; + } + + protected void setOrderBy(String schema, String table, String column, Order order) { + this.orderBySchema = schema; + this.orderByTable = table; + this.orderByColumn = column; + this.order = order; + } + + protected void setLimit(int limit) { + this.limit = limit; + } + + protected void setOffset(int offset) { + this.offset = offset; + } + + public @Nullable T findOne() { + ComputedClause computed = compute(); + List result = dataManager.query(type, computed.sql(), computed.parameters()); + if (result.isEmpty()) { + return null; + } + return result.getFirst(); + } + + public @NotNull List findAll() { //todo: in the IJ plugin make sure the annotations are present for find all and fine one (nullable and notnull) + ComputedClause computed = compute(); + return dataManager.query(type, computed.sql(), computed.parameters()); + } + + + private ComputedClause compute() { + Preconditions.checkState(!where.isEmpty(), "No clause defined"); + StringBuilder sb = new StringBuilder(); + for (InnerJoin join : where.getInnerJoins()) { + sb.append("INNER JOIN \"").append(join.referencedSchema()).append("\".\"").append(join.referencedTable()).append("\" ON "); + for (int i = 0; i < join.columnsInReferringTable().length; i++) { + sb.append("\"").append(join.referringSchema()).append("\".\"").append(join.referringTable()).append("\".\"").append(join.columnsInReferringTable()[i]).append("\" = \"") + .append(join.referencedSchema()).append("\".\"").append(join.referencedTable()).append("\".\"").append(join.columnsInReferencedTable()[i]).append("\""); + if (i < join.columnsInReferringTable().length - 1) { + sb.append(" AND "); + } + } + sb.append(" "); + } + sb.append("WHERE "); + + List parameters = new ArrayList<>(); + where.buildWhereClause(sb, parameters); + if (limit > 0) { + sb.append(" LIMIT ").append(limit); + } + if (offset > 0) { + sb.append(" OFFSET ").append(offset); + } + if (orderByColumn != null) { + sb.append(" ORDER BY \"").append(orderBySchema).append("\".\"").append(orderByTable).append("\".\"").append(orderByColumn).append("\" ").append(order == Order.ASCENDING ? "ASC" : "DESC"); + } + return new ComputedClause(sb.toString(), parameters); + } + + @Override + public String toString() { + return compute().sql(); + } + + record ComputedClause(String sql, List parameters) { + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java new file mode 100644 index 00000000..cba28420 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java @@ -0,0 +1,179 @@ +package net.staticstudios.data.query; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.query.clause.*; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Stack; + +@SuppressWarnings("unused") +public abstract class BaseQueryWhere { + protected final Set innerJoins = new HashSet<>(); + private final Stack nonGrouped = new Stack<>(); + private Node root = null; + + private static void buildWhereClauseRecursive(Node node, StringBuilder sb, List parameters) { + if (node == null) { + return; + } + boolean isConditional = node.clause instanceof ConditionalClause; + if (isConditional) { + sb.append("("); + } + + buildWhereClauseRecursive(node.lhs, sb, parameters); + List clauseParams = node.clause.append(sb); + parameters.addAll(clauseParams); + buildWhereClauseRecursive(node.rhs, sb, parameters); + if (isConditional) { + sb.append(")"); + } + } + + protected void addInnerJoin(String referringSchema, String referringTable, String[] columnsInReferringTable, String referencedSchema, String referencedTable, String[] columnsInReferencedTable) { + innerJoins.add(new InnerJoin(referringSchema, referringTable, columnsInReferringTable, referencedSchema, referencedTable, columnsInReferencedTable)); + } + + protected boolean isEmpty() { + return root == null; + } + + protected Set getInnerJoins() { + return innerJoins; + } + + /** + * Push the current state onto the stack to start a new group. + */ + protected void pushGroup() { + if (root != null) { + Preconditions.checkState(root.clause instanceof ConditionalClause && root.rhs == null, "Invalid state! Cannot start a group here!"); + } + nonGrouped.push(root); + root = null; + } + + /** + * Pop the last group from the stack and set it as the new root. + * Setting the current root as the right-hand side of the popped group. + */ + protected void popGroup() { + Preconditions.checkState(!nonGrouped.isEmpty(), "Invalid state! No group to pop!"); + Node newRoot = nonGrouped.pop(); + if (newRoot == null) { + return; + } + + newRoot.rhs = root; + root = newRoot; + } + + protected void andClause() { + setConditionalClause(new AndClause()); + } + + protected void orClause() { + setConditionalClause(new OrClause()); + } + + protected void equalsClause(String schema, String table, String column, @Nullable Object o) { + if (o == null) { + nullClause(schema, table, column); + return; + } + setValueClause(new EqualsClause(schema, table, column, o)); + } + + protected void notEqualsClause(String schema, String table, String column, @Nullable Object o) { + if (o == null) { + notNullClause(schema, table, column); + return; + } + setValueClause(new NotEqualsClause(schema, table, column, o)); + } + + protected void nullClause(String schema, String table, String column) { + setValueClause(new NullClause(schema, table, column)); + } + + protected void notNullClause(String schema, String table, String column) { + setValueClause(new NotNullClause(schema, table, column)); + } + + protected void likeClause(String schema, String table, String column, String format) { + setValueClause(new LikeClause(schema, table, column, format)); + } + + protected void inClause(String referringSchema, String referringTable, String column, Object[] in) { + setValueClause(new InClause(referringSchema, referringTable, column, in)); + } + + protected void notInClause(String referringSchema, String referringTable, String column, Object[] in) { + setValueClause(new NotInClause(referringSchema, referringTable, column, in)); + } + + protected void notLikeClause(String schema, String table, String column, String format) { + setValueClause(new NotLikeClause(schema, table, column, format)); + } + + protected void betweenClause(String schema, String table, String column, Object min, Object max) { + setValueClause(new BetweenClause(schema, table, column, min, max)); + } + + protected void notBetweenClause(String schema, String table, String column, Object min, Object max) { + setValueClause(new NotBetweenClause(schema, table, column, min, max)); + } + + protected void greaterThanClause(String schema, String table, String column, Object o) { + setValueClause(new GreaterThanClause(schema, table, column, o)); + } + + protected void greaterThanOrEqualToClause(String schema, String table, String column, Object o) { + setValueClause(new GreaterThanOrEqualToClause(schema, table, column, o)); + } + + protected void lessThanClause(String schema, String table, String column, Object o) { + setValueClause(new LessThanClause(schema, table, column, o)); + } + + protected void lessThanOrEqualToClause(String schema, String table, String column, Object o) { + setValueClause(new LessThanOrEqualToClause(schema, table, column, o)); + } + + private void setConditionalClause(Clause clause) { + Preconditions.checkState(root != null, "Invalid state! Cannot set conditional clause '" + clause + "' here!"); + if (root.clause instanceof ConditionalClause) { + Node lhs = root.lhs; + Node rhs = root.rhs; + Preconditions.checkState(lhs != null && rhs != null, "Invalid state! Cannot set conditional clause '" + clause + "' here!"); + } + Node newRoot = new Node(); + newRoot.clause = clause; + newRoot.lhs = root; + root = newRoot; + } + + private void setValueClause(Clause clause) { + if (root == null) { + root = new Node(); + root.clause = clause; + } else { + Preconditions.checkState(root.rhs == null, "Invalid state! Cannot set clause '" + clause + "' here!"); + root.rhs = new Node(); + root.rhs.clause = clause; + } + } + + public void buildWhereClause(StringBuilder sb, List parameters) { + buildWhereClauseRecursive(root, sb, parameters); + } + + static class Node { + Clause clause; + Node lhs; + Node rhs; + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/InnerJoin.java b/core/src/main/java/net/staticstudios/data/query/InnerJoin.java index e3b3fbad..ae87fb70 100644 --- a/core/src/main/java/net/staticstudios/data/query/InnerJoin.java +++ b/core/src/main/java/net/staticstudios/data/query/InnerJoin.java @@ -3,24 +3,25 @@ import java.util.Arrays; import java.util.Objects; -public record InnerJoin(String schema, String table, String[] columns, String foreignSchema, String foreignTable, - String[] foreignColumns) { +public record InnerJoin(String referringSchema, String referringTable, String[] columnsInReferringTable, + String referencedSchema, String referencedTable, + String[] columnsInReferencedTable) { @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; InnerJoin that = (InnerJoin) obj; - return Objects.equals(schema, that.schema) && - Objects.equals(table, that.table) && - Arrays.equals(columns, that.columns) && - Objects.equals(foreignSchema, that.foreignSchema) && - Objects.equals(foreignTable, that.foreignTable) && - Arrays.equals(foreignColumns, that.foreignColumns); + return Objects.equals(referringSchema, that.referringSchema) && + Objects.equals(referringTable, that.referringTable) && + Arrays.equals(columnsInReferringTable, that.columnsInReferringTable) && + Objects.equals(referencedSchema, that.referencedSchema) && + Objects.equals(referencedTable, that.referencedTable) && + Arrays.equals(columnsInReferencedTable, that.columnsInReferencedTable); } @Override public int hashCode() { - return Objects.hash(schema, table, Arrays.hashCode(columns), foreignSchema, foreignTable, Arrays.hashCode(foreignColumns)); + return Objects.hash(referringSchema, referringTable, Arrays.hashCode(columnsInReferringTable), referencedSchema, referencedTable, Arrays.hashCode(columnsInReferencedTable)); } } diff --git a/core/src/main/java/net/staticstudios/data/query/clause/AndClause.java b/core/src/main/java/net/staticstudios/data/query/clause/AndClause.java index aec87c71..2b2f0151 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/AndClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/AndClause.java @@ -1,23 +1,13 @@ package net.staticstudios.data.query.clause; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; -public class AndClause implements CompositeClause { - private final Clause left; - private final Clause right; - - public AndClause(Clause left, Clause right) { - this.left = left; - this.right = right; - } +public class AndClause implements ConditionalClause { @Override public List append(StringBuilder sb) { - List values = new ArrayList<>(left.append(sb)); sb.append(" AND "); - values.addAll(right.append(sb)); - - return values; + return Collections.emptyList(); } } diff --git a/core/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java b/core/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java index 863c22dc..51ef4ddf 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/BetweenClause.java @@ -2,14 +2,14 @@ import java.util.List; -public class BetweenClause implements ValueClause { +public class BetweenClause implements ValueClause { private final String schema; private final String table; private final String column; - private final N min; - private final N max; + private final Object min; + private final Object max; - public BetweenClause(String schema, String table, String column, N min, N max) { + public BetweenClause(String schema, String table, String column, Object min, Object max) { this.schema = schema; this.table = table; this.column = column; diff --git a/core/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java b/core/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java deleted file mode 100644 index a7eb50cc..00000000 --- a/core/src/main/java/net/staticstudios/data/query/clause/CompositeClause.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.staticstudios.data.query.clause; - -public interface CompositeClause extends Clause { -} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/ConditionalClause.java b/core/src/main/java/net/staticstudios/data/query/clause/ConditionalClause.java new file mode 100644 index 00000000..10e5c221 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/ConditionalClause.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.query.clause; + +public interface ConditionalClause extends Clause { +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java b/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java index cb57498c..1cdb4532 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java @@ -2,13 +2,13 @@ import java.util.List; -public class GreaterThanClause implements ValueClause { +public class GreaterThanClause implements ValueClause { private final String schema; private final String table; private final String column; - private final N value; + private final Object value; - public GreaterThanClause(String schema, String table, String column, N value) { + public GreaterThanClause(String schema, String table, String column, Object value) { this.schema = schema; this.table = table; this.column = column; diff --git a/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java b/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java index 940f2f13..0e4c24df 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java @@ -2,13 +2,13 @@ import java.util.List; -public class GreaterThanOrEqualToClause implements ValueClause { +public class GreaterThanOrEqualToClause implements ValueClause { private final String schema; private final String table; private final String column; - private final N value; + private final Object value; - public GreaterThanOrEqualToClause(String schema, String table, String column, N value) { + public GreaterThanOrEqualToClause(String schema, String table, String column, Object value) { this.schema = schema; this.table = table; this.column = column; diff --git a/core/src/main/java/net/staticstudios/data/query/clause/InClause.java b/core/src/main/java/net/staticstudios/data/query/clause/InClause.java index 64dd75f2..8cdee4bc 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/InClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/InClause.java @@ -15,10 +15,6 @@ public InClause(String schema, String table, String column, Object[] values) { this.values = values; } - public InClause(String schema, String table, String column, List values) { - this(schema, table, column, values.toArray()); - } - @Override public List append(StringBuilder sb) { sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" IN ("); diff --git a/core/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java b/core/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java index 22221bc4..18a4e30b 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java @@ -2,13 +2,13 @@ import java.util.List; -public class LessThanClause implements ValueClause { +public class LessThanClause implements ValueClause { private final String schema; private final String table; private final String column; - private final N value; + private final Object value; - public LessThanClause(String schema, String table, String column, N value) { + public LessThanClause(String schema, String table, String column, Object value) { this.schema = schema; this.table = table; this.column = column; diff --git a/core/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java b/core/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java index 3a9546f8..53508b29 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java @@ -2,13 +2,13 @@ import java.util.List; -public class LessThanOrEqualToClause implements ValueClause { +public class LessThanOrEqualToClause implements ValueClause { private final String schema; private final String table; private final String column; - private final N value; + private final Object value; - public LessThanOrEqualToClause(String schema, String table, String column, N value) { + public LessThanOrEqualToClause(String schema, String table, String column, Object value) { this.schema = schema; this.table = table; this.column = column; diff --git a/core/src/main/java/net/staticstudios/data/query/clause/NotBetweenClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotBetweenClause.java new file mode 100644 index 00000000..93277fa8 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/NotBetweenClause.java @@ -0,0 +1,25 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class NotBetweenClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final Object min; + private final Object max; + + public NotBetweenClause(String schema, String table, String column, Object min, Object max) { + this.schema = schema; + this.table = table; + this.column = column; + this.min = min; + this.max = max; + } + + @Override + public List append(StringBuilder sb) { + sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" NOT BETWEEN ? AND ?"); + return List.of(min, max); + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/NotInClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotInClause.java index c15474aa..dabfe001 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/NotInClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/NotInClause.java @@ -15,10 +15,6 @@ public NotInClause(String schema, String table, String column, Object[] values) this.values = values; } - public NotInClause(String schema, String table, String column, List values) { - this(schema, table, column, values.toArray()); - } - @Override public List append(StringBuilder sb) { sb.append("\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\" NOT IN ("); diff --git a/core/src/main/java/net/staticstudios/data/query/clause/OrClause.java b/core/src/main/java/net/staticstudios/data/query/clause/OrClause.java index 06c45a5a..2bab731d 100644 --- a/core/src/main/java/net/staticstudios/data/query/clause/OrClause.java +++ b/core/src/main/java/net/staticstudios/data/query/clause/OrClause.java @@ -1,23 +1,13 @@ package net.staticstudios.data.query.clause; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; -public class OrClause implements CompositeClause { - private final Clause left; - private final Clause right; - - public OrClause(Clause left, Clause right) { - this.left = left; - this.right = right; - } +public class OrClause implements ConditionalClause { @Override public List append(StringBuilder sb) { - List values = new ArrayList<>(left.append(sb)); sb.append(" OR "); - values.addAll(right.append(sb)); - - return values; + return Collections.emptyList(); } } diff --git a/core/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java b/core/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java deleted file mode 100644 index 718b7c9d..00000000 --- a/core/src/main/java/net/staticstudios/data/query/clause/ParenthesisClause.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.staticstudios.data.query.clause; - -import java.util.List; - -public class ParenthesisClause implements ValueClause { - private final Clause inner; - - public ParenthesisClause(Clause clause) { - this.inner = clause; - } - - @Override - public List append(StringBuilder sb) { - sb.append("("); - List values = inner.append(sb); - sb.append(")"); - return values; - } -} diff --git a/core/src/test/java/net/staticstudios/data/CustomTypeTest.java b/core/src/test/java/net/staticstudios/data/CustomTypeTest.java index 0bf8c69e..aca00455 100644 --- a/core/src/test/java/net/staticstudios/data/CustomTypeTest.java +++ b/core/src/test/java/net/staticstudios/data/CustomTypeTest.java @@ -4,39 +4,30 @@ import net.staticstudios.data.mock.account.*; import net.staticstudios.data.mock.wrapper.booleanprimitive.BooleanWrapper; import net.staticstudios.data.mock.wrapper.booleanprimitive.BooleanWrapperDataClass; -import net.staticstudios.data.mock.wrapper.booleanprimitive.BooleanWrapperDataClassFactory; import net.staticstudios.data.mock.wrapper.booleanprimitive.BooleanWrapperValueSerializer; import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapper; import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapperDataClass; -import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapperDataClassFactory; import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapperValueSerializer; import net.staticstudios.data.mock.wrapper.doubleprimitive.DoubleWrapper; import net.staticstudios.data.mock.wrapper.doubleprimitive.DoubleWrapperDataClass; -import net.staticstudios.data.mock.wrapper.doubleprimitive.DoubleWrapperDataClassFactory; import net.staticstudios.data.mock.wrapper.doubleprimitive.DoubleWrapperValueSerializer; import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapper; import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapperDataClass; -import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapperDataClassFactory; import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapperValueSerializer; import net.staticstudios.data.mock.wrapper.integerprimitive.IntegerWrapper; import net.staticstudios.data.mock.wrapper.integerprimitive.IntegerWrapperDataClass; -import net.staticstudios.data.mock.wrapper.integerprimitive.IntegerWrapperDataClassFactory; import net.staticstudios.data.mock.wrapper.integerprimitive.IntegerWrapperValueSerializer; import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapper; import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapperDataClass; -import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapperDataClassFactory; import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapperValueSerializer; import net.staticstudios.data.mock.wrapper.stringprimitive.StringWrapper; import net.staticstudios.data.mock.wrapper.stringprimitive.StringWrapperDataClass; -import net.staticstudios.data.mock.wrapper.stringprimitive.StringWrapperDataClassFactory; import net.staticstudios.data.mock.wrapper.stringprimitive.StringWrapperValueSerializer; import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapper; import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapperDataClass; -import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapperDataClassFactory; import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapperValueSerializer; import net.staticstudios.data.mock.wrapper.uuidprimitive.UUIDWrapper; import net.staticstudios.data.mock.wrapper.uuidprimitive.UUIDWrapperDataClass; -import net.staticstudios.data.mock.wrapper.uuidprimitive.UUIDWrapperDataClassFactory; import net.staticstudios.data.mock.wrapper.uuidprimitive.UUIDWrapperValueSerializer; import org.junit.jupiter.api.Test; @@ -55,7 +46,7 @@ public void testCustomTypesSetGet() { dataManager.registerValueSerializer(new AccountSettingsValueSerializer()); dataManager.load(MockAccount.class); - MockAccount account = MockAccountFactory.builder(dataManager) + MockAccount account = MockAccount.builder(dataManager) .id(1) .insert(InsertMode.SYNC); @@ -95,7 +86,7 @@ public void testCustomTypesLoad() throws SQLException { dataManager.registerValueSerializer(settingsSerializer); dataManager.load(MockAccount.class); - MockAccount account = MockAccountQuery.where(dataManager).idIs(1).findOne(); + MockAccount account = MockAccount.query(dataManager).where(w -> w.idIs(1)).findOne(); assertNotNull(account); assertEquals(settings, account.settings.get()); assertEquals(details, account.details.get()); @@ -106,7 +97,7 @@ public void testCustomTypeWithStringPrimitive() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.registerValueSerializer(new StringWrapperValueSerializer()); dataManager.load(StringWrapperDataClass.class); - StringWrapperDataClass data = StringWrapperDataClassFactory.builder(dataManager) + StringWrapperDataClass data = StringWrapperDataClass.builder(dataManager) .id(1) .value(new StringWrapper("Hello, World!")) .insert(InsertMode.SYNC); @@ -139,7 +130,7 @@ public void testCustomTypeWithIntegerPrimitive() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.registerValueSerializer(new IntegerWrapperValueSerializer()); dataManager.load(IntegerWrapperDataClass.class); - IntegerWrapperDataClass data = IntegerWrapperDataClassFactory.builder(dataManager) + IntegerWrapperDataClass data = IntegerWrapperDataClass.builder(dataManager) .id(1) .value(new IntegerWrapper(42)) .insert(InsertMode.SYNC); @@ -173,7 +164,7 @@ public void testCustomTypeWithLongPrimitive() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.registerValueSerializer(new LongWrapperValueSerializer()); dataManager.load(LongWrapperDataClass.class); - LongWrapperDataClass data = LongWrapperDataClassFactory.builder(dataManager) + LongWrapperDataClass data = LongWrapperDataClass.builder(dataManager) .id(1) .value(new LongWrapper(1234567890123L)) .insert(InsertMode.SYNC); @@ -207,7 +198,7 @@ public void testCustomTypeWithFloatPrimitive() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.registerValueSerializer(new FloatWrapperValueSerializer()); dataManager.load(FloatWrapperDataClass.class); - FloatWrapperDataClass data = FloatWrapperDataClassFactory.builder(dataManager) + FloatWrapperDataClass data = FloatWrapperDataClass.builder(dataManager) .id(1) .value(new FloatWrapper(3.14f)) .insert(InsertMode.SYNC); @@ -241,7 +232,7 @@ public void testCustomTypeWithDoublePrimitive() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.registerValueSerializer(new DoubleWrapperValueSerializer()); dataManager.load(DoubleWrapperDataClass.class); - DoubleWrapperDataClass data = DoubleWrapperDataClassFactory.builder(dataManager) + DoubleWrapperDataClass data = DoubleWrapperDataClass.builder(dataManager) .id(1) .value(new DoubleWrapper(2.71828)) .insert(InsertMode.SYNC); @@ -275,7 +266,7 @@ public void testCustomTypeWithBooleanPrimitive() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.registerValueSerializer(new BooleanWrapperValueSerializer()); dataManager.load(BooleanWrapperDataClass.class); - BooleanWrapperDataClass data = BooleanWrapperDataClassFactory.builder(dataManager) + BooleanWrapperDataClass data = BooleanWrapperDataClass.builder(dataManager) .id(1) .value(new BooleanWrapper(true)) .insert(InsertMode.SYNC); @@ -310,7 +301,7 @@ public void testCustomTypeWithUUIDPrimitive() throws SQLException { dataManager.registerValueSerializer(new UUIDWrapperValueSerializer()); dataManager.load(UUIDWrapperDataClass.class); java.util.UUID uuid = java.util.UUID.randomUUID(); - UUIDWrapperDataClass data = UUIDWrapperDataClassFactory.builder(dataManager) + UUIDWrapperDataClass data = UUIDWrapperDataClass.builder(dataManager) .id(1) .value(new UUIDWrapper(uuid)) .insert(InsertMode.SYNC); @@ -345,7 +336,7 @@ public void testCustomTypeWithTimestampPrimitive() throws SQLException { dataManager.registerValueSerializer(new TimestampWrapperValueSerializer()); dataManager.load(TimestampWrapperDataClass.class); java.sql.Timestamp timestamp = new java.sql.Timestamp(System.currentTimeMillis()); - TimestampWrapperDataClass data = TimestampWrapperDataClassFactory.builder(dataManager) + TimestampWrapperDataClass data = TimestampWrapperDataClass.builder(dataManager) .id(1) .value(new TimestampWrapper(timestamp)) .insert(InsertMode.SYNC); @@ -380,7 +371,7 @@ public void testCustomTypeWithByteArrayPrimitive() throws SQLException { dataManager.registerValueSerializer(new ByteArrayWrapperValueSerializer()); dataManager.load(ByteArrayWrapperDataClass.class); byte[] bytes = new byte[]{1, 2, 3, 4, 5}; - ByteArrayWrapperDataClass data = ByteArrayWrapperDataClassFactory.builder(dataManager) + ByteArrayWrapperDataClass data = ByteArrayWrapperDataClass.builder(dataManager) .id(1) .value(new ByteArrayWrapper(bytes)) .insert(InsertMode.SYNC); diff --git a/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java index 5ef1b760..f7f538ca 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java @@ -3,7 +3,6 @@ import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.misc.TestUtils; import net.staticstudios.data.mock.user.MockUser; -import net.staticstudios.data.mock.user.MockUserFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,7 +27,7 @@ public void setUp() { dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - mockUser = MockUserFactory.builder(dataManager) + mockUser = MockUser.builder(dataManager) .id(id) .name("test user") .insert(InsertMode.SYNC); @@ -37,7 +36,7 @@ public void setUp() { private List createFriends(int count) { List friends = new ArrayList<>(); for (int i = 0; i < count; i++) { - MockUser friend = MockUserFactory.builder(dataManager) + MockUser friend = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("friend " + i) .insert(InsertMode.ASYNC); @@ -293,7 +292,7 @@ public void testIterator() { @Test public void testEqualsAndHashCode() { - MockUser anotherUser = MockUserFactory.builder(dataManager) + MockUser anotherUser = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("another user") .insert(InsertMode.SYNC); diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java index 4f8d470b..e87b9b28 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -3,9 +3,7 @@ import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.misc.TestUtils; import net.staticstudios.data.mock.user.MockUser; -import net.staticstudios.data.mock.user.MockUserFactory; import net.staticstudios.data.mock.user.MockUserSession; -import net.staticstudios.data.mock.user.MockUserSessionFactory; import net.staticstudios.utils.RandomUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,7 +31,7 @@ public void setUp() { dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - mockUser = MockUserFactory.builder(dataManager) + mockUser = MockUser.builder(dataManager) .id(id) .name("test user") .insert(InsertMode.SYNC); @@ -42,7 +40,7 @@ public void setUp() { private List createSessions(int count) { List sessions = new ArrayList<>(); for (int i = 0; i < count; i++) { - MockUserSession session = MockUserSessionFactory.builder(dataManager) + MockUserSession session = MockUserSession.builder(dataManager) .id(UUID.randomUUID()) .timestamp(Timestamp.from(Instant.ofEpochSecond(RandomUtils.randomInt(0, 1_000_000_000)))) .insert(InsertMode.ASYNC); @@ -297,7 +295,7 @@ public void testIterator() { @Test public void testEqualsAndHashCode() { - MockUser anotherMockUser = MockUserFactory.builder(dataManager) + MockUser anotherMockUser = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("another user") .insert(InsertMode.SYNC); diff --git a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java index b84e2375..1078b058 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -34,7 +34,6 @@ public void testReadData() throws SQLException { .name("user " + id) .insert(InsertMode.SYNC); } - for (UUID id : userIds) { MockUser user = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); assertEquals("user " + id, user.name.get()); @@ -260,7 +259,7 @@ public void testChangeIdColumn() { @Disabled //todo: known to break @Test public void testChangeIdColumnInPostgres() { - //todo: this and the other id column test are failing because fkeys have been changed to be on the user table. use a trigger to update the fkeys on id change, similar to the cascade delete trigger + //todo: this and the other id column test are failing because fkeys have been changed to be on the user referringTable. use a trigger to update the fkeys on id change, similar to the cascade delete trigger DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); diff --git a/core/src/test/java/net/staticstudios/data/QueryTest.java b/core/src/test/java/net/staticstudios/data/QueryTest.java index aa4248f1..6dc26719 100644 --- a/core/src/test/java/net/staticstudios/data/QueryTest.java +++ b/core/src/test/java/net/staticstudios/data/QueryTest.java @@ -2,8 +2,6 @@ import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.mock.user.MockUser; -import net.staticstudios.data.mock.user.MockUserFactory; -import net.staticstudios.data.mock.user.MockUserQuery; import org.junit.jupiter.api.Test; import java.util.List; @@ -17,13 +15,12 @@ public void testFindOneEquals() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); - MockUser original = MockUserFactory.builder(dataManager) + MockUser original = MockUser.builder(dataManager) .id(id) .name("test user") .insert(InsertMode.SYNC); - MockUser got = MockUserQuery.where(dataManager) - .idIs(id) + MockUser got = MockUser.query(dataManager).where(w -> w.idIs(id)) .findOne(); assertSame(original, got); } @@ -33,19 +30,18 @@ public void testFindAllLike() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); - MockUser original1 = MockUserFactory.builder(dataManager) + MockUser original1 = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("test user") .age(0) .insert(InsertMode.SYNC); - MockUser original2 = MockUserFactory.builder(dataManager) + MockUser original2 = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("test user2") .age(5) .insert(InsertMode.SYNC); - List got = MockUserQuery.where(dataManager) - .nameIsLike("%test user%") + List got = MockUser.query(dataManager).where(w -> w.nameIsLike("%test user%")) .orderByAge(Order.ASCENDING) .findAll(); @@ -56,8 +52,7 @@ public void testFindAllLike() { assertSame(original1, got.get(0)); assertSame(original2, got.get(1)); - got = MockUserQuery.where(dataManager) - .nameIsLike("%test user%") + got = MockUser.query(dataManager).where(w -> w.nameIsLike("%test user%")) .orderByAge(Order.DESCENDING) .findAll(); @@ -74,39 +69,36 @@ public void testQueryOnForeignColumn() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); - MockUser likesRed = MockUserFactory.builder(dataManager) + MockUser likesRed = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("Likes Red") .favoriteColor("red") .insert(InsertMode.SYNC); - MockUser likesGreen = MockUserFactory.builder(dataManager) + MockUser likesGreen = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("Likes Green") .favoriteColor("green") .insert(InsertMode.SYNC); - assertNull(MockUserQuery.where(dataManager) - .favoriteColorIs("blue") + assertNull(MockUser.query(dataManager).where(w -> w.favoriteColorIs("blue")) .findOne()); - assertSame(likesRed, MockUserQuery.where(dataManager) - .favoriteColorIs("red") + assertSame(likesRed, MockUser.query(dataManager).where(w -> w.favoriteColorIs("red")) .findOne()); - assertSame(likesGreen, MockUserQuery.where(dataManager) - .favoriteColorIs("green") + assertSame(likesGreen, MockUser.query(dataManager).where(w -> w.favoriteColorIs("green")) .findOne()); - List users = MockUserQuery.where(dataManager) - .favoriteColorIsIn("red", "green") + List users = MockUser.query(dataManager).where(w -> w + .favoriteColorIsIn("red", "green") + ) .orderByName(Order.ASCENDING) .findAll(); assertEquals(2, users.size()); assertSame(likesGreen, users.get(0)); assertSame(likesRed, users.get(1)); - users = MockUserQuery.where(dataManager) - .favoriteColorIsNotNull() + users = MockUser.query(dataManager).where(w -> w.favoriteColorIsNotNull()) .orderByFavoriteColor(Order.DESCENDING) .findAll(); assertEquals(2, users.size()); @@ -117,129 +109,126 @@ public void testQueryOnForeignColumn() { @Test public void testEqualsClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"id\" = ?", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).toString()); + assertEquals("WHERE \"public\".\"users\".\"id\" = ?", MockUser.query(dataManager).where(w -> w.idIs(UUID.randomUUID())).toString()); } @Test public void testBetweenClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"age\" BETWEEN ? AND ?", MockUserQuery.where(dataManager).ageIsBetween(0, 0).toString()); + 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(); - assertEquals("WHERE \"public\".\"users\".\"age\" < ?", MockUserQuery.where(dataManager).ageIsLessThan(0).toString()); + assertEquals("WHERE \"public\".\"users\".\"age\" < ?", MockUser.query(dataManager).where(w -> w.ageIsLessThan(0)).toString()); } @Test public void testAgeIsLessThanOrEqualToClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"age\" <= ?", MockUserQuery.where(dataManager).ageIsLessThanOrEqualTo(0).toString()); + assertEquals("WHERE \"public\".\"users\".\"age\" <= ?", MockUser.query(dataManager).where(w -> w.ageIsLessThanOrEqualTo(0)).toString()); } @Test public void testAgeIsGreaterThanClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"age\" > ?", MockUserQuery.where(dataManager).ageIsGreaterThan(0).toString()); + assertEquals("WHERE \"public\".\"users\".\"age\" > ?", MockUser.query(dataManager).where(w -> w.ageIsGreaterThan(0)).toString()); } @Test public void testAgeIsGreaterThanOrEqualToClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"age\" >= ?", MockUserQuery.where(dataManager).ageIsGreaterThanOrEqualTo(0).toString()); + assertEquals("WHERE \"public\".\"users\".\"age\" >= ?", MockUser.query(dataManager).where(w -> w.ageIsGreaterThanOrEqualTo(0)).toString()); } @Test public void testAgeIsNullClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"age\" IS NULL", MockUserQuery.where(dataManager).ageIsNull().toString()); + assertEquals("WHERE \"public\".\"users\".\"age\" IS NULL", MockUser.query(dataManager).where(w -> w.ageIsNull()).toString()); } @Test public void testAgeIsNotNullClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"age\" IS NOT NULL", MockUserQuery.where(dataManager).ageIsNotNull().toString()); + 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(); - assertEquals("WHERE \"public\".\"users\".\"name\" LIKE ?", MockUserQuery.where(dataManager).nameIsLike("%test%").toString()); + assertEquals("WHERE \"public\".\"users\".\"name\" LIKE ?", MockUser.query(dataManager).where(w -> w.nameIsLike("%test%")).toString()); } @Test public void testNameIsNotLikeClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"name\" NOT LIKE ?", MockUserQuery.where(dataManager).nameIsNotLike("%test%").toString()); + 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(); - assertEquals("WHERE \"public\".\"users\".\"name\" IN (?, ?, ?)", MockUserQuery.where(dataManager).nameIsIn("name1", "name2", "name3").toString()); + 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(); - assertEquals("WHERE \"public\".\"users\".\"name\" IN (?, ?, ?)", MockUserQuery.where(dataManager).nameIsIn(List.of("name1", "name2", "name3")).toString()); + 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(); - assertEquals("WHERE \"public\".\"users\".\"id\" = ? LIMIT 10", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).limit(10).toString()); + 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(); - assertEquals("WHERE \"public\".\"users\".\"id\" = ? OFFSET 5", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).offset(5).toString()); + 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(); - assertEquals("WHERE \"public\".\"users\".\"id\" = ? ORDER BY \"public\".\"users\".\"age\" ASC", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).orderByAge(Order.ASCENDING).toString()); + 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 testAndClauseWithoutParentheses() { + public void testAndClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"id\" = ? AND \"public\".\"users\".\"age\" BETWEEN ? AND ?", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).and().ageIsBetween(0, 5).toString()); + 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)) + ).toString()); } @Test - public void testOrClauseWithoutParentheses() { + public void testOrClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"id\" = ? OR \"public\".\"users\".\"age\" BETWEEN ? AND ?", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).or().ageIsBetween(0, 5).toString()); - } - - @Test - public void testAndClauseWithParentheses() { - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"id\" = ? AND (\"public\".\"users\".\"age\" BETWEEN ? AND ?)", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).and(q -> q.ageIsBetween(0, 5)).toString()); - } - - @Test - public void testOrClauseWithParentheses() { - DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - assertEquals("WHERE \"public\".\"users\".\"id\" = ? OR (\"public\".\"users\".\"age\" BETWEEN ? AND ?)", MockUserQuery.where(dataManager).idIs(UUID.randomUUID()).or(q -> q.ageIsBetween(0, 5)).toString()); + 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()); } @Test public void testComplexClause() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); - 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", - MockUserQuery.where(dataManager) - .idIs(UUID.randomUUID()) - .or(q -> q.ageIsBetween(0, 5)) - .and() - .nameIsLike("%test%") + 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()) + .or() + .ageIsBetween(0, 5) + .and() + .nameIsLike("%test%") + ) .orderByAge(Order.DESCENDING) .limit(10) .offset(5) .toString()); } + + //todo: test more complex cases } \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/ReferenceTest.java b/core/src/test/java/net/staticstudios/data/ReferenceTest.java index 816203a2..89644f62 100644 --- a/core/src/test/java/net/staticstudios/data/ReferenceTest.java +++ b/core/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -3,9 +3,7 @@ import net.staticstudios.data.insert.InsertContext; import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.mock.user.MockUser; -import net.staticstudios.data.mock.user.MockUserFactory; import net.staticstudios.data.mock.user.MockUserSettings; -import net.staticstudios.data.mock.user.MockUserSettingsFactory; import org.junit.jupiter.api.Test; import java.sql.Connection; @@ -22,7 +20,7 @@ public void testCreateSettingsWithoutReference() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); - MockUserSettings settings = MockUserSettingsFactory.builder(dataManager) + MockUserSettings settings = MockUserSettings.builder(dataManager) .id(UUID.randomUUID()) .insert(InsertMode.SYNC); @@ -34,13 +32,13 @@ public void testCreateSettingsThenReference() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); - MockUserSettings settings = MockUserSettingsFactory.builder(dataManager) + MockUserSettings settings = MockUserSettings.builder(dataManager) .id(UUID.randomUUID()) .insert(InsertMode.SYNC); assertNotNull(settings); - MockUser user = MockUserFactory.builder(dataManager) + MockUser user = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("test user") .settingsId(settings.id.get()) @@ -57,11 +55,11 @@ public void testCreateUserAndReferenceInSingleInsert() { UUID settingsId = UUID.randomUUID(); InsertContext ctx = dataManager.createInsertContext(); - MockUserSettingsFactory.builder(dataManager) + MockUserSettings.builder(dataManager) .id(settingsId) .insert(ctx); - MockUserFactory.builder(dataManager) + MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("test user") .settingsId(settingsId) @@ -81,13 +79,13 @@ public void testChangeReference() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); - MockUserSettings settings = MockUserSettingsFactory.builder(dataManager) + MockUserSettings settings = MockUserSettings.builder(dataManager) .id(UUID.randomUUID()) .insert(InsertMode.SYNC); assertNotNull(settings); - MockUser user = MockUserFactory.builder(dataManager) + MockUser user = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("test user") .settingsId(settings.id.get()) @@ -96,7 +94,7 @@ public void testChangeReference() { assertNotNull(user); assertSame(settings, user.settings.get()); - MockUserSettings settings2 = MockUserSettingsFactory.builder(dataManager) + MockUserSettings settings2 = MockUserSettings.builder(dataManager) .id(UUID.randomUUID()) .insert(InsertMode.SYNC); @@ -124,11 +122,11 @@ public void testDeleteStrategyCascade() throws SQLException { Connection h2Connection = getH2Connection(dataManager); Connection pgConnection = getConnection(); UUID id = UUID.randomUUID(); - MockUserSettings settings = MockUserSettingsFactory.builder(dataManager) + MockUserSettings settings = MockUserSettings.builder(dataManager) .id(id) .insert(InsertMode.SYNC); - MockUser user = MockUserFactory.builder(dataManager) + MockUser user = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("test user") .settingsId(settings.id.get()) diff --git a/core/src/test/java/net/staticstudios/data/SQLParseTest.java b/core/src/test/java/net/staticstudios/data/SQLParseTest.java index f4bf3ca4..cb910137 100644 --- a/core/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/core/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -43,17 +43,19 @@ private static String normalize(String str) { private static void assertSqlLinesEqualOrderIndependent(List expectedLines, List actualLines) { Set expectedSet = new LinkedHashSet<>(expectedLines.stream().map(l -> { - if (l.endsWith(",")) { - l = l.substring(0, l.length() - 1); - } - return l.trim(); - }).toList()); + if (l.endsWith(",")) { + l = l.substring(0, l.length() - 1); + } + return l.trim(); + }) + .toList()); Set actualSet = new LinkedHashSet<>(actualLines.stream().map(l -> { - if (l.endsWith(",")) { - l = l.substring(0, l.length() - 1); - } - return l.trim(); - }).toList()); + if (l.endsWith(",")) { + l = l.substring(0, l.length() - 1); + } + return l.trim(); + }) + .toList()); if (!expectedSet.equals(actualSet)) { Set missing = new LinkedHashSet<>(expectedSet); @@ -111,7 +113,7 @@ public void testParse() throws Exception { } Container.ExecResult result = postgres.execInContainer("pg_dump", - "--schema-only", + "--referringSchema-only", "--no-owner", "--no-privileges", "--no-comments", diff --git a/core/src/test/java/net/staticstudios/data/misc/DataTest.java b/core/src/test/java/net/staticstudios/data/misc/DataTest.java index f4665663..1e56e362 100644 --- a/core/src/test/java/net/staticstudios/data/misc/DataTest.java +++ b/core/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -22,7 +22,6 @@ import java.util.Objects; public class DataTest { - //todo: performance test static-data using: java microbenchmarking harness public static int NUM_ENVIRONMENTS = 1; public static RedisContainer redis; public static PostgreSQLContainer postgres = new PostgreSQLContainer<>( diff --git a/core/src/test/java/net/staticstudios/data/mock/post/MockPost.java b/core/src/test/java/net/staticstudios/data/mock/post/MockPost.java index 8e95c618..533b70ed 100644 --- a/core/src/test/java/net/staticstudios/data/mock/post/MockPost.java +++ b/core/src/test/java/net/staticstudios/data/mock/post/MockPost.java @@ -3,7 +3,7 @@ import net.staticstudios.data.*; /** - * Used to validate schema generation. + * Used to validate referringSchema generation. */ @Data(schema = "${POST_SCHEMA}", table = "${POST_TABLE}") public class MockPost extends UniqueData { diff --git a/core/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java b/core/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java index a7708e6d..98fd3342 100644 --- a/core/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java +++ b/core/src/test/java/net/staticstudios/data/mock/post/MockPostMetadata.java @@ -3,7 +3,7 @@ import net.staticstudios.data.*; /** - * Used to validate schema generation. + * Used to validate referringSchema generation. */ @Data(schema = "${POST_SCHEMA}", table = "${POST_TABLE}_metadata") public class MockPostMetadata extends UniqueData { diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index 5afe4bcc..ec9d1e23 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -55,7 +55,7 @@ public class MockUser extends UniqueData { @ManyToMany(link = "id=id", joinTable = "user_friends") public PersistentCollection friends; - //todo: support OneToMany Collections where the data type is not a uniquedata. in this case additional info about what table and schema to use will be required, since we will have to create this table. + //todo: support OneToMany Collections where the data type is not a uniquedata. in this case additional info about what referringTable and referringSchema to use will be required, since we will have to create this referringTable. public int getNameUpdates() { return nameUpdates.get(); 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 828a4719..0f8c12d5 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 @@ -325,10 +325,10 @@ private SyntheticBuilderClass createQueryWhereBuilderClass(PsiClass parentClass) List clauses = QueryBuilderUtils.getClausesForType(psiField, isValidReference || IntelliJPluginUtils.isNullable(psiField, type)); for (QueryClause clause : clauses) { String methodName = clause.getMethodName(psiField.getName()); - List parameterTypes = clause.getMethodParamTypes(parentClass.getManager(), innerType); SyntheticMethod queryMethod = new SyntheticMethod(parentClass, whereClass, methodName, whereType); - for (int i = 0; i < parameterTypes.size(); i++) { - queryMethod.addParameter(psiField.getName() + i, parameterTypes.get(i)); + List parameterTypes = clause.getMethodParamTypes(parentClass.getManager(), innerType, queryMethod); + for (PsiParameter parameterType : parameterTypes) { + queryMethod.addParameter(parameterType); } queryMethod.addModifier(PsiModifier.PUBLIC); queryMethod.addModifier(PsiModifier.FINAL); 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 292eefb9..dcd25582 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 @@ -6,6 +6,18 @@ import java.util.Objects; public class IntelliJPluginUtils { + public static boolean genericTypeIs(PsiType type, String classFqn) { + if (!(type instanceof PsiClassType psiClassType)) { + return false; + } + PsiType[] params = psiClassType.getParameters(); + if (params.length == 0) { + return false; + } + + return is(params[0], classFqn); + } + public static boolean is(PsiType type, String classFqn) { if (!(type instanceof PsiClassType psiClassType)) { return false; 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 fba250ea..3b02ddc1 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 @@ -41,6 +41,11 @@ public boolean isConstructor() { @Override public boolean isVarArgs() { + for (PsiParameter parameter : getParameterList().getParameters()) { + if (parameter.isVarArgs()) { + return true; + } + } return false; } 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 4a856c51..105c3add 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 @@ -18,6 +18,12 @@ public class QueryBuilderUtils { pvClauses.add(new IsClause()); pvClauses.add(new IsNotClause()); + pvClauses.add(new IsInCollectionClause()); + pvClauses.add(new IsNotInCollectionClause()); + + pvClauses.add(new IsInArrayClause()); + pvClauses.add(new IsNotInArrayClause()); + pvClauses.add(new IsNullClause()); pvClauses.add(new IsNotNullClause()); @@ -32,10 +38,12 @@ public class QueryBuilderUtils { pvClauses.add(new IsNotBetweenClause()); referenceClauses = new ArrayList<>(); - referenceClauses.add(new IsClause()); - referenceClauses.add(new IsNotClause()); - referenceClauses.add(new IsNullClause()); - referenceClauses.add(new IsNotNullClause()); + //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. +// referenceClauses.add(new IsClause()); +// referenceClauses.add(new IsNotClause()); +// referenceClauses.add(new IsNullClause()); +// referenceClauses.add(new IsNotNullClause()); } public static List getClausesForType(PsiField psiField, boolean nullable) { diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryClause.java index 990656da..9bcc5bf2 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryClause.java @@ -1,8 +1,6 @@ package net.staticstudios.data.ide.intellij.query; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiType; +import com.intellij.psi.*; import java.util.List; @@ -12,5 +10,5 @@ public interface QueryClause { String getMethodName(String fieldName); - List getMethodParamTypes(PsiManager manager, PsiType fieldType); + List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope); } diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsBetweenClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsBetweenClause.java index 67729733..b7506e26 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsBetweenClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsBetweenClause.java @@ -1,7 +1,10 @@ package net.staticstudios.data.ide.intellij.query.clause; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiType; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.query.NumericClause; import java.util.List; @@ -14,7 +17,10 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType, fieldType); + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of( + new LightParameter("min", fieldType, scope), + new LightParameter("max", fieldType, scope) + ); } } diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsClause.java index 445c7728..f8dfaf0d 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsClause.java @@ -1,8 +1,7 @@ package net.staticstudios.data.ide.intellij.query.clause; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiType; +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.query.QueryClause; import java.util.List; @@ -20,7 +19,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType); + 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/IsGreaterThanClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanClause.java index 8c94257f..a5e31952 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanClause.java @@ -1,7 +1,10 @@ package net.staticstudios.data.ide.intellij.query.clause; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiType; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.query.NumericClause; import java.util.List; @@ -14,7 +17,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType); + 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/IsGreaterThanOrEqualToClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanOrEqualToClause.java index ef603763..475ef02c 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanOrEqualToClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanOrEqualToClause.java @@ -1,7 +1,10 @@ package net.staticstudios.data.ide.intellij.query.clause; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiType; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.query.NumericClause; import java.util.List; @@ -14,7 +17,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType); + 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/IsInArrayClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsInArrayClause.java new file mode 100644 index 00000000..7ab30bd2 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsInArrayClause.java @@ -0,0 +1,47 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +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 IsInArrayClause 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/IsInCollectionClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsInCollectionClause.java new file mode 100644 index 00000000..6f0b4805 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsInCollectionClause.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +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 IsInCollectionClause 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/IsLessThanClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanClause.java index 08d24cad..66d9a80c 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanClause.java @@ -1,7 +1,10 @@ package net.staticstudios.data.ide.intellij.query.clause; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiType; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.query.NumericClause; import java.util.List; @@ -14,7 +17,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType); + 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/IsLessThanOrEqualToClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanOrEqualToClause.java index a54e61d9..0e55cd62 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanOrEqualToClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanOrEqualToClause.java @@ -1,7 +1,10 @@ package net.staticstudios.data.ide.intellij.query.clause; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiType; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.query.NumericClause; import java.util.List; @@ -14,7 +17,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType); + 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/IsLikeClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java index 0213336e..c50eb7c1 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java @@ -1,8 +1,7 @@ package net.staticstudios.data.ide.intellij.query.clause; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiType; +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; import net.staticstudios.data.ide.intellij.query.QueryClause; @@ -12,7 +11,7 @@ public class IsLikeClause implements QueryClause { @Override public boolean matches(PsiField psiField, boolean nullable) { - return IntelliJPluginUtils.is(psiField.getType(), String.class.getName()); + return IntelliJPluginUtils.genericTypeIs(psiField.getType(), String.class.getName()); } @Override @@ -21,7 +20,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType); + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of(new LightParameter("pattern", fieldType, scope)); } } diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotBetweenClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotBetweenClause.java index 7ec55c33..d2f7ae16 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotBetweenClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotBetweenClause.java @@ -1,7 +1,10 @@ package net.staticstudios.data.ide.intellij.query.clause; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiType; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.query.NumericClause; import java.util.List; @@ -14,7 +17,10 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType, fieldType); + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of( + new LightParameter("min", fieldType, scope), + new LightParameter("max", fieldType, scope) + ); } } diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotClause.java index 807eb766..1026bc20 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotClause.java @@ -1,8 +1,7 @@ package net.staticstudios.data.ide.intellij.query.clause; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiType; +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.query.QueryClause; import java.util.List; @@ -20,7 +19,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType); + 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/IsNotInArrayClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotInArrayClause.java new file mode 100644 index 00000000..5028c1f2 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotInArrayClause.java @@ -0,0 +1,47 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +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 IsNotInArrayClause 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/IsNotInCollectionClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotInCollectionClause.java new file mode 100644 index 00000000..296f7110 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotInCollectionClause.java @@ -0,0 +1,34 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +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 IsNotInCollectionClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return true; + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNotIn"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + JavaPsiFacade facade = JavaPsiFacade.getInstance(manager.getProject()); + PsiClass collectionType = facade.findClass("java.util.Collection", GlobalSearchScope.allScope(manager.getProject())); + Preconditions.checkNotNull(collectionType, "Could not find java.util.Collection class"); + PsiTypeParameter[] typeParameters = collectionType.getTypeParameters(); + Preconditions.checkState(typeParameters.length == 1, "Expected Collection to have one type parameter"); + PsiSubstitutor substitutor = PsiSubstitutor.EMPTY; + substitutor = substitutor.put(typeParameters[0], fieldType); + return List.of(new LightParameter("values", facade.getElementFactory().createType(collectionType, substitutor), scope)); + } +} diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java index 9447a5ff..e78f18fa 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.java @@ -1,8 +1,7 @@ package net.staticstudios.data.ide.intellij.query.clause; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiType; +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; import net.staticstudios.data.ide.intellij.query.QueryClause; @@ -12,7 +11,7 @@ public class IsNotLikeClause implements QueryClause { @Override public boolean matches(PsiField psiField, boolean nullable) { - return IntelliJPluginUtils.is(psiField.getType(), String.class.getName()); + return IntelliJPluginUtils.genericTypeIs(psiField.getType(), String.class.getName()); } @Override @@ -21,7 +20,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return List.of(fieldType); + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of(new LightParameter("pattern", fieldType, scope)); } } diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotNullClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotNullClause.java index 0736068a..f4e843cb 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotNullClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotNullClause.java @@ -1,11 +1,8 @@ package net.staticstudios.data.ide.intellij.query.clause; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiType; +import com.intellij.psi.*; import net.staticstudios.data.ide.intellij.query.QueryClause; -import java.util.Collections; import java.util.List; public class IsNotNullClause implements QueryClause { @@ -21,7 +18,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return Collections.emptyList(); + 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/IsNullClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNullClause.java index 3dfe904f..c2a79c30 100644 --- a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNullClause.java +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNullClause.java @@ -1,11 +1,8 @@ package net.staticstudios.data.ide.intellij.query.clause; -import com.intellij.psi.PsiField; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiType; +import com.intellij.psi.*; import net.staticstudios.data.ide.intellij.query.QueryClause; -import java.util.Collections; import java.util.List; public class IsNullClause implements QueryClause { @@ -21,7 +18,7 @@ public String getMethodName(String fieldName) { } @Override - public List getMethodParamTypes(PsiManager manager, PsiType fieldType) { - return Collections.emptyList(); + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of(); } } diff --git a/javac-plugin/build.gradle b/javac-plugin/build.gradle index be573b91..c995ee66 100644 --- a/javac-plugin/build.gradle +++ b/javac-plugin/build.gradle @@ -7,7 +7,7 @@ repositories { } dependencies { - implementation(project(":utils")) + implementation project(":utils") implementation 'org.jetbrains:annotations:24.0.1' implementation("com.google.guava:guava:33.5.0-jre") } @@ -27,7 +27,9 @@ tasks.withType(JavaCompile).configureEach { } java { + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 toolchain { languageVersion = JavaLanguageVersion.of(21) } -} +} \ No newline at end of file diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java index 86ddcaca..514a8180 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java @@ -5,6 +5,10 @@ import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.Names; +import net.staticstudios.data.utils.Link; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; public abstract class AbstractBuilderProcessor { protected final JCTree.JCCompilationUnit compilationUnit; @@ -13,10 +17,16 @@ public abstract class AbstractBuilderProcessor { protected final JCTree.JCClassDecl dataClassDecl; protected final ParsedDataAnnotation dataAnnotation; private final String builderClassSuffix; - private final String builderMethodName; + private final @Nullable String builderMethodName; protected JCTree.JCClassDecl builderClassDecl; - public AbstractBuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation, String builderClassSuffix, String builderMethodName) { + public AbstractBuilderProcessor(JCTree.JCCompilationUnit compilationUnit, + TreeMaker treeMaker, + Names names, + JCTree.JCClassDecl dataClassDecl, + ParsedDataAnnotation dataAnnotation, String builderClassSuffix, + @Nullable String builderMethodName + ) { this.compilationUnit = compilationUnit; this.treeMaker = treeMaker; this.names = names; @@ -41,54 +51,90 @@ public void runProcessor() { addImports(); makeBuilderClass(); - makeBuilderMethod(); - makeParameterizedBuilderMethod(); + if (builderMethodName != null) { + makeBuilderMethod(); + makeParameterizedBuilderMethod(); + } process(); } - private void makeBuilderClass() { + protected @Nullable SuperClass extending() { + return null; + } + + protected void makeBuilderClass() { + SuperClass superClass = extending(); + JCTree.JCExpression classExtends; + JCTree.JCExpression superCall; + if (superClass != null) { + classExtends = treeMaker.TypeApply( + treeMaker.Ident(names.fromString(superClass.simpleName())), + superClass.superParms() + ); + superCall = treeMaker.Apply( + List.nil(), + treeMaker.Ident(names.fromString("super")), + superClass.args() + ); + } else { + classExtends = null; + superCall = null; + } + builderClassDecl = treeMaker.ClassDef( treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), names.fromString(getBuilderClassName()), List.nil(), - null, + classExtends, List.nil(), List.nil() ); - JCTree.JCVariableDecl dataManagerField = treeMaker.VarDef( - treeMaker.Modifiers(Flags.PRIVATE | Flags.FINAL), - names.fromString("dataManager"), - treeMaker.Ident(names.fromString("DataManager")), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(dataManagerField); + + java.util.List constructorBodyStatements = new ArrayList<>(); + if (superCall != null) { + constructorBodyStatements.add(treeMaker.Exec(superCall)); + } + + if (this.builderMethodName != null) { + JCTree.JCVariableDecl dataManagerField = treeMaker.VarDef( + treeMaker.Modifiers(Flags.PRIVATE | Flags.FINAL), + names.fromString("dataManager"), + treeMaker.Ident(names.fromString("DataManager")), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(dataManagerField); + constructorBodyStatements.add( + treeMaker.Exec( + treeMaker.Assign( + treeMaker.Select( + treeMaker.Ident(names.fromString("this")), + names.fromString("dataManager") + ), + treeMaker.Ident(names.fromString("dataManager")) + ) + ) + ); + } JCTree.JCMethodDecl constructor = treeMaker.MethodDef( treeMaker.Modifiers(Flags.PUBLIC), names.fromString(""), null, List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("dataManager"), - treeMaker.Ident(names.fromString("DataManager")), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), - names.fromString("dataManager") - ), - treeMaker.Ident(names.fromString("dataManager")) + this.builderMethodName == null ? + List.nil() + : + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("dataManager"), + treeMaker.Ident(names.fromString("DataManager")), + null ) - ) - )), + ), + List.nil(), + treeMaker.Block(0, List.from(constructorBodyStatements)), null ); builderClassDecl.defs = builderClassDecl.defs.append(constructor); @@ -161,4 +207,103 @@ private void makeBuilderMethod() { public String getBuilderClassName() { return dataClassDecl.name.toString() + builderClassSuffix; } + + public String storeSchema(String fieldName, String encoded) { + String schemaFieldName = getStoredSchemaFieldName(fieldName); + JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, schemaFieldName, treeMaker.Ident(names.fromString("String")), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("ValueUtils")), + names.fromString("parseValue") + ), + List.of( + treeMaker.Literal(encoded) + ) + ) + ); + return schemaFieldName; + } + + public String storeTable(String fieldName, String encoded) { + String tableFieldName = getStoredTableFieldName(fieldName); + JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, tableFieldName, treeMaker.Ident(names.fromString("String")), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("ValueUtils")), + names.fromString("parseValue") + ), + List.of( + treeMaker.Literal(encoded) + ) + ) + ); + return tableFieldName; + } + + public String storeColumn(String fieldName, String encoded) { + String columnFieldName = getStoredColumnFieldName(fieldName); + JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, columnFieldName, treeMaker.Ident(names.fromString("String")), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("ValueUtils")), + names.fromString("parseValue") + ), + List.of( + treeMaker.Literal(encoded) + ) + ) + ); + return columnFieldName; + } + + public String getStoredSchemaFieldName(String fieldName) { + return fieldName + "$schema"; + } + + public String getStoredTableFieldName(String fieldName) { + return fieldName + "$table"; + } + + public String getStoredColumnFieldName(String fieldName) { + return fieldName + "$column"; + } + + public void storeLinks(String fieldName, java.util.List links) { + String referringColumnsFieldName = fieldName + "$referringColumns"; + String referencedColumnsFieldName = fieldName + "$referencedColumns"; + JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, referringColumnsFieldName, treeMaker.TypeArray(treeMaker.Ident(names.fromString("String"))), + treeMaker.NewArray( + treeMaker.Ident(names.fromString("String")), + List.nil(), + List.from( + links.stream().map(link -> + treeMaker.Literal(link.columnInReferringTable()) + ).toList() + ) + ) + ); + + JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, referencedColumnsFieldName, treeMaker.TypeArray(treeMaker.Ident(names.fromString("String"))), + treeMaker.NewArray( + treeMaker.Ident(names.fromString("String")), + List.nil(), + List.from( + links.stream().map(link -> + treeMaker.Literal(link.columnInReferencedTable()) + ).toList() + ) + ) + ); + } + + public String getStoredReferringColumnsFieldName(String fieldName) { + return fieldName + "$referringColumns"; + } + + public String getStoredReferencedColumnsFieldName(String fieldName) { + return fieldName + "$referencedColumns"; + } } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java index b7aa895e..15efa90f 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java @@ -11,9 +11,15 @@ import java.util.Collection; public class BuilderProcessor extends AbstractBuilderProcessor { + private final Collection persistentValues; + private final Collection references; - public BuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation) { + public BuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation, + Collection persistentValues, Collection references + ) { super(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation, "Builder", "builder"); + this.persistentValues = persistentValues; + this.references = references; } public static boolean hasProcessed(JCTree.JCClassDecl classDecl) { @@ -34,12 +40,10 @@ protected void addImports() { @Override protected void process() { - Collection persistentValues = ParsedPersistentValue.extractPersistentValues(dataClassDecl, dataAnnotation, treeMaker, names); for (ParsedPersistentValue pv : persistentValues) { processValue(pv); } - Collection references = ParsedReference.extractReferences(dataClassDecl, dataAnnotation, treeMaker, names); for (ParsedReference ref : references) { processReference(ref); } @@ -50,45 +54,9 @@ protected void process() { private void processValue(ParsedPersistentValue pv) { - String schemaFieldName = pv.getFieldName() + "$schema"; - String tableFieldName = pv.getFieldName() + "$table"; - String columnFieldName = pv.getFieldName() + "$column"; - - JCTree.JCExpression stringType = treeMaker.Ident(names.fromString("String")); - JCTree.JCExpression schemaInit = treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("ValueUtils")), - names.fromString("parseValue") - ), - List.of( - treeMaker.Literal(pv.getSchema()) - ) - ); - JCTree.JCExpression tableInit = treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("ValueUtils")), - names.fromString("parseValue") - ), - List.of( - treeMaker.Literal(pv.getTable()) - ) - ); - JCTree.JCExpression columnInit = treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("ValueUtils")), - names.fromString("parseValue") - ), - List.of( - treeMaker.Literal(pv.getColumn()) - ) - ); - - JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, schemaFieldName, stringType, schemaInit); - JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, tableFieldName, stringType, tableInit); - JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, columnFieldName, stringType, columnInit); + storeSchema(pv.getFieldName(), pv.getSchema()); + storeTable(pv.getFieldName(), pv.getTable()); + storeColumn(pv.getFieldName(), pv.getColumn()); JCTree.JCExpression nullInit = treeMaker.Literal(TypeTag.BOT, null); diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/DummyProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/DummyProcessor.java new file mode 100644 index 00000000..f8e129e0 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/DummyProcessor.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.compiler.javac; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; +import java.util.Set; + +/** + * This processor does nothing. It exists so that gradle properly picks up the javac plugin. + */ +@SupportedAnnotationTypes("net.staticstudios.data.Data") +@SupportedSourceVersion(SourceVersion.RELEASE_21) +public class DummyProcessor extends AbstractProcessor { + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + return true; + } +} \ No newline at end of file diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java index c97d9f13..d01f8161 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java @@ -120,6 +120,11 @@ public static boolean isFQN(@Nullable Symbol.VarSymbol varSymbol, @NotNull Strin return null; // Treat empty strings as null } + public static boolean getBooleanAnnotationValue(@NotNull Attribute.Compound annotation, @NotNull String key) { + Boolean value = getAnnotationValue(annotation, Boolean.class, key); + return value != null ? value : false; + } + public static @Nullable T getAnnotationValue(@NotNull Attribute.Compound annotation, @NotNull Class type, @NotNull String key) { @@ -350,6 +355,29 @@ private static void getFields(@NotNull Symbol.ClassSymbol classSymbol, @NotNull // Fallback to a simple identifier (covers type variables / wildcards minimally) return treeMaker.Ident(names.fromString(type.toString())); } + + public static boolean isType(JCTree.JCExpression expression, Class clazz) { + if (expression.type == null) { + return expression.toString().equals(clazz.getCanonicalName()); + } + String typeName = expression.type.toString(); + return typeName.equals(clazz.getCanonicalName()); + } + + public static boolean isNumericType(JCTree.JCExpression expression) { + String typeName; + if (expression.type == null) { + typeName = expression.toString(); + } else { + typeName = expression.type.toString(); + } + return switch (typeName) { + case "byte", "short", "int", "long", "float", "double", + "java.lang.Byte", "java.lang.Short", "java.lang.Integer", + "java.lang.Long", "java.lang.Float", "java.lang.Double" -> true; + default -> false; + }; + } } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java index b9056eec..c19f2751 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java @@ -1,19 +1,28 @@ package net.staticstudios.data.compiler.javac; import com.sun.tools.javac.tree.JCTree; +import net.staticstudios.data.utils.Link; + +import java.util.List; class ParsedForeignPersistentValue extends ParsedPersistentValue { private final String insertStrategy; + private final List links; - public ParsedForeignPersistentValue(String fieldName, String schema, String table, String column, JCTree.JCExpression type, String insertStrategy) { - super(fieldName, schema, table, column, type); + public ParsedForeignPersistentValue(String fieldName, String schema, String table, String column, boolean nullable, JCTree.JCExpression type, String insertStrategy, List links) { + super(fieldName, schema, table, column, nullable, type); this.insertStrategy = insertStrategy; + this.links = links; } public String getInsertStrategy() { return insertStrategy; } + public List getLinks() { + return links; + } + @Override public String toString() { return "ParsedForeignPersistentValue{" + @@ -21,8 +30,10 @@ public String toString() { ", schema='" + getSchema() + '\'' + ", table='" + getTable() + '\'' + ", column='" + getColumn() + '\'' + + ", nullable=" + isNullable() + ", type=" + getType() + ", insertStrategy='" + insertStrategy + '\'' + + ", links=" + links + '}'; } } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java index 0bb675e2..acebc762 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java @@ -6,6 +6,7 @@ import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.Names; import net.staticstudios.data.utils.Constants; +import net.staticstudios.data.utils.Link; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -13,18 +14,20 @@ import java.util.List; import java.util.Objects; -class ParsedPersistentValue { +public class ParsedPersistentValue { private final String fieldName; private final String schema; private final String table; private final String column; + private final boolean nullable; private final JCTree.JCExpression type; - public ParsedPersistentValue(String fieldName, String schema, String table, String column, JCTree.JCExpression type) { + public ParsedPersistentValue(String fieldName, String schema, String table, String column, boolean nullable, JCTree.JCExpression type) { this.fieldName = fieldName; this.schema = schema; this.table = table; this.column = column; + this.nullable = nullable; this.type = type; } @@ -49,20 +52,28 @@ public static Collection extractPersistentValues(@NotNull String columnName = Objects.requireNonNull(JavaCPluginUtils.getStringAnnotationValue(annotation, "name")); String schemaValue; String tableValue; + boolean nullable; if (isIdColumnAnnotation) { schemaValue = dataAnnotation.getSchema(); tableValue = dataAnnotation.getTable(); + nullable = false; } else { - schemaValue = JavaCPluginUtils.getStringAnnotationValue(annotation, "schema"); - tableValue = JavaCPluginUtils.getStringAnnotationValue(annotation, "table"); - - if (schemaValue == null) { + if (isForeignColumnAnnotation) { + schemaValue = JavaCPluginUtils.getStringAnnotationValue(annotation, "schema"); + tableValue = JavaCPluginUtils.getStringAnnotationValue(annotation, "table"); + if (schemaValue == null) { + schemaValue = dataAnnotation.getSchema(); + } + if (tableValue == null) { + tableValue = dataAnnotation.getTable(); + } + } else { schemaValue = dataAnnotation.getSchema(); - } - if (tableValue == null) { tableValue = dataAnnotation.getTable(); } + + nullable = JavaCPluginUtils.getBooleanAnnotationValue(annotation, "nullable"); } JCTree.JCExpression typeExpression = JavaCPluginUtils.getGenericTypeExpression(treeMaker, names, varSymbol, 0); @@ -78,8 +89,10 @@ public static Collection extractPersistentValues(@NotNull schemaValue, tableValue, columnName, + nullable, typeExpression, - insertStrategy + insertStrategy, + Link.parseRawLinks(JavaCPluginUtils.getStringAnnotationValue(annotation, "link")) ); } else { parsedPersistentValue = new ParsedPersistentValue( @@ -87,6 +100,7 @@ public static Collection extractPersistentValues(@NotNull schemaValue, tableValue, columnName, + nullable, typeExpression ); } @@ -114,6 +128,10 @@ public String getColumn() { return column; } + public boolean isNullable() { + return nullable; + } + public JCTree.JCExpression getType() { return type; } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java index b3e7ab36..35a6ccc6 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java @@ -13,7 +13,7 @@ import java.util.Collection; import java.util.List; -class ParsedReference { +public class ParsedReference { private final String fieldName; private final List links; private final JCTree.JCExpression type; @@ -77,23 +77,4 @@ public String toString() { ", type=" + type + '}'; } - -// private void settings(That t) { -// //this method just needs to set the id column values. the column names will be known at compile time -// //then during insert() is where it get tricky, but create a dummy instance to get the runtime type and them set the values accordingly, in the proper table. table/schema is obtained from runtime type metadata -// -// -// //todo: if the referenced type is abstract, dont support setting in the builder. -// String schema; //lookup at runtime due to inheritance -// String table; //lookup at runtime due to inheritance -//// String[] linkingColumnsInReferringTable = referenceLinkingColumns_[fieldName]; //todo: we dont need this here but we will need it during insert() -// String[] linkingColumnsInReferencedTable = referenceLinkedColumns_[fieldName]; -// -// for (int i = 0; i < linkingColumnsInReferringTable.length; i++) { -//// String colInReferringTable = linkingColumnsInReferringTable[i]; -// String colInReferencedTable = linkingColumnsInReferencedTable[i]; -// this.refenceLinkingValues_[fieldName] = new Object[]... -// } -// } - //todo: when storing the links, we can have two cols. each a static string array storing the parsed columns. we know the linking columns, and the table schema/table from the reference itself. (what if the refernced data is abstract? } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java index a3f1b5c7..a249de82 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java @@ -1,24 +1,1140 @@ package net.staticstudios.data.compiler.javac; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.TypeTag; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.Names; +import net.staticstudios.data.utils.StringUtils; +import org.jetbrains.annotations.Nullable; -public class QueryBuilderProcessor extends AbstractBuilderProcessor { +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +public class QueryBuilderProcessor extends AbstractBuilderProcessor { + private final Collection persistentValues; + private final Collection references; + private final String whereClassName; - public QueryBuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation) { + public QueryBuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation, + Collection persistentValues, Collection references + ) { super(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation, "QueryBuilder", "query"); + this.persistentValues = persistentValues; + this.references = references; + + QueryWhereProcessor whereProcessor = new QueryWhereProcessor(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation); + this.whereClassName = whereProcessor.getBuilderClassName(); + whereProcessor.runProcessor(); } @Override protected void addImports() { JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "ValueUtils"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.query", "BaseQueryBuilder"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "Order"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.query", "BaseQueryWhere"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "java.util.function", "Function"); + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "java.util", "Collection"); + } + @Override + protected @Nullable SuperClass extending() { + return new SuperClass( + "BaseQueryBuilder", + List.of( + treeMaker.Ident(dataClassDecl.name), + treeMaker.Ident(names.fromString(whereClassName)) + ), + List.of( + treeMaker.Ident(names.fromString("dataManager")), + treeMaker.Select( + treeMaker.Ident(dataClassDecl.name), + names.fromString("class") + ), + treeMaker.NewClass( + null, + List.nil(), + treeMaker.Ident(names.fromString(whereClassName)), + List.nil(), + null + ) + ) + ); } @Override protected void process() { - //todo: impl + addWhereMethod(); + addLimitMethod(); + addOffsetMethod(); + + for (ParsedPersistentValue pv : persistentValues) { + processValue(pv); + } + } + + + private void processValue(ParsedPersistentValue pv) { + String schemaFieldName = storeSchema(pv.getFieldName(), pv.getSchema()); + String tableFieldName = storeTable(pv.getFieldName(), pv.getTable()); + String columnFieldName = storeColumn(pv.getFieldName(), pv.getColumn()); + + addOrderByMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName()); + } + + private void addOrderByMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName) { + JCTree.JCMethodDecl orderByMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("orderBy" + StringUtils.capitalize(fieldName)), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("order"), + treeMaker.Ident(names.fromString("Order")), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("setOrderBy") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString("order")) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(orderByMethod); + } + + private void addLimitMethod() { + JCTree.JCMethodDecl limitMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("limit"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("limit"), + treeMaker.TypeIdent(TypeTag.INT), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("setLimit") + ), + List.of( + treeMaker.Ident(names.fromString("limit")) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(limitMethod); + } + + private void addOffsetMethod() { + JCTree.JCMethodDecl offsetMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("offset"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("offset"), + treeMaker.TypeIdent(TypeTag.INT), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("setOffset") + ), + List.of( + treeMaker.Ident(names.fromString("offset")) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(offsetMethod); + } + + private void addWhereMethod() { + JCTree.JCMethodDecl whereMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("where"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("function"), + treeMaker.TypeApply( + treeMaker.Ident(names.fromString("Function")), + List.of( + treeMaker.Ident(names.fromString(whereClassName)), + treeMaker.Ident(names.fromString(whereClassName)) + ) + ), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("function")), + names.fromString("apply") + ), + List.of( + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("where") + ) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(whereMethod); + } + + class QueryWhereProcessor extends AbstractBuilderProcessor { + private String dataSchemaFieldName; + private String dataTableFieldName; + + public QueryWhereProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation) { + super(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation, "QueryWhere", null); + } + + @Override + protected void addImports() { + JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.query", "BaseQueryWhere"); + } + + @Override + protected @Nullable SuperClass extending() { + return new SuperClass( + "BaseQueryWhere", + List.nil(), + List.nil() + ); + } + + @Override + protected void process() { + dataSchemaFieldName = storeSchema("data", dataAnnotation.getSchema()); + dataTableFieldName = storeTable("data", dataAnnotation.getTable()); + + addGroupMethod(); + addAndMethod(); + addOrMethod(); + + for (ParsedPersistentValue pv : persistentValues) { + processValue(pv); + } + +// for (ParsedReference ref : references) { + //todo: process references and support is, isNot, isNull and isNotNull +// } + } + + private void processValue(ParsedPersistentValue pv) { + String schemaFieldName = storeSchema(pv.getFieldName(), pv.getSchema()); + String tableFieldName = storeTable(pv.getFieldName(), pv.getTable()); + String columnFieldName = storeColumn(pv.getFieldName(), pv.getColumn()); + + ParsedForeignPersistentValue fpv = null; + if (pv instanceof ParsedForeignPersistentValue _fpv) { + fpv = _fpv; + storeLinks(fpv.getFieldName(), fpv.getLinks()); + } + + addIsMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + addIsNotMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + + addIsInCollectionMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + addIsInArrayMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + addIsNotInCollectionMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + addIsNotInArrayMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + + if (pv.isNullable()) { + addIsNullMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), fpv); + addIsNotNullMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), fpv); + } + + if (JavaCPluginUtils.isType(pv.getType(), String.class)) { + addIsLikeMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), fpv); + addIsNotLikeMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), fpv); + } + + if (JavaCPluginUtils.isNumericType(pv.getType()) || JavaCPluginUtils.isType(pv.getType(), Timestamp.class)) { + addIsLessThanMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + addIsLessThanOrEqualToMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + addIsGreaterThanMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + addIsGreaterThanOrEqualToMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + addIsBetweenMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + addIsNotBetweenMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); + } + } + + private List clause(@Nullable ParsedForeignPersistentValue fpv, JCTree.JCStatement... statements) { + java.util.List list = new ArrayList<>(); + + if (fpv != null) { + String referencedSchemaFieldName = getStoredSchemaFieldName(fpv.getFieldName()); + String referencedTableFieldName = getStoredTableFieldName(fpv.getFieldName()); + String referencedColumnsFieldName = getStoredReferencedColumnsFieldName(fpv.getFieldName()); + String referringColumnsFieldName = getStoredReferringColumnsFieldName(fpv.getFieldName()); + list.add( + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("addInnerJoin") + ), + List.of( + treeMaker.Ident(names.fromString(dataSchemaFieldName)), + treeMaker.Ident(names.fromString(dataTableFieldName)), + treeMaker.Ident(names.fromString(referringColumnsFieldName)), + treeMaker.Ident(names.fromString(referencedSchemaFieldName)), + treeMaker.Ident(names.fromString(referencedTableFieldName)), + treeMaker.Ident(names.fromString(referencedColumnsFieldName) + ) + ) + ) + ) + ); + } + + list.addAll(Arrays.asList(statements)); + + return List.from(list); + } + + private void addIsMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "Is"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(fieldName), + type, + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("equalsClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString(fieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isMethod); + } + + private void addIsNotMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isNotMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsNot"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(fieldName), + type, + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("equalsClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString(fieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isNotMethod); + } + + private void addIsNullMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isNullMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsNull"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("nullClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isNullMethod); + } + + private void addIsNotNullMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isNotNullMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsNotNull"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("notNullClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isNotNullMethod); + } + + private void addIsInCollectionMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isInMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsIn"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(fieldName), + treeMaker.TypeApply( + treeMaker.Ident(names.fromString("Collection")), + List.of(type) + ), + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("inClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString(fieldName)), + names.fromString("toArray") + ), + List.nil() + ) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isInMethod); + } + + private void addIsInArrayMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isInMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsIn"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER | Flags.VARARGS), + names.fromString(fieldName), + treeMaker.TypeArray(type), + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("inClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString(fieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isInMethod); + } + + private void addIsNotInCollectionMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isNotInMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsNotIn"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(fieldName), + treeMaker.TypeApply( + treeMaker.Ident(names.fromString("Collection")), + List.of(type) + ), + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("notInClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString(fieldName)), + names.fromString("toArray") + ), + List.nil() + ) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isNotInMethod); + } + + private void addIsNotInArrayMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isNotInMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsNotIn"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER | Flags.VARARGS), + names.fromString(fieldName), + treeMaker.TypeArray(type), + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("notInClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString(fieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isNotInMethod); + } + + private void addIsLikeMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isLikeMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsLike"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("pattern"), + treeMaker.Ident(names.fromString("String")), + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("likeClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString("pattern")) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isLikeMethod); + } + + private void addIsNotLikeMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isNotLikeMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsNotLike"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("pattern"), + treeMaker.Ident(names.fromString("String")), + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("notLikeClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString("pattern")) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isNotLikeMethod); + } + + private void addIsLessThanMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl lessThanMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsLessThan"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(fieldName), + type, + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("lessThanClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString(fieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(lessThanMethod); + } + + private void addIsLessThanOrEqualToMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl lessThanOrEqualToMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsLessThanOrEqualTo"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(fieldName), + type, + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("lessThanOrEqualToClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString(fieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(lessThanOrEqualToMethod); + } + + private void addIsGreaterThanMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl greaterThanMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsGreaterThan"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(fieldName), + type, + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("greaterThanClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString(fieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(greaterThanMethod); + } + + private void addIsGreaterThanOrEqualToMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl greaterThanOrEqualToMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsGreaterThanOrEqualTo"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString(fieldName), + type, + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("greaterThanOrEqualToClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString(fieldName)) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(greaterThanOrEqualToMethod); + } + + private void addIsBetweenMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isBetweenMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsBetween"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("min"), + type, + null + ), + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("max"), + type, + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("betweenClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString("min")), + treeMaker.Ident(names.fromString("max")) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isBetweenMethod); + } + + private void addIsNotBetweenMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { + JCTree.JCMethodDecl isNotBetweenMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(fieldName + "IsNotBetween"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("min"), + type, + null + ), + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("max"), + type, + null + ) + ), + List.nil(), + treeMaker.Block(0, clause(fpv, + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("notBetweenClause") + ), + List.of( + treeMaker.Ident(names.fromString(schemaFieldName)), + treeMaker.Ident(names.fromString(tableFieldName)), + treeMaker.Ident(names.fromString(columnFieldName)), + treeMaker.Ident(names.fromString("min")), + treeMaker.Ident(names.fromString("max")) + ) + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(isNotBetweenMethod); + } + + private void addGroupMethod() { + JCTree.JCMethodDecl groupMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("group"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + names.fromString("function"), + treeMaker.TypeApply( + treeMaker.Ident(names.fromString("Function")), + List.of( + treeMaker.Ident(names.fromString(getBuilderClassName())), + treeMaker.Ident(names.fromString(getBuilderClassName())) + ) + ), + null + ) + ), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("pushGroup") + ), + List.nil() + ) + ), + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("function")), + names.fromString("apply") + ), + List.of( + treeMaker.Ident(names.fromString("this")) + ) + ) + ), + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("popGroup") + ), + List.nil() + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(groupMethod); + } + + private void addAndMethod() { + JCTree.JCMethodDecl andMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("and"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("andClause") + ), + List.nil() + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(andMethod); + } + + private void addOrMethod() { + JCTree.JCMethodDecl andMethod = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("or"), + treeMaker.Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + treeMaker.Block(0, List.of( + treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(names.fromString("super")), + names.fromString("orClause") + ), + List.nil() + ) + ), + treeMaker.Return( + treeMaker.Ident(names.fromString("this")) + ) + )), + null + ); + builderClassDecl.defs = builderClassDecl.defs.append(andMethod); + } } } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java index 556ac399..7ff13385 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java @@ -9,11 +9,10 @@ import com.sun.tools.javac.util.Names; import net.staticstudios.data.utils.Constants; +import java.util.Collection; + public class StaticDataJavacPlugin implements Plugin { - //TODO: properly implement this and match the IntelliJ plugin's behavior - // note: delegate a lot of behavior to utility classes to avoid generated unnecessary (and less reliable/more complex) code. - // i.e. AbstractQueryBuilder or something @Override public String getName() { @@ -42,8 +41,10 @@ public Void visitClass(ClassTree node, Void unused) { if (!BuilderProcessor.hasProcessed(classDecl)) { ParsedDataAnnotation dataAnnotation = ParsedDataAnnotation.extract(classDecl); - new BuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation).runProcessor(); - new QueryBuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation).runProcessor(); + Collection persistentValues = ParsedPersistentValue.extractPersistentValues(classDecl, dataAnnotation, treeMaker, names); + Collection references = ParsedReference.extractReferences(classDecl, dataAnnotation, treeMaker, names); + new BuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation, persistentValues, references).runProcessor(); + new QueryBuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation, persistentValues, references).runProcessor(); } } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/SuperClass.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/SuperClass.java new file mode 100644 index 00000000..8bf6c84f --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/SuperClass.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.List; + +public record SuperClass(String simpleName, List superParms, List args) { +} diff --git a/javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 00000000..b8722a66 --- /dev/null +++ b/javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +net.staticstudios.data.compiler.javac.DummyProcessor diff --git a/processor/build.gradle b/processor/build.gradle deleted file mode 100644 index e3103bab..00000000 --- a/processor/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -plugins { - id 'java' -} - -group = 'net.staticstudios' -version = '3.0.0-SNAPSHOT' - -repositories { - mavenCentral() -} - -java { - targetCompatibility = JavaVersion.VERSION_21 - sourceCompatibility = JavaVersion.VERSION_21 - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} - -dependencies { - implementation project(":utils") - implementation project(":annotations") - implementation 'com.palantir.javapoet:javapoet:0.7.0' - - compileOnly 'com.google.auto.service:auto-service-annotations:1.1.1' - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' -} - -test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java b/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java deleted file mode 100644 index 01434a55..00000000 --- a/processor/src/main/java/net/staticstudios/data/processor/DataProcessor.java +++ /dev/null @@ -1,158 +0,0 @@ -package net.staticstudios.data.processor; - -import com.palantir.javapoet.*; -import net.staticstudios.data.Data; -import net.staticstudios.data.InsertStrategy; - -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedSourceVersion; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.Element; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.PackageElement; -import javax.lang.model.element.TypeElement; -import javax.tools.Diagnostic; -import java.io.IOException; -import java.util.List; -import java.util.Set; - -@SupportedAnnotationTypes("net.staticstudios.data.Data") -@SupportedSourceVersion(SourceVersion.RELEASE_21) -public class DataProcessor extends AbstractProcessor { //TODO: delete this class since annotation processing is no longer used. - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - for (Element annotated : roundEnv.getElementsAnnotatedWith(Data.class)) { - if (!(annotated instanceof TypeElement type)) continue; - - try { - Data dataAnnotation = type.getAnnotation(Data.class); - assert dataAnnotation != null; - List metadataList = MetadataUtils.extractMetadata(type); - - generateFactory(type, dataAnnotation, metadataList); - new QueryFactory(processingEnv, type, dataAnnotation, metadataList).generateQueryBuilder(); - } catch (IOException e) { - processingEnv.getMessager().printMessage( - Diagnostic.Kind.ERROR, - "Failed to generate factory: " + e.getMessage(), - annotated - ); - } - } - return true; - } - - private void generateFactory(TypeElement entityType, Data dataAnnotation, List metadataList) throws IOException { - if (entityType.getModifiers().contains(Modifier.ABSTRACT)) { - return; - } - - String entityName = entityType.getSimpleName().toString(); - String factoryName = entityName + "Factory"; - PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(entityType); - String packageName = packageElement.isUnnamed() ? "" : packageElement.getQualifiedName().toString(); - - ClassName entityClass = ClassName.get(packageName, entityName); - ClassName dataManager = ClassName.get("net.staticstudios.data", "DataManager"); - ClassName insertMode = ClassName.get("net.staticstudios.data", "InsertMode"); - ClassName insertContext = ClassName.get("net.staticstudios.data.insert", "InsertContext"); - - - TypeSpec.Builder factoryBuilder = TypeSpec.classBuilder(factoryName); - TypeSpec.Builder builderType = TypeSpec.classBuilder("Builder") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addField(dataManager, "dataManager", Modifier.PRIVATE, Modifier.FINAL) - .addMethod(MethodSpec.constructorBuilder() - .addParameter(dataManager, "dataManager") - .addStatement("this.dataManager = dataManager") - .build()); - - //todo: support collections and references. - - - MethodSpec.Builder insertCtxMethod = MethodSpec.methodBuilder("insert") - .addModifiers(Modifier.PUBLIC) - .returns(TypeName.VOID) - .addParameter(insertContext, "ctx"); - - for (Metadata metadata : metadataList) { - if (metadata instanceof PersistentValueMetadata persistentValueMetadata) { - SchemaTableColumnStatics statics = SchemaTableColumnStatics.generateSchemaTableColumnStatics(builderType, persistentValueMetadata); - builderType.addField(persistentValueMetadata.genericType(), persistentValueMetadata.fieldName(), Modifier.PRIVATE); - - builderType.addMethod(MethodSpec.methodBuilder(persistentValueMetadata.fieldName()) - .addModifiers(Modifier.PUBLIC) - .returns(ClassName.get(packageName, factoryName, "Builder")) - .addParameter(persistentValueMetadata.genericType(), persistentValueMetadata.fieldName()) - .addStatement("this.$N = $N", persistentValueMetadata.fieldName(), persistentValueMetadata.fieldName()) - .addStatement("return this") - .build()); - - if (persistentValueMetadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { -// insertCtxMethod.beginControlFlow("if (this.$N != null)", persistentValueMetadata.fieldName()); - insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N, $T.$L)", - statics.schemaFieldName(), - statics.tableFieldName(), - statics.columnFieldName(), - persistentValueMetadata.fieldName(), - InsertStrategy.class, - foreignPersistentValueMetadata.insertStrategy()); - } else { - insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N, null)", - statics.schemaFieldName(), - statics.tableFieldName(), - statics.columnFieldName(), - persistentValueMetadata.fieldName()); - } - - if (persistentValueMetadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { -// for (ForeignLink link : MetadataUtils.makeFPVStatics(builderType, foreignPersistentValueMetadata, metadataList, dataAnnotation, statics)) { -// insertCtxMethod.addStatement("ctx.set($N, $N, $N, this.$N, null)", -// statics.schemaFieldName(), -// statics.tableFieldName(), -// link.foreignColumnFieldName(), -// link.localColumnMetadata().fieldName()); -// } //todo: testing what happens and if this is needed -// insertCtxMethod.endControlFlow(); - } - } - } - - MethodSpec.Builder insertModeMethod = MethodSpec.methodBuilder("insert") - .addModifiers(Modifier.PUBLIC) - .returns(entityClass) - .addParameter(insertMode, "mode") - .addStatement("$T ctx = dataManager.createInsertContext()", insertContext) - .addStatement("this.insert(ctx)") - .addStatement("return ctx.insert(mode).get($T.class)", entityClass); - - builderType.addMethod(insertModeMethod.build()); - builderType.addMethod(insertCtxMethod.build()); - - TypeSpec factory = factoryBuilder - .addModifiers(Modifier.PUBLIC, Modifier.FINAL) - .addMethod(MethodSpec.constructorBuilder() - .addModifiers(Modifier.PRIVATE) - .build()) - .addMethod(MethodSpec.methodBuilder("builder") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(ClassName.get(packageName, factoryName, "Builder")) - .addParameter(dataManager, "dataManager") - .addStatement("return new Builder(dataManager)") - .build()) - .addMethod(MethodSpec.methodBuilder("builder") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(ClassName.get(packageName, factoryName, "Builder")) - .addStatement("return new Builder(DataManager.getInstance())") - .build()) - .addType(builderType.build()) - .build(); - - JavaFile.builder(packageName, factory) - .indent(" ") - .build() - .writeTo(processingEnv.getFiler()); - } -} \ No newline at end of file diff --git a/processor/src/main/java/net/staticstudios/data/processor/ForeignLink.java b/processor/src/main/java/net/staticstudios/data/processor/ForeignLink.java deleted file mode 100644 index daade031..00000000 --- a/processor/src/main/java/net/staticstudios/data/processor/ForeignLink.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.staticstudios.data.processor; - -public record ForeignLink(String foreignColumnFieldName, PersistentValueMetadata localColumnMetadata) { -} diff --git a/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java b/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java deleted file mode 100644 index 424a59b1..00000000 --- a/processor/src/main/java/net/staticstudios/data/processor/ForeignPersistentValueMetadata.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.staticstudios.data.processor; - -import com.palantir.javapoet.TypeName; -import net.staticstudios.data.InsertStrategy; - -import java.util.Map; - -public class ForeignPersistentValueMetadata extends PersistentValueMetadata { - private final Map links; - private final InsertStrategy insertStrategy; - - public ForeignPersistentValueMetadata(String schema, String table, String column, String fieldName, TypeName genericType, boolean nullable, Map links, InsertStrategy insertStrategy) { - super(schema, table, column, fieldName, genericType, nullable); - this.links = links; - this.insertStrategy = insertStrategy; - } - - public Map links() { - return links; - } - - public InsertStrategy insertStrategy() { - return insertStrategy; - } -} diff --git a/processor/src/main/java/net/staticstudios/data/processor/Metadata.java b/processor/src/main/java/net/staticstudios/data/processor/Metadata.java deleted file mode 100644 index 00d240dd..00000000 --- a/processor/src/main/java/net/staticstudios/data/processor/Metadata.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.staticstudios.data.processor; - -public interface Metadata { -} diff --git a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java b/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java deleted file mode 100644 index 08e8bf20..00000000 --- a/processor/src/main/java/net/staticstudios/data/processor/MetadataUtils.java +++ /dev/null @@ -1,151 +0,0 @@ -package net.staticstudios.data.processor; - -import com.palantir.javapoet.ClassName; -import com.palantir.javapoet.FieldSpec; -import com.palantir.javapoet.TypeName; -import com.palantir.javapoet.TypeSpec; -import net.staticstudios.data.*; -import net.staticstudios.data.utils.StringUtils; - -import javax.lang.model.element.Modifier; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.ElementFilter; -import java.util.*; - -public class MetadataUtils { - private static final String FQN_OBJECT = Object.class.getName(); - private static final String FQN_PERSISTENT_VALUE = "net.staticstudios.data.PersistentValue"; - - public static List extractMetadata(TypeElement typeElement) { - Data dataAnnotation = typeElement.getAnnotation(Data.class); - if (dataAnnotation == null) { - return Collections.emptyList(); - } - - - List metadata = new ArrayList<>(); - extractMetadata(dataAnnotation, metadata, typeElement); - - return metadata; - } - - private static void extractMetadata(Data dataAnnotation, List list, TypeElement typeElement) { - for (VariableElement field : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) { - if (field.getModifiers().contains(Modifier.STATIC)) { - continue; - } - - TypeMirror mirror = field.asType(); - if (!(mirror instanceof DeclaredType declaredType)) { - continue; - } - TypeElement fieldTypeElement = (TypeElement) declaredType.asElement(); - if (fieldTypeElement.getQualifiedName().toString().equals(FQN_PERSISTENT_VALUE)) { - TypeMirror innerType = declaredType.getTypeArguments().getFirst(); - PersistentValueMetadata metadata = getPersistentValueMetadata(dataAnnotation, field, TypeName.get(innerType)); - list.add(metadata); - } - } - - TypeMirror superClass = typeElement.getSuperclass(); - if (superClass instanceof DeclaredType declaredSuper && declaredSuper.asElement() instanceof TypeElement superElement) { - if (superClass.getKind() != TypeKind.NONE && superClass.getKind() != TypeKind.VOID && !superElement.getQualifiedName().toString().equals(FQN_OBJECT)) { - extractMetadata(dataAnnotation, list, superElement); - } - } - } - - private static PersistentValueMetadata getPersistentValueMetadata(Data dataAnnotation, VariableElement field, TypeName typeName) { - String schemaName = null; - String tableName = null; - String columnName = null; - boolean nullable = false; - - IdColumn idColumn = field.getAnnotation(IdColumn.class); - Column column = field.getAnnotation(Column.class); - ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); - Insert insert = field.getAnnotation(Insert.class); - - if (idColumn != null) { - tableName = dataAnnotation.table(); - schemaName = dataAnnotation.schema(); - columnName = idColumn.name(); - } else if (column != null) { - tableName = dataAnnotation.table(); - schemaName = dataAnnotation.schema(); - columnName = column.name(); - nullable = column.nullable(); - } else if (foreignColumn != null) { - tableName = foreignColumn.table().isEmpty() ? dataAnnotation.table() : foreignColumn.table(); - schemaName = foreignColumn.schema().isEmpty() ? dataAnnotation.schema() : foreignColumn.schema(); - columnName = foreignColumn.name(); - nullable = foreignColumn.nullable(); - } - - if (idColumn != null || column != null) { - return new PersistentValueMetadata( - schemaName, - tableName, - columnName, - field.getSimpleName().toString(), - typeName, - nullable - ); - } - if (foreignColumn != null) { - Map links = new HashMap<>(); - for (String link : StringUtils.parseCommaSeperatedList(foreignColumn.link())) { - String[] parts = link.split("="); - if (parts.length != 2) { - throw new IllegalArgumentException("Invalid link format in @ForeignColumn: " + link); - } - links.put(parts[0].trim(), parts[1].trim()); - } - - InsertStrategy insertStrategy = insert != null ? insert.value() : InsertStrategy.PREFER_EXISTING; - - return new ForeignPersistentValueMetadata( - schemaName, - tableName, - columnName, - field.getSimpleName().toString(), - typeName, - nullable, - links, - insertStrategy - ); - } - throw new IllegalStateException("Field " + field.getSimpleName() + " is not annotated with @IdColumn, @Column, or @ForeignColumn"); - } - - public static List makeFPVStatics(TypeSpec.Builder builder, ForeignPersistentValueMetadata foreignPersistentValueMetadata, List metadataList, Data dataAnnotation, SchemaTableColumnStatics statics) { - List staticNames = new ArrayList<>(); - int i = 0; - for (Map.Entry link : foreignPersistentValueMetadata.links().entrySet()) { - String localColumn = link.getKey(); - String foreignColumn = link.getValue(); - - PersistentValueMetadata localColumnMetadata = metadataList.stream() - .filter(m -> m instanceof PersistentValueMetadata) - .map(m -> (PersistentValueMetadata) m) - .filter(m -> m.column().equals(localColumn) && m.table().equals(dataAnnotation.table()) && m.schema().equals(dataAnnotation.schema())) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Could not find local column metadata for link: " + localColumn)); - - String columnLinkFieldName = statics.columnFieldName() + "$" + localColumnMetadata.fieldName() + "$" + i; - - builder.addField(FieldSpec.builder(String.class, columnLinkFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), foreignColumn) - .build()); - - staticNames.add(new ForeignLink(columnLinkFieldName, localColumnMetadata)); - i++; - } - return staticNames; - } - -} diff --git a/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java b/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java deleted file mode 100644 index d2630e28..00000000 --- a/processor/src/main/java/net/staticstudios/data/processor/PersistentValueMetadata.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.staticstudios.data.processor; - -import com.palantir.javapoet.TypeName; - -public class PersistentValueMetadata implements Metadata { - private final String schema; - private final String table; - private final String column; - private final String fieldName; - private final TypeName genericType; - private final boolean nullable; - - - public PersistentValueMetadata(String schema, String table, String column, String fieldName, - TypeName genericType, boolean nullable) { - this.schema = schema; - this.table = table; - this.column = column; - this.fieldName = fieldName; - this.genericType = genericType; - this.nullable = nullable; - } - - public String schema() { - return schema; - } - - public String table() { - return table; - } - - public String column() { - return column; - } - - public String fieldName() { - return fieldName; - } - - public boolean nullable() { - return nullable; - } - - public TypeName genericType() { - return genericType; - } -} diff --git a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java b/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java deleted file mode 100644 index d2fcd11c..00000000 --- a/processor/src/main/java/net/staticstudios/data/processor/QueryFactory.java +++ /dev/null @@ -1,323 +0,0 @@ -package net.staticstudios.data.processor; - -import com.palantir.javapoet.*; -import net.staticstudios.data.Data; - -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.PackageElement; -import javax.lang.model.element.TypeElement; -import java.io.IOException; -import java.sql.Timestamp; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; - -public class QueryFactory { - private static final ClassName DATA_MANAGER_CLASS_NAME = ClassName.get("net.staticstudios.data", "DataManager"); - private static final ClassName ABSTRACT_QUERY_BUILDER_CLASS_NAME = ClassName.get("net.staticstudios.data.query", "AbstractQueryBuilder"); - private final ProcessingEnvironment processingEnv; - private final TypeElement entityType; - private final Data dataAnnotation; - private final List metadataList; - private final String entityName; - private final String queryName; - private final String packageName; - private final ClassName entityClass; - private final ClassName builderClassName; - private final ClassName conditionalBuilderClassName; - private final Map> foreignPersistentValueLinkFieldNames = new HashMap<>(); - private final Map persistentValueStatics = new HashMap<>(); - - public QueryFactory(ProcessingEnvironment processingEnv, TypeElement entityType, Data dataAnnotation, List metadataList) { - this.processingEnv = processingEnv; - this.entityType = entityType; - this.dataAnnotation = dataAnnotation; - this.metadataList = metadataList; - this.entityName = entityType.getSimpleName().toString(); - this.queryName = entityName + "Query"; - PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(entityType); - this.packageName = packageElement.isUnnamed() ? "" : packageElement.getQualifiedName().toString(); - this.entityClass = ClassName.get(packageName, entityName); - this.builderClassName = ClassName.get(packageName, queryName, "Builder"); - this.conditionalBuilderClassName = ClassName.get(packageName, queryName, "ConditionalBuilder"); - } - - public void generateQueryBuilder() throws IOException { - TypeSpec.Builder queryBuilder = TypeSpec.classBuilder(queryName); - - foreignPersistentValueLinkFieldNames.clear(); - for (Metadata metadata : metadataList) { - if (!(metadata instanceof PersistentValueMetadata persistentValueMetadata)) { - continue; - } - SchemaTableColumnStatics statics = SchemaTableColumnStatics.generateSchemaTableColumnStatics(queryBuilder, persistentValueMetadata); - persistentValueStatics.put(persistentValueMetadata, statics); - if (metadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { - foreignPersistentValueLinkFieldNames.put(foreignPersistentValueMetadata, MetadataUtils.makeFPVStatics(queryBuilder, foreignPersistentValueMetadata, metadataList, dataAnnotation, statics)); - } - } - - - TypeSpec.Builder conditionalBuilderType = TypeSpec.classBuilder("ConditionalBuilder") - .superclass(ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query", "AbstractConditionalBuilder"), builderClassName, conditionalBuilderClassName, entityClass)) - .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addMethod(MethodSpec.constructorBuilder() - .addParameter(ParameterizedTypeName.get(ABSTRACT_QUERY_BUILDER_CLASS_NAME, builderClassName, conditionalBuilderClassName, entityClass), "queryBuilder") - .addStatement("super(queryBuilder)") - .build()); - - TypeSpec.Builder builderType = TypeSpec.classBuilder("Builder") - .superclass(ParameterizedTypeName.get(ABSTRACT_QUERY_BUILDER_CLASS_NAME, builderClassName, conditionalBuilderClassName, entityClass)) - .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - .addMethod(MethodSpec.constructorBuilder() - .addParameter(DATA_MANAGER_CLASS_NAME, "dataManager") - .addStatement("super(dataManager, $N.class)", entityClass.simpleName()) - .build()) - .addMethod(MethodSpec.methodBuilder("createInstance") - .addModifiers(Modifier.PROTECTED) - .returns(ParameterizedTypeName.get(ABSTRACT_QUERY_BUILDER_CLASS_NAME, builderClassName, conditionalBuilderClassName, entityClass)) - .addStatement("return new Builder(this.dataManager)") - .build()) - .addMethod(MethodSpec.methodBuilder("createConditionalInstance") - .addModifiers(Modifier.PROTECTED) - .returns(conditionalBuilderClassName) - .addStatement("return new ConditionalBuilder(this)") - .build()); - - for (Metadata metadata : metadataList) { - if (metadata instanceof PersistentValueMetadata persistentValueMetadata) { - makeEqualsClause(builderType, persistentValueMetadata); - makeNotEqualsClause(builderType, persistentValueMetadata); - makeInClause(builderType, persistentValueMetadata); - makeNotInClause(builderType, persistentValueMetadata); - - if (persistentValueMetadata.nullable()) { - makeNullClause(builderType, persistentValueMetadata); - makeNotNullClause(builderType, persistentValueMetadata); - } - - if (TypeName.FLOAT.box().equals(persistentValueMetadata.genericType()) - || TypeName.DOUBLE.box().equals(persistentValueMetadata.genericType()) - || TypeName.LONG.box().equals(persistentValueMetadata.genericType()) - || TypeName.SHORT.box().equals(persistentValueMetadata.genericType()) - || TypeName.BYTE.box().equals(persistentValueMetadata.genericType()) - || TypeName.INT.box().equals(persistentValueMetadata.genericType()) - || TypeName.get(Timestamp.class).equals(persistentValueMetadata.genericType()) - ) { - makeLessThanClause(builderType, persistentValueMetadata); - makeLessThanOrEqualToClause(builderType, persistentValueMetadata); - makeGreaterThanClause(builderType, persistentValueMetadata); - makeGreaterThanOrEqualToClause(builderType, persistentValueMetadata); - makeBetweenClause(builderType, persistentValueMetadata); - } - - if (TypeName.get(String.class).equals(persistentValueMetadata.genericType())) { - makeLikeClause(builderType, persistentValueMetadata); - makeNotLikeClause(builderType, persistentValueMetadata); - } - - makeOrderByClause(builderType, persistentValueMetadata, builderClassName); - makeOrderByClause(conditionalBuilderType, persistentValueMetadata, conditionalBuilderClassName); - } - } - - TypeSpec query = queryBuilder - .addModifiers(Modifier.PUBLIC, Modifier.FINAL) - .addMethod(MethodSpec.constructorBuilder() - .addModifiers(Modifier.PRIVATE) - .build()) - .addMethod(MethodSpec.methodBuilder("where") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(ClassName.get(packageName, queryName, "Builder")) - .addParameter(DATA_MANAGER_CLASS_NAME, "dataManager") - .addStatement("return new Builder(dataManager)") - .build()) - .addMethod(MethodSpec.methodBuilder("where") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(ClassName.get(packageName, queryName, "Builder")) - .addStatement("return new Builder(DataManager.getInstance())") - .build()) - .addType(builderType.build()) - .addType(conditionalBuilderType.build()) - .build(); - - JavaFile.builder(packageName, query) - .indent(" ") - .build() - .writeTo(processingEnv.getFiler()); - } - - - private void makeNotEqualsClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotEqualsClause"), "IsNot"); - } - - private void makeEqualsClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "EqualsClause"), "Is"); - } - - private void makeInClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "InClause"), "IsIn", true); - makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "InClause"), "IsIn", false, ParameterizedTypeName.get(ClassName.get(List.class), persistentValueMetadata.genericType())); - } - - private void makeNotInClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotInClause"), "IsNotIn", true); - makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotInClause"), "IsNotIn", false, ParameterizedTypeName.get(ClassName.get(List.class), persistentValueMetadata.genericType())); - } - - private void makeLessThanClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, - ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "LessThanClause"), persistentValueMetadata.genericType()), - "IsLessThan"); - } - - private void makeLessThanOrEqualToClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, - ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "LessThanOrEqualToClause"), persistentValueMetadata.genericType()), - "IsLessThanOrEqualTo"); - } - - private void makeGreaterThanClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, - ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "GreaterThanClause"), persistentValueMetadata.genericType()), - "IsGreaterThan"); - } - - private void makeGreaterThanOrEqualToClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, - ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "GreaterThanOrEqualToClause"), persistentValueMetadata.genericType()), - "IsGreaterThanOrEqualTo"); - } - - private void makeNullClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeNonValuedClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NullClause"), "IsNull"); - } - - private void makeNotNullClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeNonValuedClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotNullClause"), "IsNotNull"); - } - - private void makeLikeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "LikeClause"), "IsLike"); - } - - private void makeNotLikeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - makeClause(builderType, persistentValueMetadata, ClassName.get("net.staticstudios.data.query.clause", "NotLikeClause"), "IsNotLike"); - } - - private void makeBetweenClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - builderType.addMethod(MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + "IsBetween") - .addModifiers(Modifier.PUBLIC) - .returns(conditionalBuilderClassName) - .addParameter(persistentValueMetadata.genericType(), "start") - .addParameter(persistentValueMetadata.genericType(), "end") - .addStatement("return set(new $T($N, $N, $N, start, end))", - ParameterizedTypeName.get(ClassName.get("net.staticstudios.data.query.clause", "BetweenClause"), persistentValueMetadata.genericType()), - persistentValueMetadata.fieldName() + "$schema", - persistentValueMetadata.fieldName() + "$table", - persistentValueMetadata.fieldName() + "$column" - ) - .build()); - } - - private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix) { - makeClause(builderType, persistentValueMetadata, clauseTypeName, suffix, false); - } - - private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix, boolean varargs) { - makeClause(builderType, persistentValueMetadata, clauseTypeName, suffix, varargs, persistentValueMetadata.genericType()); - } - - private void makeClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix, boolean varargs, TypeName parameterType) { - MethodSpec.Builder builder = MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + suffix) - .addModifiers(Modifier.PUBLIC) - .returns(conditionalBuilderClassName) - .addParameter(varargs ? ArrayTypeName.of(parameterType) : parameterType, persistentValueMetadata.fieldName()); - - handleForeignPersistentValue(builder, persistentValueMetadata); - - builder.varargs(varargs) - .addStatement("return set(new $T($N, $N, $N, $N))", - clauseTypeName, - persistentValueMetadata.fieldName() + "$schema", - persistentValueMetadata.fieldName() + "$table", - persistentValueMetadata.fieldName() + "$column", - persistentValueMetadata.fieldName()); - - - builderType.addMethod(builder.build()); - } - - private void makeNonValuedClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, TypeName clauseTypeName, String suffix) { - MethodSpec.Builder builder = MethodSpec.methodBuilder(persistentValueMetadata.fieldName() + suffix) - .addModifiers(Modifier.PUBLIC) - .returns(conditionalBuilderClassName); - - handleForeignPersistentValue(builder, persistentValueMetadata); - - builder.addStatement("return set(new $T($N, $N, $N))", - clauseTypeName, - persistentValueMetadata.fieldName() + "$schema", - persistentValueMetadata.fieldName() + "$table", - persistentValueMetadata.fieldName() + "$column" - ); - - builderType.addMethod(builder.build()); - } - - private void makeOrderByClause(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata, ClassName returnType) { - String methodName = "orderBy" + persistentValueMetadata.fieldName().substring(0, 1).toUpperCase() + persistentValueMetadata.fieldName().substring(1); - MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName) - .addModifiers(Modifier.PUBLIC) - .returns(returnType) - .addParameter(ClassName.get("net.staticstudios.data", "Order"), "order"); - - handleForeignPersistentValue(builder, persistentValueMetadata); - - builder.addStatement("orderBy($N, $N, $N, order)", - persistentValueMetadata.fieldName() + "$schema", - persistentValueMetadata.fieldName() + "$table", - persistentValueMetadata.fieldName() + "$column" - ) - .addStatement("return this"); - - builderType.addMethod(builder.build()); - } - - private void handleForeignPersistentValue(MethodSpec.Builder builder, PersistentValueMetadata persistentValueMetadata) { - if (persistentValueMetadata instanceof ForeignPersistentValueMetadata foreignPersistentValueMetadata) { - AtomicReference localStatics = new AtomicReference<>(); - String[] columns = foreignPersistentValueMetadata.links().keySet().stream() - .map(column -> metadataList.stream() - .filter(m -> m instanceof PersistentValueMetadata) - .map(m -> (PersistentValueMetadata) m) - .filter(m -> m.column().equals(column) && m.table().equals(dataAnnotation.table()) && m.schema().equals(dataAnnotation.schema())) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Could not find local column metadata for link: " + column))) - .peek(m -> { - if (localStatics.get() == null) { - localStatics.set(persistentValueStatics.get(m)); - } - }) - .map(m -> persistentValueStatics.get(m).columnFieldName()) - .toArray(String[]::new); - String[] foreignColumns = foreignPersistentValueLinkFieldNames.get(foreignPersistentValueMetadata).stream() - .map(ForeignLink::foreignColumnFieldName) - .toArray(String[]::new); - - SchemaTableColumnStatics foreignStatics = persistentValueStatics.get(persistentValueMetadata); - - builder.addStatement("super.innerJoin($N, $N, $L, $N, $N, $L)", - localStatics.get().schemaFieldName(), - localStatics.get().tableFieldName(), - CodeBlock.of("new String[]{ $L }", String.join(", ", columns)), - foreignStatics.schemaFieldName(), - foreignStatics.tableFieldName(), - CodeBlock.of("new String[]{ $L }", String.join(", ", foreignColumns)) - ); - } - } -} diff --git a/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java b/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java deleted file mode 100644 index cef32120..00000000 --- a/processor/src/main/java/net/staticstudios/data/processor/SchemaTableColumnStatics.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.staticstudios.data.processor; - -import com.palantir.javapoet.ClassName; -import com.palantir.javapoet.FieldSpec; -import com.palantir.javapoet.TypeSpec; - -import javax.lang.model.element.Modifier; - -public record SchemaTableColumnStatics(String schemaFieldName, String tableFieldName, String columnFieldName) { - public static SchemaTableColumnStatics generateSchemaTableColumnStatics(TypeSpec.Builder builderType, PersistentValueMetadata persistentValueMetadata) { - String schemaFieldName = persistentValueMetadata.fieldName() + "$schema"; - String tableFieldName = persistentValueMetadata.fieldName() + "$table"; - String columnFieldName = persistentValueMetadata.fieldName() + "$column"; - - // since we support env variables in the name, parse these at runtime. - builderType.addField(FieldSpec.builder(String.class, schemaFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.schema()) - .build()); - builderType.addField(FieldSpec.builder(String.class, tableFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.table()) - .build()); - builderType.addField(FieldSpec.builder(String.class, columnFieldName, Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) - .initializer("$T.parseValue($S)", ClassName.get("net.staticstudios.data.util", "ValueUtils"), persistentValueMetadata.column()) - .build()); - - return new SchemaTableColumnStatics(schemaFieldName, tableFieldName, columnFieldName); - } -} diff --git a/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors b/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors deleted file mode 100644 index 5d220ddc..00000000 --- a/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors +++ /dev/null @@ -1,2 +0,0 @@ -net.staticstudios.data.processor.DataProcessor,isolating - diff --git a/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor deleted file mode 100644 index c8df4050..00000000 --- a/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor +++ /dev/null @@ -1 +0,0 @@ -net.staticstudios.data.processor.DataProcessor diff --git a/settings.gradle b/settings.gradle index 2199725e..7e26f8e3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +1,6 @@ rootProject.name = 'static-data' -include 'annotations' -include 'processor' include 'benchmark' include 'core' include 'javac-plugin' diff --git a/utils/src/main/java/net/staticstudios/data/utils/StringUtils.java b/utils/src/main/java/net/staticstudios/data/utils/StringUtils.java index 0df40733..ed6b30cc 100644 --- a/utils/src/main/java/net/staticstudios/data/utils/StringUtils.java +++ b/utils/src/main/java/net/staticstudios/data/utils/StringUtils.java @@ -6,4 +6,11 @@ public class StringUtils { public static List parseCommaSeperatedList(String input) { return List.of(input.split(",")); } + + public static String capitalize(String input) { + if (input == null || input.isEmpty()) { + return input; + } + return input.substring(0, 1).toUpperCase() + input.substring(1); + } } From 3cf538404f2fb7539835fda590141c58c0bf1074 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 1 Nov 2025 06:25:59 -0400 Subject: [PATCH 42/75] cleanup --- core/build.gradle | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index 18b423a7..e679ebcc 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -23,19 +23,6 @@ dependencies { implementation 'net.staticstudios:static-utils:1.0.6-SNAPSHOT' implementation 'com.h2database:h2:2.3.232' implementation 'org.jetbrains:annotations:24.0.1' -// api project(":annotations") -// implementation project(":annotations") -// annotationProcessor project(":processor") -// compileOnly project(":processor") - -// -// testAnnotationProcessor project(":processor") -// testCompileOnly project(":processor") - -// testImplementation(platform('org.junit:junit-bom:5.10.3')) -// testImplementation('org.junit.jupiter:junit-jupiter') -// testImplementation('org.junit-pioneer:junit-pioneer:2.3.0'); -// testRuntimeOnly('org.junit.platform:junit-platform-launcher') testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' @@ -44,7 +31,7 @@ dependencies { testImplementation("org.testcontainers:postgresql:1.19.8") testImplementation("com.redis:testcontainers-redis:2.2.2") testImplementation("org.slf4j:slf4j-log4j12:2.0.16") - compileOnly project(':javac-plugin') //TODO: java-c + compileOnly project(':javac-plugin') annotationProcessor project(':javac-plugin') testCompileOnly project(':javac-plugin') testAnnotationProcessor project(':javac-plugin') From 3384bb226c1daf57089490dc421347ffc56d41db Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 5 Nov 2025 11:29:02 -0500 Subject: [PATCH 43/75] switch to AP --- annotations/build.gradle | 20 + .../java/net/staticstudios/data/Column.java | 0 .../java/net/staticstudios/data/Data.java | 0 .../net/staticstudios/data/DefaultValue.java | 0 .../java/net/staticstudios/data/Delete.java | 0 .../staticstudios/data/DeleteStrategy.java | 0 .../net/staticstudios/data/ForeignColumn.java | 0 .../java/net/staticstudios/data/IdColumn.java | 0 .../java/net/staticstudios/data/Insert.java | 0 .../net/staticstudios/data/InsertMode.java | 0 .../staticstudios/data/InsertStrategy.java | 0 .../net/staticstudios/data/ManyToMany.java | 0 .../net/staticstudios/data/OneToMany.java | 0 .../java/net/staticstudios/data/OneToOne.java | 0 .../java/net/staticstudios/data/Order.java | 0 .../staticstudios/data/UpdateInterval.java | 0 .../staticstudios/data/UpdateStrategy.java | 0 benchmark/build.gradle | 8 +- .../data/benchmark/StaticDataBenchmark.java | 39 +- core/build.gradle | 44 +- .../data/PersistentValueTest.java | 34 +- .../net/staticstudios/data/SQLParseTest.java | 4 +- javac-plugin/build.gradle | 1 + .../javac/AbstractBuilderProcessor.java | 309 ----- .../data/compiler/javac/DummyProcessor.java | 21 - .../data/compiler/javac/JavaCPluginUtils.java | 383 ------ .../data/compiler/javac/Parent.java | 12 + .../data/compiler/javac/ParsedAnnotation.java | 14 - .../javac/ParsedColumnAnnotation.java | 61 - .../compiler/javac/ParsedDataAnnotation.java | 37 - .../compiler/javac/ParsedPersistentValue.java | 149 --- .../data/compiler/javac/ParsedReference.java | 80 -- .../data/compiler/javac/Permit.java | 44 + .../data/compiler/javac/ProcessorContext.java | 24 + .../compiler/javac/QueryBuilderProcessor.java | 1140 ----------------- .../compiler/javac/StaticDataJavacPlugin.java | 57 - .../compiler/javac/StaticDataProcessor.java | 262 ++++ .../data/compiler/javac/SuperClass.java | 7 - .../javac/javac/AbstractBuilderProcessor.java | 391 ++++++ .../javac/{ => javac}/BuilderProcessor.java | 396 +++--- .../ParsedForeignPersistentValue.java | 11 +- .../javac/javac/ParsedPersistentValue.java | 152 +++ .../compiler/javac/javac/ParsedReference.java | 81 ++ .../javac/javac/PositionedTreeMaker.java | 181 +++ .../javac/javac/QueryBuilderProcessor.java | 1111 ++++++++++++++++ .../data/compiler/javac/javac/SuperClass.java | 7 + .../data/compiler/javac/util/SimpleField.java | 6 + .../data/compiler/javac/util/TypeUtils.java | 136 ++ .../services/com.sun.source.util.Plugin | 1 - .../javax.annotation.processing.Processor | 2 +- settings.gradle | 3 +- 51 files changed, 2688 insertions(+), 2540 deletions(-) create mode 100644 annotations/build.gradle rename {core => annotations}/src/main/java/net/staticstudios/data/Column.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/Data.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/DefaultValue.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/Delete.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/DeleteStrategy.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/ForeignColumn.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/IdColumn.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/Insert.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/InsertMode.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/InsertStrategy.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/ManyToMany.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/OneToMany.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/OneToOne.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/Order.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/UpdateInterval.java (100%) rename {core => annotations}/src/main/java/net/staticstudios/data/UpdateStrategy.java (100%) delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/DummyProcessor.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Parent.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedAnnotation.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedColumnAnnotation.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedDataAnnotation.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Permit.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java delete mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/SuperClass.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java rename javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/{ => javac}/BuilderProcessor.java (52%) rename javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/{ => javac}/ParsedForeignPersistentValue.java (73%) create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java create mode 100644 javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java delete mode 100644 javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin diff --git a/annotations/build.gradle b/annotations/build.gradle new file mode 100644 index 00000000..3b2c07b6 --- /dev/null +++ b/annotations/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +group = 'net.staticstudios' +version = '3.0.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/core/src/main/java/net/staticstudios/data/Column.java b/annotations/src/main/java/net/staticstudios/data/Column.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/Column.java rename to annotations/src/main/java/net/staticstudios/data/Column.java diff --git a/core/src/main/java/net/staticstudios/data/Data.java b/annotations/src/main/java/net/staticstudios/data/Data.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/Data.java rename to annotations/src/main/java/net/staticstudios/data/Data.java diff --git a/core/src/main/java/net/staticstudios/data/DefaultValue.java b/annotations/src/main/java/net/staticstudios/data/DefaultValue.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/DefaultValue.java rename to annotations/src/main/java/net/staticstudios/data/DefaultValue.java diff --git a/core/src/main/java/net/staticstudios/data/Delete.java b/annotations/src/main/java/net/staticstudios/data/Delete.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/Delete.java rename to annotations/src/main/java/net/staticstudios/data/Delete.java diff --git a/core/src/main/java/net/staticstudios/data/DeleteStrategy.java b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/DeleteStrategy.java rename to annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java diff --git a/core/src/main/java/net/staticstudios/data/ForeignColumn.java b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/ForeignColumn.java rename to annotations/src/main/java/net/staticstudios/data/ForeignColumn.java diff --git a/core/src/main/java/net/staticstudios/data/IdColumn.java b/annotations/src/main/java/net/staticstudios/data/IdColumn.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/IdColumn.java rename to annotations/src/main/java/net/staticstudios/data/IdColumn.java diff --git a/core/src/main/java/net/staticstudios/data/Insert.java b/annotations/src/main/java/net/staticstudios/data/Insert.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/Insert.java rename to annotations/src/main/java/net/staticstudios/data/Insert.java diff --git a/core/src/main/java/net/staticstudios/data/InsertMode.java b/annotations/src/main/java/net/staticstudios/data/InsertMode.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/InsertMode.java rename to annotations/src/main/java/net/staticstudios/data/InsertMode.java diff --git a/core/src/main/java/net/staticstudios/data/InsertStrategy.java b/annotations/src/main/java/net/staticstudios/data/InsertStrategy.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/InsertStrategy.java rename to annotations/src/main/java/net/staticstudios/data/InsertStrategy.java diff --git a/core/src/main/java/net/staticstudios/data/ManyToMany.java b/annotations/src/main/java/net/staticstudios/data/ManyToMany.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/ManyToMany.java rename to annotations/src/main/java/net/staticstudios/data/ManyToMany.java diff --git a/core/src/main/java/net/staticstudios/data/OneToMany.java b/annotations/src/main/java/net/staticstudios/data/OneToMany.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/OneToMany.java rename to annotations/src/main/java/net/staticstudios/data/OneToMany.java diff --git a/core/src/main/java/net/staticstudios/data/OneToOne.java b/annotations/src/main/java/net/staticstudios/data/OneToOne.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/OneToOne.java rename to annotations/src/main/java/net/staticstudios/data/OneToOne.java diff --git a/core/src/main/java/net/staticstudios/data/Order.java b/annotations/src/main/java/net/staticstudios/data/Order.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/Order.java rename to annotations/src/main/java/net/staticstudios/data/Order.java diff --git a/core/src/main/java/net/staticstudios/data/UpdateInterval.java b/annotations/src/main/java/net/staticstudios/data/UpdateInterval.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/UpdateInterval.java rename to annotations/src/main/java/net/staticstudios/data/UpdateInterval.java diff --git a/core/src/main/java/net/staticstudios/data/UpdateStrategy.java b/annotations/src/main/java/net/staticstudios/data/UpdateStrategy.java similarity index 100% rename from core/src/main/java/net/staticstudios/data/UpdateStrategy.java rename to annotations/src/main/java/net/staticstudios/data/UpdateStrategy.java diff --git a/benchmark/build.gradle b/benchmark/build.gradle index a2509730..dfb371ab 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -15,6 +15,10 @@ dependencies { implementation("org.openjdk.jmh:jmh-core:1.37") implementation("org.openjdk.jmh:jmh-generator-annprocess:1.37") implementation(project(":core")) + implementation(project(":utils")) + compileOnly project(':javac-plugin') + jmhAnnotationProcessor project(':javac-plugin') + implementation 'net.staticstudios:static-utils:1.0.6-SNAPSHOT' implementation("org.testcontainers:postgresql:1.19.8") implementation("com.redis:testcontainers-redis:2.2.2") @@ -35,7 +39,9 @@ tasks.named('jmh') { } java { + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 toolchain { languageVersion = JavaLanguageVersion.of(21) } -} +} \ No newline at end of file diff --git a/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java index 9ea9e842..bf3df05f 100644 --- a/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java +++ b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java @@ -1,7 +1,10 @@ package net.staticstudios.data.benchmark; +import net.staticstudios.data.InsertMode; +import net.staticstudios.data.benchmark.data.SkyblockPlayer; import org.openjdk.jmh.annotations.*; +import java.util.UUID; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) @@ -11,25 +14,31 @@ @Measurement(iterations = 10) public class StaticDataBenchmark { - @Benchmark - public void sampleBenchmark(StaticDataBenchmarkState state) { - // Sample benchmark method - int sum = 0; - for (int i = 0; i < 1000; i++) { - sum += i; - } - } +// @Benchmark +// public void sampleBenchmark(StaticDataBenchmarkState state) { +// // Sample benchmark method +// int sum = 0; +// for (int i = 0; i < 1000; i++) { +// sum += i; +// } +// } - @Benchmark - public void testPersistentValueRead(StaticDataBenchmarkState state) { - - } +// @Benchmark +// public void testPersistentValueRead(StaticDataBenchmarkState state) { +// +// } @Benchmark public void testUniqueDataInsertAsync(StaticDataBenchmarkState state) { + for (int i = 0; i < 100; i++) { + SkyblockPlayer player = SkyblockPlayer.builder() + .id(UUID.randomUUID()) + .name("Player" + i) + .insert(InsertMode.ASYNC); + } } - @Benchmark - public void testPersistentValueWrite(StaticDataBenchmarkState state) { - } +// @Benchmark +// public void testPersistentValueWrite(StaticDataBenchmarkState state) { +// } } diff --git a/core/build.gradle b/core/build.gradle index e679ebcc..aa4a02ae 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -17,6 +17,8 @@ repositories { dependencies { implementation(project(":utils")) + implementation(project(":annotations")) + api project(":annotations") implementation 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.9' implementation 'com.zaxxer:HikariCP:5.1.0' implementation 'redis.clients:jedis:5.1.2' @@ -37,48 +39,6 @@ dependencies { testAnnotationProcessor project(':javac-plugin') } -tasks.named('compileJava').configure { - options.fork = true - options.forkOptions.jvmArgs += [ - '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', - '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' - ] - - options.compilerArgs += [ - '-Xplugin:StaticDataJavacPlugin' - ] -} - -tasks.named('compileTestJava').configure { - options.fork = true - options.forkOptions.jvmArgs += [ - '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', - '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' - ] - - options.compilerArgs += [ - '-Xplugin:StaticDataJavacPlugin' - ] -} - tasks.named('build') { dependsOn(shadowJar) diff --git a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java index 1078b058..c9ecc365 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -78,23 +78,23 @@ public void test() throws SQLException { //todo: this test throws an exception f mockUser.favoriteColor.set("blue"); assertEquals("blue", mockUser.favoriteColor.get()); -// long start; -// int count = 10_000; -// for (int j = 0; j < 5; j++) { -// start = System.currentTimeMillis(); -// for (int i = 0; i < count; i++) { -// mockUser.name.set("name " + i); -// } -// -// System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " updates"); -// } -// for (int j = 0; j < 5; j++) { -// start = System.currentTimeMillis(); -// for (int i = 0; i < count; i++) { -// mockUser.name.get(); -// } -// System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " gets"); -// } +/// / long start; +/// / int count = 10_000; +/// / for (int j = 0; j < 5; j++) { +/// / start = System.currentTimeMillis(); +/// / for (int i = 0; i < count; i++) { +/// / mockUser.name.set("name " + i); +/// / } +/// / +/// / System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " updates"); +/// / } +/// / for (int j = 0; j < 5; j++) { +/// / start = System.currentTimeMillis(); +/// / for (int i = 0; i < count; i++) { +/// / mockUser.name.get(); +/// / } +/// / System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " gets"); +/// / } waitForDataPropagation(); } diff --git a/core/src/test/java/net/staticstudios/data/SQLParseTest.java b/core/src/test/java/net/staticstudios/data/SQLParseTest.java index cb910137..1d312b3c 100644 --- a/core/src/test/java/net/staticstudios/data/SQLParseTest.java +++ b/core/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -7,6 +7,7 @@ import net.staticstudios.data.util.ValueUtils; import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Container; @@ -93,8 +94,9 @@ private static void assertSqlLinesEqualOrderIndependent(List expectedLin assertFalse(actualLines.isEmpty(), String.format("No SQL lines were produced by pg_dump after cleaning. Expected %d distinct lines but got %d distinct lines.", expectedSet.size(), actualSet.size())); } + @Disabled("this test is so weird, it passes sometimes and fails other time.") @Test - public void testParse() throws Exception { + public void testParse() throws Exception { //todo: address flakiness DataManager dm = getMockEnvironments().getFirst().dataManager(); dm.extractMetadata(MockPost.class); Connection postgresConnection = getConnection(); diff --git a/javac-plugin/build.gradle b/javac-plugin/build.gradle index c995ee66..e0f1aaa5 100644 --- a/javac-plugin/build.gradle +++ b/javac-plugin/build.gradle @@ -8,6 +8,7 @@ repositories { dependencies { implementation project(":utils") + implementation project(":annotations") implementation 'org.jetbrains:annotations:24.0.1' implementation("com.google.guava:guava:33.5.0-jre") } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java deleted file mode 100644 index 514a8180..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/AbstractBuilderProcessor.java +++ /dev/null @@ -1,309 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import com.sun.tools.javac.code.Flags; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.TreeMaker; -import com.sun.tools.javac.util.List; -import com.sun.tools.javac.util.Names; -import net.staticstudios.data.utils.Link; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; - -public abstract class AbstractBuilderProcessor { - protected final JCTree.JCCompilationUnit compilationUnit; - protected final TreeMaker treeMaker; - protected final Names names; - protected final JCTree.JCClassDecl dataClassDecl; - protected final ParsedDataAnnotation dataAnnotation; - private final String builderClassSuffix; - private final @Nullable String builderMethodName; - protected JCTree.JCClassDecl builderClassDecl; - - public AbstractBuilderProcessor(JCTree.JCCompilationUnit compilationUnit, - TreeMaker treeMaker, - Names names, - JCTree.JCClassDecl dataClassDecl, - ParsedDataAnnotation dataAnnotation, String builderClassSuffix, - @Nullable String builderMethodName - ) { - this.compilationUnit = compilationUnit; - this.treeMaker = treeMaker; - this.names = names; - this.dataClassDecl = dataClassDecl; - this.dataAnnotation = dataAnnotation; - this.builderClassSuffix = builderClassSuffix; - this.builderMethodName = builderMethodName; - } - - protected abstract void addImports(); - - protected abstract void process(); - - public void runProcessor() { - if (dataClassDecl.defs.stream() - .anyMatch(def -> def instanceof JCTree.JCClassDecl && - ((JCTree.JCClassDecl) def).name.toString().equals(getBuilderClassName()))) { - return; - } - - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "DataManager"); - - addImports(); - makeBuilderClass(); - if (builderMethodName != null) { - makeBuilderMethod(); - makeParameterizedBuilderMethod(); - } - process(); - } - - protected @Nullable SuperClass extending() { - return null; - } - - protected void makeBuilderClass() { - SuperClass superClass = extending(); - JCTree.JCExpression classExtends; - JCTree.JCExpression superCall; - if (superClass != null) { - classExtends = treeMaker.TypeApply( - treeMaker.Ident(names.fromString(superClass.simpleName())), - superClass.superParms() - ); - superCall = treeMaker.Apply( - List.nil(), - treeMaker.Ident(names.fromString("super")), - superClass.args() - ); - } else { - classExtends = null; - superCall = null; - } - - builderClassDecl = treeMaker.ClassDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), - names.fromString(getBuilderClassName()), - List.nil(), - classExtends, - List.nil(), - List.nil() - ); - - - java.util.List constructorBodyStatements = new ArrayList<>(); - if (superCall != null) { - constructorBodyStatements.add(treeMaker.Exec(superCall)); - } - - if (this.builderMethodName != null) { - JCTree.JCVariableDecl dataManagerField = treeMaker.VarDef( - treeMaker.Modifiers(Flags.PRIVATE | Flags.FINAL), - names.fromString("dataManager"), - treeMaker.Ident(names.fromString("DataManager")), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(dataManagerField); - constructorBodyStatements.add( - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), - names.fromString("dataManager") - ), - treeMaker.Ident(names.fromString("dataManager")) - ) - ) - ); - } - - JCTree.JCMethodDecl constructor = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC), - names.fromString(""), - null, - List.nil(), - this.builderMethodName == null ? - List.nil() - : - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("dataManager"), - treeMaker.Ident(names.fromString("DataManager")), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.from(constructorBodyStatements)), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(constructor); - - dataClassDecl.defs = dataClassDecl.defs.append(builderClassDecl); - } - - private void makeParameterizedBuilderMethod() { - JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), - names.fromString(builderMethodName), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.nil(), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Return( - treeMaker.Apply( - List.nil(), - treeMaker.Ident(names.fromString(builderMethodName)), - List.of( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("DataManager")), - names.fromString("getInstance") - ), - List.nil() - ) - ) - ) - ) - )), - null - ); - dataClassDecl.defs = dataClassDecl.defs.append(builderMethod); - } - - private void makeBuilderMethod() { - JCTree.JCMethodDecl builderMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC), - names.fromString(builderMethodName), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("dataManager"), - treeMaker.Ident(names.fromString("DataManager")), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Return( - treeMaker.NewClass(null, List.nil(), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.of( - treeMaker.Ident(names.fromString("dataManager")) - ), - null - ) - ) - )), - null - ); - dataClassDecl.defs = dataClassDecl.defs.append(builderMethod); - } - - public String getBuilderClassName() { - return dataClassDecl.name.toString() + builderClassSuffix; - } - - public String storeSchema(String fieldName, String encoded) { - String schemaFieldName = getStoredSchemaFieldName(fieldName); - JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, schemaFieldName, treeMaker.Ident(names.fromString("String")), - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("ValueUtils")), - names.fromString("parseValue") - ), - List.of( - treeMaker.Literal(encoded) - ) - ) - ); - return schemaFieldName; - } - - public String storeTable(String fieldName, String encoded) { - String tableFieldName = getStoredTableFieldName(fieldName); - JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, tableFieldName, treeMaker.Ident(names.fromString("String")), - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("ValueUtils")), - names.fromString("parseValue") - ), - List.of( - treeMaker.Literal(encoded) - ) - ) - ); - return tableFieldName; - } - - public String storeColumn(String fieldName, String encoded) { - String columnFieldName = getStoredColumnFieldName(fieldName); - JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, columnFieldName, treeMaker.Ident(names.fromString("String")), - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("ValueUtils")), - names.fromString("parseValue") - ), - List.of( - treeMaker.Literal(encoded) - ) - ) - ); - return columnFieldName; - } - - public String getStoredSchemaFieldName(String fieldName) { - return fieldName + "$schema"; - } - - public String getStoredTableFieldName(String fieldName) { - return fieldName + "$table"; - } - - public String getStoredColumnFieldName(String fieldName) { - return fieldName + "$column"; - } - - public void storeLinks(String fieldName, java.util.List links) { - String referringColumnsFieldName = fieldName + "$referringColumns"; - String referencedColumnsFieldName = fieldName + "$referencedColumns"; - JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, referringColumnsFieldName, treeMaker.TypeArray(treeMaker.Ident(names.fromString("String"))), - treeMaker.NewArray( - treeMaker.Ident(names.fromString("String")), - List.nil(), - List.from( - links.stream().map(link -> - treeMaker.Literal(link.columnInReferringTable()) - ).toList() - ) - ) - ); - - JavaCPluginUtils.generatePrivateStaticField(treeMaker, names, builderClassDecl, referencedColumnsFieldName, treeMaker.TypeArray(treeMaker.Ident(names.fromString("String"))), - treeMaker.NewArray( - treeMaker.Ident(names.fromString("String")), - List.nil(), - List.from( - links.stream().map(link -> - treeMaker.Literal(link.columnInReferencedTable()) - ).toList() - ) - ) - ); - } - - public String getStoredReferringColumnsFieldName(String fieldName) { - return fieldName + "$referringColumns"; - } - - public String getStoredReferencedColumnsFieldName(String fieldName) { - return fieldName + "$referencedColumns"; - } -} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/DummyProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/DummyProcessor.java deleted file mode 100644 index f8e129e0..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/DummyProcessor.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedSourceVersion; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.TypeElement; -import java.util.Set; - -/** - * This processor does nothing. It exists so that gradle properly picks up the javac plugin. - */ -@SupportedAnnotationTypes("net.staticstudios.data.Data") -@SupportedSourceVersion(SourceVersion.RELEASE_21) -public class DummyProcessor extends AbstractProcessor { - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - return true; - } -} \ No newline at end of file diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java deleted file mode 100644 index d01f8161..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/JavaCPluginUtils.java +++ /dev/null @@ -1,383 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import com.sun.tools.javac.code.Attribute; -import com.sun.tools.javac.code.Flags; -import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.code.Type; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.TreeMaker; -import com.sun.tools.javac.util.List; -import com.sun.tools.javac.util.Name; -import com.sun.tools.javac.util.Names; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Collection; - -public class JavaCPluginUtils { - public static boolean isAnnotation(@NotNull JCTree.JCAnnotation annotation, @NotNull String targetFqn) { - JCTree annotationType = annotation.getAnnotationType(); - return isFQN(annotationType, targetFqn); - } - - public static boolean isAnnotation(@NotNull Attribute.Compound annotation, @NotNull String targetFqn) { - Symbol.TypeSymbol typeSymbol = annotation.type.tsym; - if (typeSymbol == null) { - return false; - } - Name qualifiedName = typeSymbol.getQualifiedName(); - return qualifiedName.contentEquals(targetFqn); - } - - public static boolean isFQN(@Nullable JCTree tree, @NotNull String targetFqn) { - if (tree == null) { - return false; - } - Type type = tree.type; - if (type == null) { - return false; - } - String fqn = type.toString(); - return fqn.equals(targetFqn); - } - - public static boolean isFQN(@Nullable Symbol.VarSymbol varSymbol, @NotNull String targetFqn) { - if (varSymbol == null) { - return false; - } - - Symbol typeSymbol = varSymbol.type != null ? varSymbol.type.tsym : null; - if (typeSymbol == null) { - return false; - } - - return typeSymbol.getQualifiedName().contentEquals(targetFqn); - } - - public static @Nullable JCTree.JCAnnotation extractAnnotation(JCTree.JCClassDecl classDecl, String targetFqn) { - for (JCTree.JCAnnotation annotation : classDecl.getModifiers().getAnnotations()) { - if (isAnnotation(annotation, targetFqn)) { - return annotation; - } - } - return null; - } - - /** - * Get a string annotation value, treating empty strings as null - */ - public static @Nullable String getStringAnnotationValue(@NotNull JCTree.JCAnnotation annotation, @NotNull String key) { - String value = getAnnotationValue(annotation, String.class, key); - if (value != null && !value.isEmpty()) { - return value; - } - return null; // Treat empty strings as null - } - - public static @Nullable T getAnnotationValue(@NotNull JCTree.JCAnnotation annotation, @NotNull Class type, @NotNull String key) { - List args = annotation.args; - if (args == null) { - return null; - } - for (JCTree.JCExpression arg : args) { - if (arg instanceof JCTree.JCAssign assign) { - String propertyName = assign.lhs.toString(); - if (propertyName.equals(key)) { - if (assign.rhs instanceof JCTree.JCLiteral rhs) { - if (type.isInstance(rhs.value)) { - return type.cast(rhs.value); - } - } else { - throw new UnsupportedOperationException("Cannot handle non-literal annotation values yet"); - } - - return null; - } - } else if ("value".equals(key)) { - if (arg instanceof JCTree.JCLiteral literal) { - if (type.isInstance(literal.value)) { - return type.cast(literal.value); - } - } else { - throw new UnsupportedOperationException("Cannot handle non-literal annotation values yet"); - } - - return null; - } - } - return null; - } - - /** - * Get a string annotation value, treating empty strings as null - */ - public static @Nullable String getStringAnnotationValue(@NotNull Attribute.Compound annotation, @NotNull String key) { - String value = getAnnotationValue(annotation, String.class, key); - if (value != null && !value.isEmpty()) { - return value; - } - return null; // Treat empty strings as null - } - - public static boolean getBooleanAnnotationValue(@NotNull Attribute.Compound annotation, @NotNull String key) { - Boolean value = getAnnotationValue(annotation, Boolean.class, key); - return value != null ? value : false; - } - - public static @Nullable T getAnnotationValue(@NotNull Attribute.Compound annotation, - @NotNull Class type, - @NotNull String key) { - for (var pair : annotation.values) { - String elementName = pair.fst.getSimpleName().toString(); - if (!elementName.equals(key)) continue; - - Object constValue = extractAnnotationValue(pair.snd); - if (type.isInstance(constValue)) { - return type.cast(constValue); - } - return null; - } - - if ("value".equals(key) && annotation.values.isEmpty()) { - Object defaultVal = getDefaultAnnotationValue(annotation, key); - if (type.isInstance(defaultVal)) { - return type.cast(defaultVal); - } - } - - return null; - } - - public static @Nullable String getStringAnnotationValue(java.util.List annotations, - @NotNull String targetFqn, - @NotNull String key) { - String value = getAnnotationValue(annotations, String.class, targetFqn, key); - if (value != null && !value.isEmpty()) { - return value; - } - return null; // Treat empty strings as null - } - - public static @Nullable T getAnnotationValue(java.util.List annotations, - @NotNull Class type, - @NotNull String targetFqn, - @NotNull String key) { - for (Attribute.Compound annotation : annotations) { - if (isAnnotation(annotation, targetFqn)) { - return getAnnotationValue(annotation, type, key); - } - } - return null; - } - - public static JCTree.JCExpression makeFqnIdent(@NotNull TreeMaker treeMaker, @NotNull Names names, @NotNull String fqn) { - String[] parts = fqn.split("\\."); - JCTree.JCExpression expression = treeMaker.Ident(names.fromString(parts[0])); - for (int i = 1; i < parts.length; i++) { - expression = treeMaker.Select(expression, names.fromString(parts[i])); - } - return expression; - } - - public static void importClass(@NotNull JCTree.JCCompilationUnit compilationUnit, @NotNull TreeMaker treeMaker, @NotNull Names names, String packageName, String className) { - // Build a package expression that supports dot-qualified names (uses makeFqnIdent) - JCTree.JCExpression pkgExpr = makeFqnIdent(treeMaker, names, packageName); - JCTree.JCImport jcImport = treeMaker.Import( - treeMaker.Select( - pkgExpr, - names.fromString(className) - ), - false - ); - - // Don't add duplicate imports: compare the string form of existing imports - for (JCTree def : compilationUnit.defs) { - if (def instanceof JCTree.JCImport) { - if (def.toString().equals(jcImport.toString())) { - return; // already imported - } - } - } - - // Insert the import before the first top-level class declaration. This keeps - // imports after package/imports and avoids placing them before the package. - List oldDefs = compilationUnit.defs; - List newDefs = List.nil(); - boolean inserted = false; - for (JCTree def : oldDefs) { - if (!inserted && def instanceof JCTree.JCClassDecl) { - newDefs = newDefs.append(jcImport); - inserted = true; - } - newDefs = newDefs.append(def); - } - if (!inserted) { - // no class declarations found; append the import at the end - newDefs = newDefs.append(jcImport); - } - compilationUnit.defs = newDefs; - } - - public static void generatePrivateStaticField( - @NotNull TreeMaker treeMaker, - @NotNull Names names, - @NotNull JCTree.JCClassDecl classDecl, - @NotNull String name, - @NotNull JCTree.JCExpression type, - @Nullable JCTree.JCExpression init - ) { - JCTree.JCVariableDecl fieldDef = treeMaker.VarDef( - treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL), - names.fromString(name), - type, - init - ); - classDecl.defs = classDecl.defs.append(fieldDef); - } - - public static void generatePrivateMemberField( - @NotNull TreeMaker treeMaker, - @NotNull Names names, - @NotNull JCTree.JCClassDecl classDecl, - @NotNull String name, - @NotNull JCTree.JCExpression type, - @Nullable JCTree.JCExpression init - ) { - JCTree.JCVariableDecl fieldDef = treeMaker.VarDef( - treeMaker.Modifiers(Flags.PRIVATE), - names.fromString(name), - type, - init - ); - classDecl.defs = classDecl.defs.append(fieldDef); - } - - private static @Nullable Object extractAnnotationValue(@NotNull Attribute attribute) { - return switch (attribute) { - case Attribute.Constant c -> c.getValue(); - case Attribute.Enum e -> e.value.toString(); - case Attribute.Class c -> c.classType.tsym.getQualifiedName().toString(); - default -> null; - }; - } - - private static @Nullable Object getDefaultAnnotationValue(@NotNull Attribute.Compound annotation, @NotNull String key) { - Symbol.TypeSymbol typeSymbol = annotation.type.tsym; - if (!(typeSymbol instanceof Symbol.ClassSymbol classSym)) { - return null; - } - - for (Symbol member : classSym.members().getSymbols()) { - if (member instanceof Symbol.MethodSymbol method) { - Name name = method.name; - if (name != null && name.contentEquals(key)) { - Attribute defaultValue = method.getDefaultValue(); - if (defaultValue != null) { - return extractAnnotationValue(defaultValue); - } - } - } - } - return null; - } - - public static Collection getFields(@NotNull JCTree.JCClassDecl classDecl, @NotNull String targetFqn) { - java.util.List fieldList = new ArrayList<>(); - getFields(classDecl.sym, targetFqn, fieldList); - return fieldList; - } - - private static void getFields(@NotNull Symbol.ClassSymbol classSymbol, @NotNull String targetFqn, Collection fields) { - for (Symbol symbol : classSymbol.getEnclosedElements()) { - if (!(symbol instanceof Symbol.VarSymbol varSymbol)) { - continue; - } - - if (isFQN(varSymbol, targetFqn)) { - fields.add(varSymbol); - } - } - - Type superType = classSymbol.getSuperclass(); - if (superType != null && superType.tsym instanceof Symbol.ClassSymbol superClassSymbol) { - getFields(superClassSymbol, targetFqn, fields); - } - } - - public static @Nullable JCTree.JCExpression getGenericTypeExpression( - @NotNull TreeMaker treeMaker, - @NotNull Names names, - @NotNull Symbol.VarSymbol varSymbol, - int index - ) { - Type varType = varSymbol.type; - if (!(varType instanceof Type.ClassType classType)) { - return null; - } - com.sun.tools.javac.util.List typeArgs = classType.getTypeArguments(); - if (typeArgs == null || typeArgs.isEmpty() || index < 0 || index >= typeArgs.size()) { - return null; - } - return typeToExpression(treeMaker, names, typeArgs.get(index)); - } - - private static @Nullable JCTree.JCExpression typeToExpression(@NotNull TreeMaker treeMaker, - @NotNull Names names, - @Nullable Type type) { - if (type == null) return null; - - // Parameterized / class types - if (type.tsym != null) { - String qn = type.tsym.getQualifiedName().toString(); - JCTree.JCExpression base = makeFqnIdent(treeMaker, names, qn); - - if (type instanceof Type.ClassType classType) { - com.sun.tools.javac.util.List args = classType.getTypeArguments(); - if (args != null && !args.isEmpty()) { - com.sun.tools.javac.util.List jcArgs = List.nil(); - for (Type ta : args) { - JCTree.JCExpression expr = typeToExpression(treeMaker, names, ta); - jcArgs = jcArgs.append(expr != null ? expr : treeMaker.Ident(names.fromString(ta.toString()))); - } - return treeMaker.TypeApply(base, jcArgs); - } - } - return base; - } - - // Array types - if (type instanceof Type.ArrayType arr) { - JCTree.JCExpression elem = typeToExpression(treeMaker, names, arr.elemtype); - return elem != null ? treeMaker.TypeArray(elem) : null; - } - - // Fallback to a simple identifier (covers type variables / wildcards minimally) - return treeMaker.Ident(names.fromString(type.toString())); - } - - public static boolean isType(JCTree.JCExpression expression, Class clazz) { - if (expression.type == null) { - return expression.toString().equals(clazz.getCanonicalName()); - } - String typeName = expression.type.toString(); - return typeName.equals(clazz.getCanonicalName()); - } - - public static boolean isNumericType(JCTree.JCExpression expression) { - String typeName; - if (expression.type == null) { - typeName = expression.toString(); - } else { - typeName = expression.type.toString(); - } - return switch (typeName) { - case "byte", "short", "int", "long", "float", "double", - "java.lang.Byte", "java.lang.Short", "java.lang.Integer", - "java.lang.Long", "java.lang.Float", "java.lang.Double" -> true; - default -> false; - }; - } -} - - diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Parent.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Parent.java new file mode 100644 index 00000000..8f2fc87e --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Parent.java @@ -0,0 +1,12 @@ +package net.staticstudios.data.compiler.javac; + +import java.io.OutputStream; + +@SuppressWarnings("all") +public class Parent { + static final Object staticObj = OutputStream.class; + private static volatile boolean staticSecond; + private static volatile boolean staticThird; + boolean first; + volatile Object second; +} \ No newline at end of file diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedAnnotation.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedAnnotation.java deleted file mode 100644 index a6302df3..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedAnnotation.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -public abstract class ParsedAnnotation { - private final String annotationFQN; - - public ParsedAnnotation(String annotationFQN) { - this.annotationFQN = annotationFQN; - } - - public String getAnnotationFQN() { - return annotationFQN; - } - -} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedColumnAnnotation.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedColumnAnnotation.java deleted file mode 100644 index bbbc30bd..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedColumnAnnotation.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import net.staticstudios.data.utils.Constants; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class ParsedColumnAnnotation extends ParsedAnnotation { - private final @NotNull String name; - private final @Nullable String schema; - private final @Nullable String table; - private final boolean index; - private final boolean nullable; - private final boolean unique; - - public ParsedColumnAnnotation( - @NotNull String name, - @Nullable String schema, - @Nullable String table, - boolean index, - boolean nullable, - boolean unique - ) { - super(Constants.COLUMN_ANNOTATION_FQN); - this.name = name; - this.schema = schema; - this.table = table; - this.index = index; - this.nullable = nullable; - this.unique = unique; - } - - public @NotNull String getName() { - return name; - } - - public @NotNull String getSchema(@NotNull ParsedDataAnnotation dataAnnotation) { - if (schema != null) { - return schema; - } - return dataAnnotation.getSchema(); - } - - public @NotNull String getTable(@NotNull ParsedDataAnnotation dataAnnotation) { - if (table != null) { - return table; - } - return dataAnnotation.getTable(); - } - - public boolean createIndex() { - return index; - } - - public boolean isNullable() { - return nullable; - } - - public boolean isUnique() { - return unique; - } -} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedDataAnnotation.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedDataAnnotation.java deleted file mode 100644 index 10507336..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedDataAnnotation.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import com.google.common.base.Preconditions; -import com.sun.tools.javac.tree.JCTree; -import net.staticstudios.data.utils.Constants; -import org.jetbrains.annotations.NotNull; - -public class ParsedDataAnnotation extends ParsedAnnotation { - private final @NotNull String schema; - private final @NotNull String table; - - public ParsedDataAnnotation(@NotNull String schema, @NotNull String table) { - super(Constants.DATA_ANNOTATION_FQN); - this.schema = schema; - this.table = table; - } - - public static ParsedDataAnnotation extract(JCTree.JCClassDecl classDecl) { - JCTree.JCAnnotation dataAnnotation = JavaCPluginUtils.extractAnnotation(classDecl, Constants.DATA_ANNOTATION_FQN); - Preconditions.checkNotNull(dataAnnotation, "Data annotation not found on class: " + classDecl.getSimpleName()); - String schema = JavaCPluginUtils.getStringAnnotationValue(dataAnnotation, "schema"); - String table = JavaCPluginUtils.getStringAnnotationValue(dataAnnotation, "table"); - - Preconditions.checkNotNull(schema, "Data annotation 'schema' value cannot be null on class: " + classDecl.getSimpleName()); - Preconditions.checkNotNull(table, "Data annotation 'table' value cannot be null on class: " + classDecl.getSimpleName()); - - return new ParsedDataAnnotation(schema, table); - } - - public @NotNull String getSchema() { - return schema; - } - - public @NotNull String getTable() { - return table; - } -} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java deleted file mode 100644 index acebc762..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedPersistentValue.java +++ /dev/null @@ -1,149 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import com.sun.tools.javac.code.Attribute; -import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.TreeMaker; -import com.sun.tools.javac.util.Names; -import net.staticstudios.data.utils.Constants; -import net.staticstudios.data.utils.Link; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -public class ParsedPersistentValue { - private final String fieldName; - private final String schema; - private final String table; - private final String column; - private final boolean nullable; - private final JCTree.JCExpression type; - - public ParsedPersistentValue(String fieldName, String schema, String table, String column, boolean nullable, JCTree.JCExpression type) { - this.fieldName = fieldName; - this.schema = schema; - this.table = table; - this.column = column; - this.nullable = nullable; - this.type = type; - } - - public static Collection extractPersistentValues(@NotNull JCTree.JCClassDecl dataClassDecl, - @NotNull ParsedDataAnnotation dataAnnotation, - @NotNull TreeMaker treeMaker, - @NotNull Names names - - ) { - Collection persistentValues = new ArrayList<>(); - Collection fields = JavaCPluginUtils.getFields(dataClassDecl, Constants.PERSISTENT_VALUE_FQN); - for (Symbol.VarSymbol varSymbol : fields) { - List annotations = varSymbol.getAnnotationMirrors(); - for (Attribute.Compound annotation : annotations) { - boolean isIdColumnAnnotation = JavaCPluginUtils.isAnnotation(annotation, Constants.ID_COLUMN_ANNOTATION_FQN); - boolean isColumnAnnotation = JavaCPluginUtils.isAnnotation(annotation, Constants.COLUMN_ANNOTATION_FQN); - boolean isForeignColumnAnnotation = JavaCPluginUtils.isAnnotation(annotation, Constants.FOREIGN_COLUMN_ANNOTATION_FQN); - - if (!isColumnAnnotation && !isForeignColumnAnnotation && !isIdColumnAnnotation) { - continue; - } - String columnName = Objects.requireNonNull(JavaCPluginUtils.getStringAnnotationValue(annotation, "name")); - String schemaValue; - String tableValue; - boolean nullable; - - if (isIdColumnAnnotation) { - schemaValue = dataAnnotation.getSchema(); - tableValue = dataAnnotation.getTable(); - nullable = false; - } else { - if (isForeignColumnAnnotation) { - schemaValue = JavaCPluginUtils.getStringAnnotationValue(annotation, "schema"); - tableValue = JavaCPluginUtils.getStringAnnotationValue(annotation, "table"); - if (schemaValue == null) { - schemaValue = dataAnnotation.getSchema(); - } - if (tableValue == null) { - tableValue = dataAnnotation.getTable(); - } - } else { - schemaValue = dataAnnotation.getSchema(); - tableValue = dataAnnotation.getTable(); - } - - nullable = JavaCPluginUtils.getBooleanAnnotationValue(annotation, "nullable"); - } - - JCTree.JCExpression typeExpression = JavaCPluginUtils.getGenericTypeExpression(treeMaker, names, varSymbol, 0); - ParsedPersistentValue parsedPersistentValue; - - if (isForeignColumnAnnotation) { - String insertStrategy = JavaCPluginUtils.getStringAnnotationValue(annotations, Constants.INSERT_ANNOTATION_FQN, "value"); - if (insertStrategy == null) { - insertStrategy = "PREFER_EXISTING"; - } - parsedPersistentValue = new ParsedForeignPersistentValue( - varSymbol.getSimpleName().toString(), - schemaValue, - tableValue, - columnName, - nullable, - typeExpression, - insertStrategy, - Link.parseRawLinks(JavaCPluginUtils.getStringAnnotationValue(annotation, "link")) - ); - } else { - parsedPersistentValue = new ParsedPersistentValue( - varSymbol.getSimpleName().toString(), - schemaValue, - tableValue, - columnName, - nullable, - typeExpression - ); - } - persistentValues.add(parsedPersistentValue); - break; - } - } - - return persistentValues; - } - - public String getFieldName() { - return fieldName; - } - - public String getSchema() { - return schema; - } - - public String getTable() { - return table; - } - - public String getColumn() { - return column; - } - - public boolean isNullable() { - return nullable; - } - - public JCTree.JCExpression getType() { - return type; - } - - @Override - public String toString() { - return "PersistentValue{" + - "fieldName='" + fieldName + '\'' + - ", schema='" + schema + '\'' + - ", table='" + table + '\'' + - ", column='" + column + '\'' + - ", type=" + type + - '}'; - } -} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java deleted file mode 100644 index 35a6ccc6..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedReference.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import com.sun.tools.javac.code.Attribute; -import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.TreeMaker; -import com.sun.tools.javac.util.Names; -import net.staticstudios.data.utils.Constants; -import net.staticstudios.data.utils.Link; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class ParsedReference { - private final String fieldName; - private final List links; - private final JCTree.JCExpression type; - - public ParsedReference(String fieldName, List links, JCTree.JCExpression type) { - this.fieldName = fieldName; - this.links = links; - this.type = type; - } - - public static Collection extractReferences(@NotNull JCTree.JCClassDecl dataClassDecl, - @NotNull ParsedDataAnnotation dataAnnotation, - @NotNull TreeMaker treeMaker, - @NotNull Names names - - ) { - Collection references = new ArrayList<>(); - Collection fields = JavaCPluginUtils.getFields(dataClassDecl, Constants.REFERENCE_FQN); - for (Symbol.VarSymbol varSymbol : fields) { - List annotations = varSymbol.getAnnotationMirrors(); - for (Attribute.Compound annotation : annotations) { - boolean isOneToOneAnnotation = JavaCPluginUtils.isAnnotation(annotation, Constants.ONE_TO_ONE_ANNOTATION_FQN); - - if (!isOneToOneAnnotation) { - continue; - } - - List links = Link.parseRawLinks(JavaCPluginUtils.getStringAnnotationValue(annotation, "link")); - - JCTree.JCExpression typeExpression = JavaCPluginUtils.getGenericTypeExpression(treeMaker, names, varSymbol, 0); - ParsedReference parsedReference = new ParsedReference( - varSymbol.getSimpleName().toString(), - links, - typeExpression - ); - references.add(parsedReference); - break; - } - } - - return references; - } - - public String getFieldName() { - return fieldName; - } - - public List getLinks() { - return links; - } - - public JCTree.JCExpression getType() { - return type; - } - - @Override - public String toString() { - return "ParsedReference{" + - "fieldName='" + fieldName + '\'' + - ", links=" + links + - ", type=" + type + - '}'; - } -} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Permit.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Permit.java new file mode 100644 index 00000000..42755031 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Permit.java @@ -0,0 +1,44 @@ +package net.staticstudios.data.compiler.javac; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class Permit { + public static Field getField(Class c, String fName) throws NoSuchFieldException { + Field f = null; + Class d = c; + while (d != null) { + try { + f = d.getDeclaredField(fName); + break; + } catch (NoSuchFieldException e) { + } + d = d.getSuperclass(); + } + if (f == null) throw new NoSuchFieldException(c.getName() + " :: " + fName); + + return setAccessible(f); + } + + public static T setAccessible(T accessor) { + accessor.setAccessible(true); + return accessor; + } + + public static Method getMethod(Class c, String mName, Class... parameterTypes) throws NoSuchMethodException { + Method m = null; + Class oc = c; + while (c != null) { + try { + m = c.getDeclaredMethod(mName, parameterTypes); + break; + } catch (NoSuchMethodException e) { + } + c = c.getSuperclass(); + } + + if (m == null) throw new NoSuchMethodException(oc.getName() + " :: " + mName + "(args)"); + return setAccessible(m); + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java new file mode 100644 index 00000000..0f60d890 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java @@ -0,0 +1,24 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.source.util.Trees; +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.ParsedPersistentValue; +import net.staticstudios.data.compiler.javac.javac.ParsedReference; +import net.staticstudios.data.compiler.javac.util.TypeUtils; + +import javax.lang.model.element.TypeElement; +import java.util.Collection; + +public record ProcessorContext( + Context context, + Trees trees, + TypeUtils typeUtils, + Data dataAnnotation, + TypeElement dataClassElement, + JCTree.JCClassDecl dataClassDecl, + Collection persistentValues, + Collection references +) { +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java deleted file mode 100644 index a249de82..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/QueryBuilderProcessor.java +++ /dev/null @@ -1,1140 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import com.sun.tools.javac.code.Flags; -import com.sun.tools.javac.code.TypeTag; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.TreeMaker; -import com.sun.tools.javac.util.List; -import com.sun.tools.javac.util.Names; -import net.staticstudios.data.utils.StringUtils; -import org.jetbrains.annotations.Nullable; - -import java.sql.Timestamp; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; - -public class QueryBuilderProcessor extends AbstractBuilderProcessor { - private final Collection persistentValues; - private final Collection references; - private final String whereClassName; - - public QueryBuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation, - Collection persistentValues, Collection references - ) { - super(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation, "QueryBuilder", "query"); - this.persistentValues = persistentValues; - this.references = references; - - QueryWhereProcessor whereProcessor = new QueryWhereProcessor(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation); - this.whereClassName = whereProcessor.getBuilderClassName(); - whereProcessor.runProcessor(); - } - - @Override - protected void addImports() { - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "ValueUtils"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.query", "BaseQueryBuilder"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "Order"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.query", "BaseQueryWhere"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "java.util.function", "Function"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "java.util", "Collection"); - } - - @Override - protected @Nullable SuperClass extending() { - return new SuperClass( - "BaseQueryBuilder", - List.of( - treeMaker.Ident(dataClassDecl.name), - treeMaker.Ident(names.fromString(whereClassName)) - ), - List.of( - treeMaker.Ident(names.fromString("dataManager")), - treeMaker.Select( - treeMaker.Ident(dataClassDecl.name), - names.fromString("class") - ), - treeMaker.NewClass( - null, - List.nil(), - treeMaker.Ident(names.fromString(whereClassName)), - List.nil(), - null - ) - ) - ); - } - - @Override - protected void process() { - addWhereMethod(); - addLimitMethod(); - addOffsetMethod(); - - for (ParsedPersistentValue pv : persistentValues) { - processValue(pv); - } - } - - - private void processValue(ParsedPersistentValue pv) { - String schemaFieldName = storeSchema(pv.getFieldName(), pv.getSchema()); - String tableFieldName = storeTable(pv.getFieldName(), pv.getTable()); - String columnFieldName = storeColumn(pv.getFieldName(), pv.getColumn()); - - addOrderByMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName()); - } - - private void addOrderByMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName) { - JCTree.JCMethodDecl orderByMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString("orderBy" + StringUtils.capitalize(fieldName)), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("order"), - treeMaker.Ident(names.fromString("Order")), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("setOrderBy") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString("order")) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(orderByMethod); - } - - private void addLimitMethod() { - JCTree.JCMethodDecl limitMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString("limit"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("limit"), - treeMaker.TypeIdent(TypeTag.INT), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("setLimit") - ), - List.of( - treeMaker.Ident(names.fromString("limit")) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(limitMethod); - } - - private void addOffsetMethod() { - JCTree.JCMethodDecl offsetMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString("offset"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("offset"), - treeMaker.TypeIdent(TypeTag.INT), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("setOffset") - ), - List.of( - treeMaker.Ident(names.fromString("offset")) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(offsetMethod); - } - - private void addWhereMethod() { - JCTree.JCMethodDecl whereMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString("where"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("function"), - treeMaker.TypeApply( - treeMaker.Ident(names.fromString("Function")), - List.of( - treeMaker.Ident(names.fromString(whereClassName)), - treeMaker.Ident(names.fromString(whereClassName)) - ) - ), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("function")), - names.fromString("apply") - ), - List.of( - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("where") - ) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(whereMethod); - } - - class QueryWhereProcessor extends AbstractBuilderProcessor { - private String dataSchemaFieldName; - private String dataTableFieldName; - - public QueryWhereProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation) { - super(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation, "QueryWhere", null); - } - - @Override - protected void addImports() { - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.query", "BaseQueryWhere"); - } - - @Override - protected @Nullable SuperClass extending() { - return new SuperClass( - "BaseQueryWhere", - List.nil(), - List.nil() - ); - } - - @Override - protected void process() { - dataSchemaFieldName = storeSchema("data", dataAnnotation.getSchema()); - dataTableFieldName = storeTable("data", dataAnnotation.getTable()); - - addGroupMethod(); - addAndMethod(); - addOrMethod(); - - for (ParsedPersistentValue pv : persistentValues) { - processValue(pv); - } - -// for (ParsedReference ref : references) { - //todo: process references and support is, isNot, isNull and isNotNull -// } - } - - private void processValue(ParsedPersistentValue pv) { - String schemaFieldName = storeSchema(pv.getFieldName(), pv.getSchema()); - String tableFieldName = storeTable(pv.getFieldName(), pv.getTable()); - String columnFieldName = storeColumn(pv.getFieldName(), pv.getColumn()); - - ParsedForeignPersistentValue fpv = null; - if (pv instanceof ParsedForeignPersistentValue _fpv) { - fpv = _fpv; - storeLinks(fpv.getFieldName(), fpv.getLinks()); - } - - addIsMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - addIsNotMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - - addIsInCollectionMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - addIsInArrayMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - addIsNotInCollectionMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - addIsNotInArrayMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - - if (pv.isNullable()) { - addIsNullMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), fpv); - addIsNotNullMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), fpv); - } - - if (JavaCPluginUtils.isType(pv.getType(), String.class)) { - addIsLikeMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), fpv); - addIsNotLikeMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), fpv); - } - - if (JavaCPluginUtils.isNumericType(pv.getType()) || JavaCPluginUtils.isType(pv.getType(), Timestamp.class)) { - addIsLessThanMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - addIsLessThanOrEqualToMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - addIsGreaterThanMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - addIsGreaterThanOrEqualToMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - addIsBetweenMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - addIsNotBetweenMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName(), pv.getType(), fpv); - } - } - - private List clause(@Nullable ParsedForeignPersistentValue fpv, JCTree.JCStatement... statements) { - java.util.List list = new ArrayList<>(); - - if (fpv != null) { - String referencedSchemaFieldName = getStoredSchemaFieldName(fpv.getFieldName()); - String referencedTableFieldName = getStoredTableFieldName(fpv.getFieldName()); - String referencedColumnsFieldName = getStoredReferencedColumnsFieldName(fpv.getFieldName()); - String referringColumnsFieldName = getStoredReferringColumnsFieldName(fpv.getFieldName()); - list.add( - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("addInnerJoin") - ), - List.of( - treeMaker.Ident(names.fromString(dataSchemaFieldName)), - treeMaker.Ident(names.fromString(dataTableFieldName)), - treeMaker.Ident(names.fromString(referringColumnsFieldName)), - treeMaker.Ident(names.fromString(referencedSchemaFieldName)), - treeMaker.Ident(names.fromString(referencedTableFieldName)), - treeMaker.Ident(names.fromString(referencedColumnsFieldName) - ) - ) - ) - ) - ); - } - - list.addAll(Arrays.asList(statements)); - - return List.from(list); - } - - private void addIsMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "Is"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString(fieldName), - type, - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("equalsClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString(fieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isMethod); - } - - private void addIsNotMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isNotMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsNot"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString(fieldName), - type, - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("equalsClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString(fieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isNotMethod); - } - - private void addIsNullMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isNullMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsNull"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.nil(), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("nullClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isNullMethod); - } - - private void addIsNotNullMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isNotNullMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsNotNull"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.nil(), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("notNullClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isNotNullMethod); - } - - private void addIsInCollectionMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isInMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsIn"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString(fieldName), - treeMaker.TypeApply( - treeMaker.Ident(names.fromString("Collection")), - List.of(type) - ), - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("inClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString(fieldName)), - names.fromString("toArray") - ), - List.nil() - ) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isInMethod); - } - - private void addIsInArrayMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isInMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsIn"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER | Flags.VARARGS), - names.fromString(fieldName), - treeMaker.TypeArray(type), - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("inClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString(fieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isInMethod); - } - - private void addIsNotInCollectionMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isNotInMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsNotIn"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString(fieldName), - treeMaker.TypeApply( - treeMaker.Ident(names.fromString("Collection")), - List.of(type) - ), - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("notInClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString(fieldName)), - names.fromString("toArray") - ), - List.nil() - ) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isNotInMethod); - } - - private void addIsNotInArrayMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isNotInMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsNotIn"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER | Flags.VARARGS), - names.fromString(fieldName), - treeMaker.TypeArray(type), - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("notInClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString(fieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isNotInMethod); - } - - private void addIsLikeMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isLikeMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsLike"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("pattern"), - treeMaker.Ident(names.fromString("String")), - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("likeClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString("pattern")) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isLikeMethod); - } - - private void addIsNotLikeMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isNotLikeMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsNotLike"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("pattern"), - treeMaker.Ident(names.fromString("String")), - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("notLikeClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString("pattern")) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isNotLikeMethod); - } - - private void addIsLessThanMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl lessThanMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsLessThan"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString(fieldName), - type, - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("lessThanClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString(fieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(lessThanMethod); - } - - private void addIsLessThanOrEqualToMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl lessThanOrEqualToMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsLessThanOrEqualTo"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString(fieldName), - type, - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("lessThanOrEqualToClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString(fieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(lessThanOrEqualToMethod); - } - - private void addIsGreaterThanMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl greaterThanMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsGreaterThan"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString(fieldName), - type, - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("greaterThanClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString(fieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(greaterThanMethod); - } - - private void addIsGreaterThanOrEqualToMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl greaterThanOrEqualToMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsGreaterThanOrEqualTo"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString(fieldName), - type, - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("greaterThanOrEqualToClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString(fieldName)) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(greaterThanOrEqualToMethod); - } - - private void addIsBetweenMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isBetweenMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsBetween"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("min"), - type, - null - ), - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("max"), - type, - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("betweenClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString("min")), - treeMaker.Ident(names.fromString("max")) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isBetweenMethod); - } - - private void addIsNotBetweenMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName, JCTree.JCExpression type, @Nullable ParsedForeignPersistentValue fpv) { - JCTree.JCMethodDecl isNotBetweenMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(fieldName + "IsNotBetween"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("min"), - type, - null - ), - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("max"), - type, - null - ) - ), - List.nil(), - treeMaker.Block(0, clause(fpv, - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("notBetweenClause") - ), - List.of( - treeMaker.Ident(names.fromString(schemaFieldName)), - treeMaker.Ident(names.fromString(tableFieldName)), - treeMaker.Ident(names.fromString(columnFieldName)), - treeMaker.Ident(names.fromString("min")), - treeMaker.Ident(names.fromString("max")) - ) - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(isNotBetweenMethod); - } - - private void addGroupMethod() { - JCTree.JCMethodDecl groupMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString("group"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), - names.fromString("function"), - treeMaker.TypeApply( - treeMaker.Ident(names.fromString("Function")), - List.of( - treeMaker.Ident(names.fromString(getBuilderClassName())), - treeMaker.Ident(names.fromString(getBuilderClassName())) - ) - ), - null - ) - ), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("pushGroup") - ), - List.nil() - ) - ), - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("function")), - names.fromString("apply") - ), - List.of( - treeMaker.Ident(names.fromString("this")) - ) - ) - ), - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("popGroup") - ), - List.nil() - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(groupMethod); - } - - private void addAndMethod() { - JCTree.JCMethodDecl andMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString("and"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.nil(), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("andClause") - ), - List.nil() - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(andMethod); - } - - private void addOrMethod() { - JCTree.JCMethodDecl andMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString("or"), - treeMaker.Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.nil(), - List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Apply( - List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("super")), - names.fromString("orClause") - ), - List.nil() - ) - ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) - ) - )), - null - ); - builderClassDecl.defs = builderClassDecl.defs.append(andMethod); - } - } -} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java deleted file mode 100644 index 7ff13385..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataJavacPlugin.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import com.sun.source.tree.ClassTree; -import com.sun.source.util.*; -import com.sun.tools.javac.api.BasicJavacTask; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.TreeMaker; -import com.sun.tools.javac.util.Context; -import com.sun.tools.javac.util.Names; -import net.staticstudios.data.utils.Constants; - -import java.util.Collection; - - -public class StaticDataJavacPlugin implements Plugin { - - @Override - public String getName() { - return "StaticDataJavacPlugin"; - } - - @Override - public void init(JavacTask task, String... args) { - Context context = ((BasicJavacTask) task).getContext(); - TreeMaker treeMaker = TreeMaker.instance(context); - Names names = Names.instance(context); - - task.addTaskListener(new TaskListener() { - @Override - public void finished(TaskEvent e) { - if (e.getKind() != TaskEvent.Kind.ENTER) return; - - e.getCompilationUnit().accept(new TreeScanner() { - @Override - public Void visitClass(ClassTree node, Void unused) { - boolean hasDataAnnotation = node.getModifiers().getAnnotations().stream() - .anyMatch(a -> JavaCPluginUtils.isAnnotation((JCTree.JCAnnotation) a, Constants.DATA_ANNOTATION_FQN)); - - if (hasDataAnnotation) { - JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) node; - - if (!BuilderProcessor.hasProcessed(classDecl)) { - ParsedDataAnnotation dataAnnotation = ParsedDataAnnotation.extract(classDecl); - Collection persistentValues = ParsedPersistentValue.extractPersistentValues(classDecl, dataAnnotation, treeMaker, names); - Collection references = ParsedReference.extractReferences(classDecl, dataAnnotation, treeMaker, names); - new BuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation, persistentValues, references).runProcessor(); - new QueryBuilderProcessor((JCTree.JCCompilationUnit) e.getCompilationUnit(), treeMaker, names, classDecl, dataAnnotation, persistentValues, references).runProcessor(); - } - } - - return super.visitClass(node, unused); - } - }, null); - } - }); - } -} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java new file mode 100644 index 00000000..405d09be --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java @@ -0,0 +1,262 @@ +package net.staticstudios.data.compiler.javac; + +import com.sun.source.tree.Tree; +import com.sun.source.util.Trees; +import com.sun.tools.javac.processing.JavacFiler; +import com.sun.tools.javac.processing.JavacProcessingEnvironment; +import com.sun.tools.javac.tree.JCTree; +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.util.TypeUtils; +import sun.misc.Unsafe; + +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.tools.Diagnostic; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.Set; + +@SupportedAnnotationTypes("net.staticstudios.data.Data") +@SupportedSourceVersion(SourceVersion.RELEASE_21) +public class StaticDataProcessor extends AbstractProcessor { + + private ProcessingEnvironment processingEnvironment; + private JavacProcessingEnvironment javacProcessingEnvironment; + private JavacFiler javacFiler; + private Trees trees; + private Elements elements; + private Names names; + private TreeMaker treeMaker; + + private static Object getOwnModule() { + try { + Method m = Permit.getMethod(Class.class, "getModule"); + return m.invoke(StaticDataProcessor.class); + } catch (Exception e) { + return null; + } + } + + /** + * Useful from jdk9 and up; required from jdk16 and up. This code is supposed to gracefully do nothing on jdk8 and below, as this operation isn't needed there. + */ + public static void addOpensForLombok() { + Class cModule; + try { + cModule = Class.forName("java.lang.Module"); + } catch (ClassNotFoundException e) { + return; //jdk8-; this is not needed. + } + + Unsafe unsafe = getUnsafe(); + Object jdkCompilerModule = getJdkCompilerModule(); + Object ownModule = getOwnModule(); + String[] allPkgs = { + "com.sun.tools.javac.code", + "com.sun.tools.javac.comp", + "com.sun.tools.javac.file", + "com.sun.tools.javac.main", + "com.sun.tools.javac.model", + "com.sun.tools.javac.parser", + "com.sun.tools.javac.processing", + "com.sun.tools.javac.tree", + "com.sun.tools.javac.util", + "com.sun.tools.javac.jvm", + }; + + try { + Method m = cModule.getDeclaredMethod("implAddOpens", String.class, cModule); + long firstFieldOffset = getFirstFieldOffset(unsafe); + unsafe.putBooleanVolatile(m, firstFieldOffset, true); + for (String p : allPkgs) m.invoke(jdkCompilerModule, p, ownModule); + } catch (Exception ignore) { + } + } + + private static Object getJdkCompilerModule() { + /* call public api: ModuleLayer.boot().findModule("jdk.compiler").get(); + but use reflection because we don't want this code to crash on jdk1.7 and below. + In that case, none of this stuff was needed in the first place, so we just exit via + the catch block and do nothing. + */ + + try { + Class cModuleLayer = Class.forName("java.lang.ModuleLayer"); + Method mBoot = cModuleLayer.getDeclaredMethod("boot"); + Object bootLayer = mBoot.invoke(null); + Class cOptional = Class.forName("java.util.Optional"); + Method mFindModule = cModuleLayer.getDeclaredMethod("findModule", String.class); + Object oCompilerO = mFindModule.invoke(bootLayer, "jdk.compiler"); + return cOptional.getDeclaredMethod("get").invoke(oCompilerO); + } catch (Exception e) { + return null; + } + } + + private static long getFirstFieldOffset(Unsafe unsafe) { + try { + return unsafe.objectFieldOffset(Parent.class.getDeclaredField("first")); + } catch (NoSuchFieldException e) { + // can't happen. + throw new RuntimeException(e); + } catch (SecurityException e) { + // can't happen + throw new RuntimeException(e); + } + } + + private static Unsafe getUnsafe() { + try { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + return (Unsafe) theUnsafe.get(null); + } catch (Exception e) { + return null; + } + } + + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.processingEnvironment = processingEnv; + this.javacProcessingEnvironment = getJavacProcessingEnvironment(processingEnv); + this.javacFiler = getJavacFiler(processingEnv.getFiler()); + this.trees = Trees.instance(processingEnv); + this.elements = processingEnv.getElementUtils(); + this.names = Names.instance(javacProcessingEnvironment.getContext()); + this.treeMaker = TreeMaker.instance(javacProcessingEnvironment.getContext()); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + Set annotated = roundEnv.getElementsAnnotatedWith(Data.class); + annotated.forEach(e -> { + TypeElement typeElement = (TypeElement) e; + Tree tree = trees.getTree(e); + TypeUtils typeUtils = new TypeUtils(processingEnvironment); + JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) tree; + if (!BuilderProcessor.hasProcessed(classDecl)) { + Data dataAnnotation = e.getAnnotation(Data.class); + Collection persistentValues = ParsedPersistentValue.extractPersistentValues(typeElement, dataAnnotation, typeUtils); + Collection references = ParsedReference.extractReferences(typeElement, dataAnnotation, typeUtils); + ProcessorContext processorContext = new ProcessorContext( + javacProcessingEnvironment.getContext(), + trees, + new TypeUtils(processingEnvironment), + dataAnnotation, + (TypeElement) e, + classDecl, + persistentValues, + references + ); + new BuilderProcessor(processorContext).runProcessor(); + new QueryBuilderProcessor(processorContext).runProcessor(); + } + }); + return !annotations.isEmpty(); + } + + /** + * This class casts the given processing environment to a JavacProcessingEnvironment. In case of + * gradle incremental compilation, the delegate ProcessingEnvironment of the gradle wrapper is returned. + */ + public JavacProcessingEnvironment getJavacProcessingEnvironment(Object procEnv) { + addOpensForLombok(); + if (procEnv instanceof JavacProcessingEnvironment) return (JavacProcessingEnvironment) procEnv; + + // try to find a "delegate" field in the object, and use this to try to obtain a JavacProcessingEnvironment + for (Class procEnvClass = procEnv.getClass(); procEnvClass != null; procEnvClass = procEnvClass.getSuperclass()) { + Object delegate = tryGetDelegateField(procEnvClass, procEnv); + if (delegate == null) delegate = tryGetProxyDelegateToField(procEnvClass, procEnv); + if (delegate == null) delegate = tryGetProcessingEnvField(procEnvClass, procEnv); + + if (delegate != null) return getJavacProcessingEnvironment(delegate); + // delegate field was not found, try on superclass + } + + processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, + "Can't get the delegate of the gradle IncrementalProcessingEnvironment. Lombok won't work."); + return null; + } + + /** + * This class returns the given filer as a JavacFiler. In case the filer is no + * JavacFiler (e.g. the Gradle IncrementalFiler), its "delegate" field is used to get the JavacFiler + * (directly or through a delegate field again) + */ + public JavacFiler getJavacFiler(Object filer) { + if (filer instanceof JavacFiler) return (JavacFiler) filer; + + // try to find a "delegate" field in the object, and use this to check for a JavacFiler + for (Class filerClass = filer.getClass(); filerClass != null; filerClass = filerClass.getSuperclass()) { + Object delegate = tryGetDelegateField(filerClass, filer); + if (delegate == null) delegate = tryGetProxyDelegateToField(filerClass, filer); + if (delegate == null) delegate = tryGetFilerField(filerClass, filer); + + if (delegate != null) return getJavacFiler(delegate); + // delegate field was not found, try on superclass + } + + processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, + "Can't get a JavacFiler from " + filer.getClass().getName() + ". Lombok won't work."); + return null; + } + + /** + * Gradle incremental processing + */ + private Object tryGetDelegateField(Class delegateClass, Object instance) { + try { + return Permit.getField(delegateClass, "delegate").get(instance); + } catch (Exception e) { + return null; + } + } + + /** + * Kotlin incremental processing + */ + private Object tryGetProcessingEnvField(Class delegateClass, Object instance) { + try { + return Permit.getField(delegateClass, "processingEnv").get(instance); + } catch (Exception e) { + return null; + } + } + + /** + * Kotlin incremental processing + */ + private Object tryGetFilerField(Class delegateClass, Object instance) { + try { + return Permit.getField(delegateClass, "filer").get(instance); + } catch (Exception e) { + return null; + } + } + + /** + * IntelliJ IDEA >= 2020.3 + */ + private Object tryGetProxyDelegateToField(Class delegateClass, Object instance) { + try { + InvocationHandler handler = Proxy.getInvocationHandler(instance); + return Permit.getField(handler.getClass(), "val$delegateTo").get(handler); + } catch (Exception e) { + return null; + } + } +} \ No newline at end of file diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/SuperClass.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/SuperClass.java deleted file mode 100644 index 8bf6c84f..00000000 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/SuperClass.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.staticstudios.data.compiler.javac; - -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.util.List; - -public record SuperClass(String simpleName, List superParms, List args) { -} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java new file mode 100644 index 00000000..80c76c41 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java @@ -0,0 +1,391 @@ +package net.staticstudios.data.compiler.javac.javac; + +import com.sun.source.util.Trees; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.comp.*; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.Names; +import net.staticstudios.data.Data; +import net.staticstudios.data.compiler.javac.ProcessorContext; +import net.staticstudios.data.compiler.javac.util.TypeUtils; +import net.staticstudios.data.utils.Link; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.util.ArrayList; + +public abstract class AbstractBuilderProcessor extends PositionedTreeMaker { + protected final Context context; + protected final Trees trees; + protected final Names names; + protected final Enter enter; + protected final MemberEnter memberEnter; + protected final TypeEnter typeEnter; + protected final JCTree.JCClassDecl dataClassDecl; + protected final Data dataAnnotation; + protected final TypeUtils typeUtils; + private final String builderClassSuffix; + private final @Nullable String builderMethodName; + protected JCTree.JCClassDecl builderClassDecl; + + public AbstractBuilderProcessor( + ProcessorContext processorContext, + String builderClassSuffix, + @Nullable String builderMethodName + ) { + super(processorContext.context(), processorContext.dataClassDecl().pos); + this.context = processorContext.context(); + this.trees = processorContext.trees(); + this.names = Names.instance(context); + this.enter = Enter.instance(context); + this.typeEnter = TypeEnter.instance(context); + this.memberEnter = MemberEnter.instance(context); + this.dataClassDecl = processorContext.dataClassDecl(); + this.dataAnnotation = processorContext.dataAnnotation(); + this.builderClassSuffix = builderClassSuffix; + this.builderMethodName = builderMethodName; + this.typeUtils = processorContext.typeUtils(); + } + + protected abstract void process(); + + public void runProcessor() { + if (dataClassDecl.defs.stream() + .anyMatch(def -> def instanceof JCTree.JCClassDecl && + ((JCTree.JCClassDecl) def).name.toString().equals(getBuilderClassName()))) { + return; + } + + makeBuilderClass(); + if (builderMethodName != null) { + makeBuilderMethod(); + makeParameterizedBuilderMethod(); + } + process(); + } + + protected @Nullable SuperClass extending() { + return null; + } + + + protected void makeBuilderClass() { + SuperClass superClass = extending(); + JCTree.JCExpression classExtends; + JCTree.JCExpression superCall; + if (superClass != null) { + classExtends = TypeApply( + chainDots(superClass.fqn().split("\\.")), + superClass.superParms() + ); + superCall = Apply( + List.nil(), + Ident(names.fromString("super")), + superClass.args() + ); + } else { + classExtends = null; + superCall = null; + } + + builderClassDecl = createClass(ClassDef( + Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString(getBuilderClassName()), + List.nil(), + classExtends, + List.nil(), + List.nil() + ), dataClassDecl); + + + java.util.List constructorBodyStatements = new ArrayList<>(); + if (superCall != null) { + constructorBodyStatements.add(Exec(superCall)); + } + + if (this.builderMethodName != null) { + createField(VarDef( + Modifiers(Flags.PRIVATE | Flags.FINAL), + names.fromString("dataManager"), + chainDots("net", "staticstudios", "data", "DataManager"), + null + ), builderClassDecl); + constructorBodyStatements.add( + Exec( + Assign( + Select( + Ident(names.fromString("this")), + names.fromString("dataManager") + ), + Ident(names.fromString("dataManager")) + ) + ) + ); + } + + createMethod(MethodDef( + Modifiers(Flags.PUBLIC), + names.fromString(""), + null, + List.nil(), + this.builderMethodName == null ? + List.nil() + : + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("dataManager"), + chainDots("net", "staticstudios", "data", "DataManager"), + null + ) + ), + List.nil(), + Block(0, List.from(constructorBodyStatements)), + null + ), builderClassDecl); + } + + private void makeBuilderMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString(builderMethodName), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + Block(0, List.of( + Return( + Apply( + List.nil(), + Ident(names.fromString(builderMethodName)), + List.of( + Apply( + List.nil(), + Select( + chainDots("net", "staticstudios", "data", "DataManager"), + names.fromString("getInstance") + ), + List.nil() + ) + ) + ) + ) + )), + null + ), dataClassDecl); + } + + private void makeParameterizedBuilderMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.STATIC), + names.fromString(builderMethodName), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("dataManager"), + chainDots("net", "staticstudios", "data", "DataManager"), + null + ) + ), + List.nil(), + Block(0, List.of( + Return( + NewClass(null, List.nil(), + Ident(names.fromString(getBuilderClassName())), + List.of( + Ident(names.fromString("dataManager")) + ), + null + ) + ) + )), + null + ), dataClassDecl); + } + + public String getBuilderClassName() { + return dataClassDecl.name.toString() + builderClassSuffix; + } + +// public JCTree.JCExpression getBuilderIdent() { +// return chainDots("" +// } + + public String storeSchema(String fieldName, String encoded) { + String schemaFieldName = getStoredSchemaFieldName(fieldName); + storeField(schemaFieldName, encoded); + return schemaFieldName; + } + + public String storeTable(String fieldName, String encoded) { + String tableFieldName = getStoredTableFieldName(fieldName); + storeField(tableFieldName, encoded); + return tableFieldName; + } + + public String storeColumn(String fieldName, String encoded) { + String columnFieldName = getStoredColumnFieldName(fieldName); + storeField(columnFieldName, encoded); + return columnFieldName; + } + + public void storeField(String fieldName, String encoded) { + createField(VarDef( + Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL), + names.fromString(fieldName), + Ident(names.fromString("String")), + Apply( + List.nil(), + Select( + chainDots("net", "staticstudios", "data", "util", "ValueUtils"), + names.fromString("parseValue") + ), + List.of( + Literal(encoded) + ) + ) + ), builderClassDecl); + } + + public String getStoredSchemaFieldName(String fieldName) { + return fieldName + "$schema"; + } + + public String getStoredTableFieldName(String fieldName) { + return fieldName + "$table"; + } + + public String getStoredColumnFieldName(String fieldName) { + return fieldName + "$column"; + } + + public void storeLinks(String fieldName, java.util.List links) { + String referringColumnsFieldName = fieldName + "$referringColumns"; + String referencedColumnsFieldName = fieldName + "$referencedColumns"; + + createField(VarDef( + Modifiers(Flags.PRIVATE | Flags.FINAL), + names.fromString(referringColumnsFieldName), + TypeArray(Ident(names.fromString("String"))), + NewArray( + Ident(names.fromString("String")), + List.nil(), + List.from( + links.stream().map(link -> + Literal(link.columnInReferringTable()) + ).toList() + ) + ) + ), builderClassDecl); + + createField(VarDef( + Modifiers(Flags.PRIVATE | Flags.FINAL), + names.fromString(referencedColumnsFieldName), + TypeArray(Ident(names.fromString("String"))), + NewArray( + Ident(names.fromString("String")), + List.nil(), + List.from( + links.stream().map(link -> + Literal(link.columnInReferencedTable()) + ).toList() + ) + ) + ), builderClassDecl); + } + + public String getStoredReferringColumnsFieldName(String fieldName) { + return fieldName + "$referringColumns"; + } + + public String getStoredReferencedColumnsFieldName(String fieldName) { + return fieldName + "$referencedColumns"; + } + + public JCTree.JCClassDecl createClass(JCTree.JCClassDecl classDecl, JCTree.JCClassDecl containingClassDecl) { + containingClassDecl.defs = containingClassDecl.defs.append(classDecl); + Symbol.ClassSymbol owner = containingClassDecl.sym; + Env env = enter.getEnv(owner); + + if (env == null) { + env = enter.getClassEnv(owner); + } + +// classEnter(classDecl, env); + return classDecl; + } + + public JCTree.JCMethodDecl createMethod(JCTree.JCMethodDecl methodDecl, JCTree.JCClassDecl classDecl) { + classDecl.defs = classDecl.defs.append(methodDecl); + Symbol.ClassSymbol owner = classDecl.sym; + Env env = enter.getEnv(owner); + + if (env == null) { + env = enter.getClassEnv(owner); + } + +// memberEnter(methodDecl, env); + + return methodDecl; + } + + public JCTree.JCVariableDecl createField(JCTree.JCVariableDecl fieldDecl, JCTree.JCClassDecl classDecl) { + classDecl.defs = classDecl.defs.append(fieldDecl); + + Symbol.ClassSymbol owner = classDecl.sym; + Env env = enter.getEnv(owner); + + if (env == null) { + env = enter.getClassEnv(owner); + } +// memberEnter(fieldDecl, env); + + return fieldDecl; + } + + private void classEnter(JCTree tree, Env env) { + try { + Method method = Enter.class.getDeclaredMethod("classEnter", JCTree.class, Env.class); + method.setAccessible(true); + method.invoke(enter, tree, env); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + private void classEnter(List trees, Env env) { + try { + Method method = Enter.class.getDeclaredMethod("classEnter", List.class, Env.class); + method.setAccessible(true); + method.invoke(enter, trees, env); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void memberEnter(JCTree tree, Env env) { + try { + Method method = MemberEnter.class.getDeclaredMethod("memberEnter", JCTree.class, Env.class); + method.setAccessible(true); + method.invoke(memberEnter, tree, env); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected void memberEnter(List trees, Env env) { + try { + Method method = MemberEnter.class.getDeclaredMethod("memberEnter", List.class, Env.class); + method.setAccessible(true); + method.invoke(memberEnter, trees, env); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java similarity index 52% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java rename to javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java index 15efa90f..8702a6f4 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/BuilderProcessor.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java @@ -1,11 +1,11 @@ -package net.staticstudios.data.compiler.javac; +package net.staticstudios.data.compiler.javac.javac; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.TypeTag; import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.List; -import com.sun.tools.javac.util.Names; +import net.staticstudios.data.InsertStrategy; +import net.staticstudios.data.compiler.javac.ProcessorContext; import java.util.ArrayList; import java.util.Collection; @@ -14,12 +14,10 @@ public class BuilderProcessor extends AbstractBuilderProcessor { private final Collection persistentValues; private final Collection references; - public BuilderProcessor(JCTree.JCCompilationUnit compilationUnit, TreeMaker treeMaker, Names names, JCTree.JCClassDecl dataClassDecl, ParsedDataAnnotation dataAnnotation, - Collection persistentValues, Collection references - ) { - super(compilationUnit, treeMaker, names, dataClassDecl, dataAnnotation, "Builder", "builder"); - this.persistentValues = persistentValues; - this.references = references; + public BuilderProcessor(ProcessorContext processorContext) { + super(processorContext, "Builder", "builder"); + this.persistentValues = processorContext.persistentValues(); + this.references = processorContext.references(); } public static boolean hasProcessed(JCTree.JCClassDecl classDecl) { @@ -28,16 +26,6 @@ public static boolean hasProcessed(JCTree.JCClassDecl classDecl) { ((JCTree.JCClassDecl) def).name.toString().equals(classDecl.name + "Builder")); } - @Override - protected void addImports() { - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "ValueUtils"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.insert", "InsertContext"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "InsertMode"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data", "InsertStrategy"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "UniqueDataMetadata"); - JavaCPluginUtils.importClass(compilationUnit, treeMaker, names, "net.staticstudios.data.util", "ColumnValuePair"); - } - @Override protected void process() { for (ParsedPersistentValue pv : persistentValues) { @@ -58,42 +46,43 @@ private void processValue(ParsedPersistentValue pv) { storeTable(pv.getFieldName(), pv.getTable()); storeColumn(pv.getFieldName(), pv.getColumn()); - JCTree.JCExpression nullInit = treeMaker.Literal(TypeTag.BOT, null); - - JavaCPluginUtils.generatePrivateMemberField(treeMaker, names, builderClassDecl, pv.getFieldName(), pv.getType(), nullInit); + createField(VarDef( + Modifiers(Flags.PRIVATE), + names.fromString(pv.getFieldName()), + chainDots(pv.getTypeFQNParts()), + Literal(TypeTag.BOT, null) + ), builderClassDecl); - JCTree.JCMethodDecl setterMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), names.fromString(pv.getFieldName()), - treeMaker.Ident(names.fromString(getBuilderClassName())), + Ident(names.fromString(getBuilderClassName())), List.nil(), List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), + VarDef( + Modifiers(Flags.PARAMETER), names.fromString(pv.getFieldName()), - pv.getType(), + chainDots(pv.getTypeFQNParts()), null ) ), List.nil(), - treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + Block(0, List.of( + Exec( + Assign( + Select( + Ident(names.fromString("this")), names.fromString(pv.getFieldName()) ), - treeMaker.Ident(names.fromString(pv.getFieldName())) + Ident(names.fromString(pv.getFieldName())) ) ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) + Return( + Ident(names.fromString("this")) ) )), null - ); - - builderClassDecl.defs = builderClassDecl.defs.append(setterMethod); + ), builderClassDecl); } private void processReference(ParsedReference ref) { @@ -101,29 +90,41 @@ private void processReference(ParsedReference ref) { String schemaFieldName = ref.getFieldName() + "_reference$schema"; String tableFieldName = ref.getFieldName() + "_reference$table"; - JCTree.JCExpression arrayType = treeMaker.TypeArray(treeMaker.Ident(names.fromString("ColumnValuePair"))); - JCTree.JCExpression stringType = treeMaker.Ident(names.fromString("String")); - - JCTree.JCExpression nullInit = treeMaker.Literal(TypeTag.BOT, null); - - JavaCPluginUtils.generatePrivateMemberField(treeMaker, names, builderClassDecl, idColumnValuePairsFieldName, arrayType, nullInit); - JavaCPluginUtils.generatePrivateMemberField(treeMaker, names, builderClassDecl, schemaFieldName, stringType, nullInit); - JavaCPluginUtils.generatePrivateMemberField(treeMaker, names, builderClassDecl, tableFieldName, stringType, nullInit); - - var handleNotNull = treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + createField(VarDef( + Modifiers(Flags.PRIVATE), + names.fromString(idColumnValuePairsFieldName), + TypeArray(chainDots("net", "staticstudios", "data", "util", "ColumnValuePair")), + Literal(TypeTag.BOT, null) + ), builderClassDecl); + + createField(VarDef( + Modifiers(Flags.PRIVATE), + names.fromString(schemaFieldName), + Ident(names.fromString("String")), + Literal(TypeTag.BOT, null) + ), builderClassDecl); + + createField(VarDef( + Modifiers(Flags.PRIVATE), + names.fromString(tableFieldName), + Ident(names.fromString("String")), + Literal(TypeTag.BOT, null) + ), builderClassDecl); + + var handleNotNull = Block(0, List.of( + Exec( + Assign( + Select( + Ident(names.fromString("this")), names.fromString(idColumnValuePairsFieldName) ), - treeMaker.Apply( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Apply( + Select( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString(ref.getFieldName())), + Select( + Ident(names.fromString(ref.getFieldName())), names.fromString("getIdColumns") ), List.nil() @@ -134,45 +135,45 @@ private void processReference(ParsedReference ref) { ) ) ), - treeMaker.VarDef( - treeMaker.Modifiers(0), + VarDef( + Modifiers(0), names.fromString("__$metadata"), - treeMaker.Ident(names.fromString("UniqueDataMetadata")), - treeMaker.Apply( + chainDots("net", "staticstudios", "data", "util", "UniqueDataMetadata"), + Apply( List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString(ref.getFieldName())), + Select( + Ident(names.fromString(ref.getFieldName())), names.fromString("getMetadata") ), List.nil() ) ), - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + Exec( + Assign( + Select( + Ident(names.fromString("this")), names.fromString(schemaFieldName) ), - treeMaker.Apply( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("__$metadata")), + Select( + Ident(names.fromString("__$metadata")), names.fromString("schema") ), List.nil() ) ) ), - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + Exec( + Assign( + Select( + Ident(names.fromString("this")), names.fromString(tableFieldName) ), - treeMaker.Apply( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("__$metadata")), + Select( + Ident(names.fromString("__$metadata")), names.fromString("table") ), List.nil() @@ -181,91 +182,90 @@ private void processReference(ParsedReference ref) { ) )); - var handleNull = treeMaker.Block(0, List.of( - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + var handleNull = Block(0, List.of( + Exec( + Assign( + Select( + Ident(names.fromString("this")), names.fromString(idColumnValuePairsFieldName) ), - treeMaker.Literal(TypeTag.BOT, null) + Literal(TypeTag.BOT, null) ) ), - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + Exec( + Assign( + Select( + Ident(names.fromString("this")), names.fromString(schemaFieldName) ), - treeMaker.Literal(TypeTag.BOT, null) + Literal(TypeTag.BOT, null) ) ), - treeMaker.Exec( - treeMaker.Assign( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + Exec( + Assign( + Select( + Ident(names.fromString("this")), names.fromString(tableFieldName) ), - treeMaker.Literal(TypeTag.BOT, null) + Literal(TypeTag.BOT, null) ) ) )); - JCTree.JCMethodDecl setterMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), names.fromString(ref.getFieldName()), - treeMaker.Ident(names.fromString(getBuilderClassName())), + Ident(names.fromString(getBuilderClassName())), List.nil(), List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), + VarDef( + Modifiers(Flags.PARAMETER), names.fromString(ref.getFieldName()), - ref.getType(), + chainDots(ref.getTypeFQNParts()), null ) ), List.nil(), - treeMaker.Block(0, List.of( - treeMaker.If( - treeMaker.Binary( + Block(0, List.of( + If( + Binary( JCTree.Tag.NE, - treeMaker.Ident(names.fromString(ref.getFieldName())), - treeMaker.Literal(TypeTag.BOT, null) + Ident(names.fromString(ref.getFieldName())), + Literal(TypeTag.BOT, null) ), handleNotNull, handleNull ), - treeMaker.Return( - treeMaker.Ident(names.fromString("this")) + Return( + Ident(names.fromString("this")) ) )), null - ); - builderClassDecl.defs = builderClassDecl.defs.append(setterMethod); + ), builderClassDecl); } private void makeInsertContextMethod(Collection parsedPersistentValues, Collection parsedReferences) { java.util.List bodyStatements = new ArrayList<>(); for (ParsedPersistentValue pv : parsedPersistentValues) { - JCTree.JCExpression schemaFieldAccess = treeMaker.Ident(names.fromString(pv.getFieldName() + "$schema")); - JCTree.JCExpression tableFieldAccess = treeMaker.Ident(names.fromString(pv.getFieldName() + "$table")); - JCTree.JCExpression columnFieldAccess = treeMaker.Ident(names.fromString(pv.getFieldName() + "$column")); + JCTree.JCExpression schemaFieldAccess = Ident(names.fromString(pv.getFieldName() + "$schema")); + JCTree.JCExpression tableFieldAccess = Ident(names.fromString(pv.getFieldName() + "$table")); + JCTree.JCExpression columnFieldAccess = Ident(names.fromString(pv.getFieldName() + "$column")); - JCTree.JCExpression fieldAccess = treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + JCTree.JCExpression fieldAccess = Select( + Ident(names.fromString("this")), names.fromString(pv.getFieldName()) ); - String insertStrategy = null; + InsertStrategy insertStrategy = null; if (pv instanceof ParsedForeignPersistentValue foreignPv) { insertStrategy = foreignPv.getInsertStrategy(); } - JCTree.JCExpression insertStatement = treeMaker.Apply( + JCTree.JCExpression insertStatement = Apply( List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("ctx")), + Select( + Ident(names.fromString("ctx")), names.fromString("set") ), List.of( @@ -274,16 +274,16 @@ private void makeInsertContextMethod(Collection parsedPer columnFieldAccess, fieldAccess, insertStrategy != null ? - treeMaker.Select( - treeMaker.Ident(names.fromString("InsertStrategy")), - names.fromString(insertStrategy) + Select( + chainDots("net", "staticstudios", "data", "InsertStrategy"), + names.fromString(insertStrategy.name()) ) : - treeMaker.Literal(TypeTag.BOT, null) + Literal(TypeTag.BOT, null) ) ); - bodyStatements.add(treeMaker.Exec(insertStatement)); + bodyStatements.add(Exec(insertStatement)); } for (ParsedReference ref : parsedReferences) { @@ -293,80 +293,80 @@ private void makeInsertContextMethod(Collection parsedPer bodyStatements.add( - treeMaker.If( - treeMaker.Binary( + If( + Binary( JCTree.Tag.NE, - treeMaker.Ident(names.fromString(idColumnValuePairsFieldName)), - treeMaker.Literal(TypeTag.BOT, null) + Ident(names.fromString(idColumnValuePairsFieldName)), + Literal(TypeTag.BOT, null) ), - treeMaker.ForLoop( + ForLoop( List.of( - treeMaker.VarDef( - treeMaker.Modifiers(0), + VarDef( + Modifiers(0), names.fromString("i"), - treeMaker.TypeIdent(TypeTag.INT), - treeMaker.Literal(0) + TypeIdent(TypeTag.INT), + Literal(0) ) ), - treeMaker.Binary( + Binary( JCTree.Tag.LT, - treeMaker.Ident(names.fromString("i")), - treeMaker.Select( - treeMaker.Ident(names.fromString(idColumnValuePairsFieldName)), + Ident(names.fromString("i")), + Select( + Ident(names.fromString(idColumnValuePairsFieldName)), names.fromString("length") ) ), List.of( - treeMaker.Exec( - treeMaker.Unary( + Exec( + Unary( JCTree.Tag.POSTINC, - treeMaker.Ident(names.fromString("i")) + Ident(names.fromString("i")) ) ) ), - treeMaker.Block( + Block( 0, List.of( - treeMaker.Exec( - treeMaker.Apply( + Exec( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("ctx")), + Select( + Ident(names.fromString("ctx")), names.fromString("set") ), List.of( - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + Select( + Ident(names.fromString("this")), names.fromString(schemaFieldName) ), - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + Select( + Ident(names.fromString("this")), names.fromString(tableFieldName) ), - treeMaker.Apply( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Indexed( - treeMaker.Ident(names.fromString(idColumnValuePairsFieldName)), - treeMaker.Ident(names.fromString("i")) + Select( + Indexed( + Ident(names.fromString(idColumnValuePairsFieldName)), + Ident(names.fromString("i")) ), names.fromString("column") ), List.nil() ), - treeMaker.Apply( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Indexed( - treeMaker.Ident(names.fromString(idColumnValuePairsFieldName)), - treeMaker.Ident(names.fromString("i")) + Select( + Indexed( + Ident(names.fromString(idColumnValuePairsFieldName)), + Ident(names.fromString("i")) ), names.fromString("value") ), List.nil() ), - treeMaker.Select( - treeMaker.Ident(names.fromString("InsertStrategy")), + Select( + chainDots("net", "staticstudios", "data", "InsertStrategy"), names.fromString("OVERWRITE_EXISTING") ) ) @@ -381,86 +381,86 @@ private void makeInsertContextMethod(Collection parsedPer ); } - JCTree.JCMethodDecl insertMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), names.fromString("insert"), - treeMaker.TypeIdent(TypeTag.VOID), + TypeIdent(TypeTag.VOID), List.nil(), List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), + VarDef( + Modifiers(Flags.PARAMETER), names.fromString("ctx"), - treeMaker.Ident(names.fromString("InsertContext")), + chainDots("net", "staticstudios", "data", "insert", "InsertContext"), null ) ), List.nil(), - treeMaker.Block(0, List.from(bodyStatements)), + Block(0, List.from(bodyStatements)), null - ); - builderClassDecl.defs = builderClassDecl.defs.append(insertMethod); + ), builderClassDecl); + } public void makeInsertModeMethod() { - JCTree.JCMethodDecl insertMethod = treeMaker.MethodDef( - treeMaker.Modifiers(Flags.PUBLIC | Flags.FINAL), + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), names.fromString("insert"), - treeMaker.Ident(dataClassDecl.name), + Ident(dataClassDecl.name), List.nil(), List.of( - treeMaker.VarDef( - treeMaker.Modifiers(Flags.PARAMETER), + VarDef( + Modifiers(Flags.PARAMETER), names.fromString("mode"), - treeMaker.Ident(names.fromString("InsertMode")), + chainDots("net", "staticstudios", "data", "InsertMode"), null ) ), List.nil(), - treeMaker.Block(0, List.of( - treeMaker.VarDef( - treeMaker.Modifiers(0), + Block(0, List.of( + VarDef( + Modifiers(0), names.fromString("ctx"), - treeMaker.Ident(names.fromString("InsertContext")), - treeMaker.Apply( + chainDots("net", "staticstudios", "data", "insert", "InsertContext"), + Apply( List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("dataManager")), + Select( + Ident(names.fromString("dataManager")), names.fromString("createInsertContext") ), List.nil() ) ), - treeMaker.Exec( - treeMaker.Apply( + Exec( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("this")), + Select( + Ident(names.fromString("this")), names.fromString("insert") ), List.of( - treeMaker.Ident(names.fromString("ctx")) + Ident(names.fromString("ctx")) ) ) ), - treeMaker.Return( - treeMaker.Apply( + Return( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Apply( + Select( + Apply( List.nil(), - treeMaker.Select( - treeMaker.Ident(names.fromString("ctx")), + Select( + Ident(names.fromString("ctx")), names.fromString("insert") ), List.of( - treeMaker.Ident(names.fromString("mode")) + Ident(names.fromString("mode")) ) ), names.fromString("get") ), List.of( - treeMaker.Select( - treeMaker.Ident(dataClassDecl.name), + Select( + Ident(dataClassDecl.name), names.fromString("class") ) ) @@ -468,7 +468,7 @@ public void makeInsertModeMethod() { ) )), null - ); - builderClassDecl.defs = builderClassDecl.defs.append(insertMethod); + ), builderClassDecl); + } } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java similarity index 73% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java rename to javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java index c19f2751..04a234fe 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ParsedForeignPersistentValue.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java @@ -1,21 +1,22 @@ -package net.staticstudios.data.compiler.javac; +package net.staticstudios.data.compiler.javac.javac; -import com.sun.tools.javac.tree.JCTree; +import net.staticstudios.data.InsertStrategy; import net.staticstudios.data.utils.Link; +import javax.lang.model.element.TypeElement; import java.util.List; class ParsedForeignPersistentValue extends ParsedPersistentValue { - private final String insertStrategy; + private final InsertStrategy insertStrategy; private final List links; - public ParsedForeignPersistentValue(String fieldName, String schema, String table, String column, boolean nullable, JCTree.JCExpression type, String insertStrategy, List links) { + public ParsedForeignPersistentValue(String fieldName, String schema, String table, String column, boolean nullable, TypeElement type, InsertStrategy insertStrategy, List links) { super(fieldName, schema, table, column, nullable, type); this.insertStrategy = insertStrategy; this.links = links; } - public String getInsertStrategy() { + public InsertStrategy getInsertStrategy() { return insertStrategy; } diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java new file mode 100644 index 00000000..216cef41 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java @@ -0,0 +1,152 @@ +package net.staticstudios.data.compiler.javac.javac; + +import net.staticstudios.data.*; +import net.staticstudios.data.compiler.javac.util.SimpleField; +import net.staticstudios.data.compiler.javac.util.TypeUtils; +import net.staticstudios.data.utils.Constants; +import net.staticstudios.data.utils.Link; +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 ParsedPersistentValue { + private final String fieldName; + private final String schema; + private final String table; + private final String column; + private final boolean nullable; + private final TypeElement type; + + public ParsedPersistentValue(String fieldName, String schema, String table, String column, boolean nullable, TypeElement type) { + this.fieldName = fieldName; + this.schema = schema; + this.table = table; + this.column = column; + this.nullable = nullable; + this.type = type; + } + + public static Collection extractPersistentValues(@NotNull TypeElement dataClass, + @NotNull Data dataAnnotation, + @NotNull TypeUtils typeUtils + + ) { + Collection persistentValues = new ArrayList<>(); + Collection fields = typeUtils.getFields(dataClass, Constants.PERSISTENT_VALUE_FQN); + for (SimpleField pvField : fields) { + Element fieldElement = pvField.element(); + Column columnAnnotation = fieldElement.getAnnotation(Column.class); + IdColumn idColumnAnnotation = fieldElement.getAnnotation(IdColumn.class); + ForeignColumn foreignColumnAnnotation = fieldElement.getAnnotation(ForeignColumn.class); + if (columnAnnotation == null && idColumnAnnotation == null && foreignColumnAnnotation == null) { + continue; + } + + String columnName; + String schemaValue; + String tableValue; + boolean nullable; + + if (idColumnAnnotation != null) { + columnName = idColumnAnnotation.name(); + schemaValue = dataAnnotation.schema(); + tableValue = dataAnnotation.table(); + nullable = false; + } else if (foreignColumnAnnotation != null) { + columnName = foreignColumnAnnotation.name(); + schemaValue = foreignColumnAnnotation.schema().isEmpty() ? dataAnnotation.schema() : foreignColumnAnnotation.schema(); + tableValue = foreignColumnAnnotation.table().isEmpty() ? dataAnnotation.table() : foreignColumnAnnotation.table(); + nullable = foreignColumnAnnotation.nullable(); + + + } else { + columnName = columnAnnotation.name(); + schemaValue = dataAnnotation.schema(); + tableValue = dataAnnotation.table(); + nullable = columnAnnotation.nullable(); + } + + TypeMirror genericTypeMirror = typeUtils.getGenericType(fieldElement, 0); + TypeElement typeElement = (TypeElement) ((DeclaredType) genericTypeMirror).asElement(); + ParsedPersistentValue persistentValue; + + if (foreignColumnAnnotation != null) { + InsertStrategy insertStrategy = null; + Insert insertAnnotation = fieldElement.getAnnotation(Insert.class); + if (insertAnnotation != null) { + insertStrategy = insertAnnotation.value(); + } + + + persistentValue = new ParsedForeignPersistentValue( + pvField.name(), + schemaValue, + tableValue, + columnName, + nullable, + typeElement, + insertStrategy, + Link.parseRawLinks(foreignColumnAnnotation.link()) + ); + } else { + persistentValue = new ParsedPersistentValue( + pvField.name(), + schemaValue, + tableValue, + columnName, + nullable, + typeElement + ); + } + + persistentValues.add(persistentValue); + + } + + return persistentValues; + } + + public String getFieldName() { + return fieldName; + } + + public String getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getColumn() { + return column; + } + + public boolean isNullable() { + return nullable; + } + + public TypeElement getType() { + return type; + } + + public String[] getTypeFQNParts() { + return type.getQualifiedName().toString().split("\\."); + } + + @Override + public String toString() { + return "PersistentValue{" + + "fieldName='" + fieldName + '\'' + + ", schema='" + schema + '\'' + + ", table='" + table + '\'' + + ", column='" + column + '\'' + + ", type=" + type + + '}'; + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java new file mode 100644 index 00000000..bbc939fc --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java @@ -0,0 +1,81 @@ +package net.staticstudios.data.compiler.javac.javac; + +import net.staticstudios.data.Data; +import net.staticstudios.data.OneToOne; +import net.staticstudios.data.compiler.javac.util.SimpleField; +import net.staticstudios.data.compiler.javac.util.TypeUtils; +import net.staticstudios.data.utils.Constants; +import net.staticstudios.data.utils.Link; +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; +import java.util.List; + +public class ParsedReference { + private final String fieldName; + private final List links; + private final TypeElement type; + + public ParsedReference(String fieldName, List links, TypeElement type) { + this.fieldName = fieldName; + this.links = links; + this.type = type; + } + + public static Collection extractReferences(@NotNull TypeElement dataClass, + @NotNull Data dataAnnotation, + @NotNull TypeUtils typeUtils + + ) { + Collection references = new ArrayList<>(); + Collection fields = typeUtils.getFields(dataClass, Constants.REFERENCE_FQN); + for (SimpleField refField : fields) { + Element fieldElement = refField.element(); + OneToOne oneToOneAnnotation = fieldElement.getAnnotation(OneToOne.class); + if (oneToOneAnnotation == null) { + continue; + } + + TypeMirror genericTypeMirror = typeUtils.getGenericType(fieldElement, 0); + TypeElement typeElement = (TypeElement) ((DeclaredType) genericTypeMirror).asElement(); + ParsedReference parsedReference = new ParsedReference( + refField.name(), + Link.parseRawLinks(oneToOneAnnotation.link()), + typeElement + ); + references.add(parsedReference); + } + + return references; + } + + public String getFieldName() { + return fieldName; + } + + public List getLinks() { + return links; + } + + public TypeElement getType() { + return type; + } + + public String[] getTypeFQNParts() { + return type.getQualifiedName().toString().split("\\."); + } + + @Override + public String toString() { + return "ParsedReference{" + + "fieldName='" + fieldName + '\'' + + ", links=" + links + + ", type=" + type + + '}'; + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java new file mode 100644 index 00000000..1a1ce9d9 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java @@ -0,0 +1,181 @@ +package net.staticstudios.data.compiler.javac.javac; + +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.TypeTag; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.Name; +import com.sun.tools.javac.util.Names; + +public class PositionedTreeMaker { + private final TreeMaker treeMaker; + private final Names names; + private final int pos; + + public PositionedTreeMaker(Context context, int pos) { + this.treeMaker = TreeMaker.instance(context); + this.names = Names.instance(context); + this.pos = pos; + } + + public JCTree.JCExpression chainDots(String... namesArray) { + treeMaker.at(pos); + JCTree.JCExpression expr = treeMaker.Ident(this.names.fromString(namesArray[0])); + + for (int i = 1; i < namesArray.length; i++) { + treeMaker.at(pos); + expr = treeMaker.Select(expr, this.names.fromString(namesArray[i])); + } + return expr; + } + + public Name toName(String s) { + return names.fromString(s); + } + + public JCTree.JCFieldAccess Select(JCTree.JCExpression expr, Name name) { + return treeMaker.at(pos).Select(expr, name); + } + + public JCTree.JCIdent Ident(Name name) { + return treeMaker.at(pos).Ident(name); + } + + public JCTree.JCLiteral Literal(Object value) { + return treeMaker.at(pos).Literal(value); + } + + public JCTree.JCLiteral Literal(TypeTag tag, Object value) { + return treeMaker.at(pos).Literal(tag, value); + } + + public JCTree.JCClassDecl ClassDef(JCTree.JCModifiers mods, Name name, List typarams, JCTree.JCExpression extending, List implementing, List defs) { + return treeMaker.at(pos).ClassDef(mods, name, typarams, extending, implementing, defs); + } + + public JCTree.JCMethodDecl MethodDef(JCTree.JCModifiers mods, Name name, JCTree.JCExpression resType, List typarams, List params, List thrown, JCTree.JCBlock body, JCTree.JCExpression defaultValue) { + return treeMaker.at(pos).MethodDef(mods, name, resType, typarams, params, thrown, body, defaultValue); + } + + public JCTree.JCVariableDecl VarDef(JCTree.JCModifiers mods, Name name, JCTree.JCExpression vartype, JCTree.JCExpression init) { + return treeMaker.at(pos).VarDef(mods, name, vartype, init); + } + + public JCTree.JCModifiers Modifiers(long flags, List annotations) { + return treeMaker.at(pos).Modifiers(flags, annotations); + } + + /** + * Creates Modifiers with no annotations. + */ + public JCTree.JCModifiers Modifiers(long flags) { + return treeMaker.at(pos).Modifiers(flags, List.nil()); + } + + public JCTree.JCBlock Block(long flags, List stats) { + return treeMaker.at(pos).Block(flags, stats); + } + + public JCTree.JCMethodInvocation Apply(List typeargs, JCTree.JCExpression fn, List args) { + return treeMaker.at(pos).Apply(typeargs, fn, args); + } + + public JCTree.JCNewClass NewClass(JCTree.JCExpression encl, List typeargs, JCTree.JCExpression clazz, List args, JCTree.JCClassDecl def) { + return treeMaker.at(pos).NewClass(encl, typeargs, clazz, args, def); + } + + public JCTree.JCAssign Assign(JCTree.JCExpression lhs, JCTree.JCExpression rhs) { + return treeMaker.at(pos).Assign(lhs, rhs); + } + + public JCTree.JCExpressionStatement Exec(JCTree.JCExpression expr) { + return treeMaker.at(pos).Exec(expr); + } + + public JCTree.JCReturn Return(JCTree.JCExpression expr) { + return treeMaker.at(pos).Return(expr); + } + + public JCTree.JCTypeApply TypeApply(JCTree.JCExpression clazz, List args) { + return treeMaker.at(pos).TypeApply(clazz, args); + } + + public JCTree.JCArrayTypeTree TypeArray(JCTree.JCExpression elemtype) { + return treeMaker.at(pos).TypeArray(elemtype); + } + + public JCTree.JCTypeParameter TypeParameter(Name name, List bounds) { + return treeMaker.at(pos).TypeParameter(name, bounds); + } + + public JCTree.JCImport Import(JCTree.JCFieldAccess qualid, boolean staticImport) { + return treeMaker.at(pos).Import(qualid, staticImport); + } + + public JCTree.JCTypeCast TypeCast(JCTree.JCExpression type, JCTree.JCExpression expr) { + return treeMaker.at(pos).TypeCast(type, expr); + } + + public JCTree.JCTry Try(JCTree.JCBlock body, List catchers, JCTree.JCBlock finalizer) { + return treeMaker.at(pos).Try(body, catchers, finalizer); + } + + public JCTree.JCCatch Catch(JCTree.JCVariableDecl param, JCTree.JCBlock body) { + return treeMaker.at(pos).Catch(param, body); + } + + public JCTree.JCAssert Assert(JCTree.JCExpression cond, JCTree.JCExpression detail) { + return treeMaker.at(pos).Assert(cond, detail); + } + + public JCTree.JCBinary Binary(JCTree.Tag tag, JCTree.JCExpression lhs, JCTree.JCExpression rhs) { + return treeMaker.at(pos).Binary(tag, lhs, rhs); + } + + public JCTree.JCUnary Unary(JCTree.Tag tag, JCTree.JCExpression arg) { + return treeMaker.at(pos).Unary(tag, arg); + } + + public JCTree.JCNewArray NewArray(JCTree.JCExpression elemtype, List dims, List elems) { + return treeMaker.at(pos).NewArray(elemtype, dims, elems); + } + + public JCTree.JCPrimitiveTypeTree TypeIdent(TypeTag tag) { + return treeMaker.at(pos).TypeIdent(tag); + } + + public JCTree.JCArrayAccess Indexed(JCTree.JCExpression array, JCTree.JCExpression index) { + return treeMaker.at(pos).Indexed(array, index); + } + + public JCTree.JCArrayAccess Indexed(Symbol v, JCTree.JCExpression index) { + return treeMaker.at(pos).Indexed(v, index); + } + + public JCTree.JCIf If(JCTree.JCExpression cond, JCTree.JCStatement thenpart, JCTree.JCStatement elsepart) { + return treeMaker.at(pos).If(cond, thenpart, elsepart); + } + + public JCTree.JCForLoop ForLoop(List init, JCTree.JCExpression cond, List step, JCTree.JCStatement body) { + return treeMaker.at(pos).ForLoop(init, cond, step, body); + } + + public JCTree.JCEnhancedForLoop ForeachLoop(JCTree.JCVariableDecl var, JCTree.JCExpression expr, JCTree.JCBlock body) { + return treeMaker.at(pos).ForeachLoop(var, expr, body); + } + + public JCTree.JCConditional Conditional(JCTree.JCExpression cond, JCTree.JCExpression truepart, JCTree.JCExpression falsepart) { + return treeMaker.at(pos).Conditional(cond, truepart, falsepart); + } + + public JCTree.JCAnnotation Annotation(JCTree.JCExpression annotationType, List args) { + return treeMaker.at(pos).Annotation(annotationType, args); + } + + public JCTree.JCTypeUnion TypeUnion(List alternatives) { + return treeMaker.at(pos).TypeUnion(alternatives); + } + +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java new file mode 100644 index 00000000..f30d8a0a --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java @@ -0,0 +1,1111 @@ +package net.staticstudios.data.compiler.javac.javac; + +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.TypeTag; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.List; +import net.staticstudios.data.compiler.javac.ProcessorContext; +import net.staticstudios.data.utils.StringUtils; +import org.jetbrains.annotations.Nullable; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +public class QueryBuilderProcessor extends AbstractBuilderProcessor { + private final Collection persistentValues; + private final Collection references; + private final String whereClassName; + + public QueryBuilderProcessor(ProcessorContext processorContext) { + super(processorContext, "QueryBuilder", "query"); + this.persistentValues = processorContext.persistentValues(); + this.references = processorContext.references(); + + QueryWhereProcessor whereProcessor = new QueryWhereProcessor(processorContext); + this.whereClassName = whereProcessor.getBuilderClassName(); + whereProcessor.runProcessor(); + } + + @Override + protected @Nullable SuperClass extending() { + return new SuperClass( + "net.staticstudios.data.query.BaseQueryBuilder", + List.of( + Ident(dataClassDecl.name), + Ident(names.fromString(whereClassName)) + ), + List.of( + Ident(names.fromString("dataManager")), + Select( + Ident(dataClassDecl.name), + names.fromString("class") + ), + NewClass( + null, + List.nil(), + Ident(names.fromString(whereClassName)), + List.nil(), + null + ) + ) + ); + } + + @Override + protected void process() { + addWhereMethod(); + addLimitMethod(); + addOffsetMethod(); + + for (ParsedPersistentValue pv : persistentValues) { + processValue(pv); + } + } + + + private void processValue(ParsedPersistentValue pv) { + String schemaFieldName = storeSchema(pv.getFieldName(), pv.getSchema()); + String tableFieldName = storeTable(pv.getFieldName(), pv.getTable()); + String columnFieldName = storeColumn(pv.getFieldName(), pv.getColumn()); + + addOrderByMethod(schemaFieldName, tableFieldName, columnFieldName, pv.getFieldName()); + } + + private void addOrderByMethod(String schemaFieldName, String tableFieldName, String columnFieldName, String fieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("orderBy" + StringUtils.capitalize(fieldName)), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("order"), + chainDots("net", "staticstudios", "data", "Order"), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("setOrderBy") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString("order")) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + + } + + private void addLimitMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("limit"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("limit"), + TypeIdent(TypeTag.INT), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("setLimit") + ), + List.of( + Ident(names.fromString("limit")) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + + } + + private void addOffsetMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("offset"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("offset"), + TypeIdent(TypeTag.INT), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("setOffset") + ), + List.of( + Ident(names.fromString("offset")) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + + } + + private void addWhereMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("where"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("function"), + TypeApply( + chainDots("java", "util", "function", "Function"), + List.of( + Ident(names.fromString(whereClassName)), + Ident(names.fromString(whereClassName)) + ) + ), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("function")), + names.fromString("apply") + ), + List.of( + Select( + Ident(names.fromString("super")), + names.fromString("where") + ) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + + } + + class QueryWhereProcessor extends AbstractBuilderProcessor { + private String dataSchemaFieldName; + private String dataTableFieldName; + + public QueryWhereProcessor(ProcessorContext processorContext) { + super(processorContext, "QueryWhere", null); + } + + @Override + protected @Nullable SuperClass extending() { + return new SuperClass( + "net.staticstudios.data.query.BaseQueryWhere", + List.nil(), + List.nil() + ); + } + + @Override + protected void process() { + dataSchemaFieldName = storeSchema("data", dataAnnotation.schema()); + dataTableFieldName = storeTable("data", dataAnnotation.table()); + + addGroupMethod(); + addAndMethod(); + addOrMethod(); + + for (ParsedPersistentValue pv : persistentValues) { + processValue(pv); + } + +// for (ParsedReference ref : references) { + //todo: process references and support is, isNot, isNull and isNotNull +// } + } + + private void processValue(ParsedPersistentValue pv) { + String schemaFieldName = storeSchema(pv.getFieldName(), pv.getSchema()); + String tableFieldName = storeTable(pv.getFieldName(), pv.getTable()); + String columnFieldName = storeColumn(pv.getFieldName(), pv.getColumn()); + + ParsedForeignPersistentValue fpv = null; + if (pv instanceof ParsedForeignPersistentValue _fpv) { + fpv = _fpv; + storeLinks(fpv.getFieldName(), fpv.getLinks()); + } + + addIsMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsNotMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + + addIsInCollectionMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsInArrayMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsNotInCollectionMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsNotInArrayMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + + if (pv.isNullable()) { + addIsNullMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsNotNullMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + } + + if (typeUtils.isType(pv.getType(), String.class)) { + addIsLikeMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsNotLikeMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + } + + if (typeUtils.isNumericType(pv.getType()) || typeUtils.isType(pv.getType(), Timestamp.class)) { + addIsLessThanMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsLessThanOrEqualToMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsGreaterThanMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsGreaterThanOrEqualToMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsBetweenMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsNotBetweenMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + } + } + + private List clause(ParsedPersistentValue pv, JCTree.JCStatement... statements) { + java.util.List list = new ArrayList<>(); + + if (pv instanceof ParsedForeignPersistentValue fpv) { + String referencedSchemaFieldName = getStoredSchemaFieldName(fpv.getFieldName()); + String referencedTableFieldName = getStoredTableFieldName(fpv.getFieldName()); + String referencedColumnsFieldName = getStoredReferencedColumnsFieldName(fpv.getFieldName()); + String referringColumnsFieldName = getStoredReferringColumnsFieldName(fpv.getFieldName()); + list.add( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("addInnerJoin") + ), + List.of( + Ident(names.fromString(dataSchemaFieldName)), + Ident(names.fromString(dataTableFieldName)), + Ident(names.fromString(referringColumnsFieldName)), + Ident(names.fromString(referencedSchemaFieldName)), + Ident(names.fromString(referencedTableFieldName)), + Ident(names.fromString(referencedColumnsFieldName) + ) + ) + ) + ) + ); + } + + list.addAll(Arrays.asList(statements)); + + return List.from(list); + } + + private void addIsMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "Is"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + chainDots(pv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("equalsClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString(pv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsNotMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsNot"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + chainDots(pv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("equalsClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString(pv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsNullMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsNull"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("nullClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsNotNullMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsNotNull"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.nil(), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("notNullClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsInCollectionMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsIn"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + TypeApply( + chainDots("java", "util", "Collection"), + List.of( + chainDots(pv.getTypeFQNParts()) + ) + ), + null + ) + ), + List.nil(), + Block(0, clause(pv, + 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(columnFieldName)), + Apply( + List.nil(), + Select( + Ident(names.fromString(pv.getFieldName())), + names.fromString("toArray") + ), + List.nil() + ) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsInArrayMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsIn"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER | Flags.VARARGS), + names.fromString(pv.getFieldName()), + TypeArray( + chainDots(pv.getTypeFQNParts()) + ), + null + ) + ), + List.nil(), + Block(0, clause(pv, + 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(columnFieldName)), + Ident(names.fromString(pv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsNotInCollectionMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsNotIn"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + TypeApply( + chainDots("java", "util", "Collection"), + List.of( + chainDots(pv.getTypeFQNParts()) + ) + ), + null + ) + ), + List.nil(), + Block(0, clause(pv, + 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(columnFieldName)), + Apply( + List.nil(), + Select( + Ident(names.fromString(pv.getFieldName())), + names.fromString("toArray") + ), + List.nil() + ) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsNotInArrayMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsNotIn"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER | Flags.VARARGS), + names.fromString(pv.getFieldName()), + TypeArray( + chainDots(pv.getTypeFQNParts()) + ), + null + ) + ), + List.nil(), + Block(0, clause(pv, + 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(columnFieldName)), + Ident(names.fromString(pv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsLikeMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsLike"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("pattern"), + Ident(names.fromString("String")), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("likeClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString("pattern")) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsNotLikeMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsNotLike"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("pattern"), + Ident(names.fromString("String")), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("notLikeClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString("pattern")) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsLessThanMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsLessThan"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + chainDots(pv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("lessThanClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString(pv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsLessThanOrEqualToMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsLessThanOrEqualTo"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + chainDots(pv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("lessThanOrEqualToClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString(pv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsGreaterThanMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsGreaterThan"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + chainDots(pv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("greaterThanClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString(pv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsGreaterThanOrEqualToMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsGreaterThanOrEqualTo"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + chainDots(pv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("greaterThanOrEqualToClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString(pv.getFieldName())) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsBetweenMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsBetween"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("min"), + chainDots(pv.getTypeFQNParts()), + null + ), + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("max"), + chainDots(pv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("betweenClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString("min")), + Ident(names.fromString("max")) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addIsNotBetweenMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsNotBetween"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("min"), + chainDots(pv.getTypeFQNParts()), + null + ), + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("max"), + chainDots(pv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("notBetweenClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString("min")), + Ident(names.fromString("max")) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addGroupMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("group"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("function"), + TypeApply( + chainDots("java", "util", "function", "Function"), + List.of( + Ident(names.fromString(getBuilderClassName())), + Ident(names.fromString(getBuilderClassName())) + ) + ), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("pushGroup") + ), + List.nil() + ) + ), + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("function")), + names.fromString("apply") + ), + List.of( + Ident(names.fromString("this")) + ) + ) + ), + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("popGroup") + ), + List.nil() + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addAndMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("and"), + 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("andClause") + ), + List.nil() + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + private void addOrMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("or"), + 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("orClause") + ), + List.nil() + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + } +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java new file mode 100644 index 00000000..99697d6a --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.compiler.javac.javac; + +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.List; + +public record SuperClass(String fqn, List superParms, List args) { +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java new file mode 100644 index 00000000..3bea4b7e --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java @@ -0,0 +1,6 @@ +package net.staticstudios.data.compiler.javac.util; + +import javax.lang.model.element.Element; + +public record SimpleField(String name, Element element) { +} diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java new file mode 100644 index 00000000..c39dade8 --- /dev/null +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java @@ -0,0 +1,136 @@ +package net.staticstudios.data.compiler.javac.util; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class TypeUtils { + private final Elements elements; + private final Types types; + + public TypeUtils(ProcessingEnvironment processingEnv) { + this.elements = processingEnv.getElementUtils(); + this.types = processingEnv.getTypeUtils(); + } + + public boolean isNumericType(TypeElement typeElement) { + if (typeElement == null) { + return false; + } + + String fqn = typeElement.getQualifiedName().toString(); + return fqn.equals("java.lang.Byte") || + fqn.equals("java.lang.Short") || + fqn.equals("java.lang.Integer") || + fqn.equals("java.lang.Long") || + fqn.equals("java.lang.Float") || + fqn.equals("java.lang.Double") || + fqn.equals("java.math.BigInteger") || + fqn.equals("java.math.BigDecimal"); + } + + public boolean isType(TypeElement typeElement, Class clazz) { + if (typeElement == null || clazz == null) { + return false; + } + + String fqn = typeElement.getQualifiedName().toString(); + return fqn.equals(clazz.getCanonicalName()); + } + + public Collection getFields(TypeElement typeElement, String targetFQN) { + Collection fields = new ArrayList<>(); + discoverFields(typeElement, targetFQN, fields); + return fields; + } + + private void discoverFields(Element element, @NotNull String targetFQN, Collection fields) { + if (element == null) { + return; + } + + TypeElement targetTypeElement; + TypeMirror targetTypeMirror = null; + targetTypeElement = elements.getTypeElement(targetFQN); + if (targetTypeElement != null) { + targetTypeMirror = targetTypeElement.asType(); + } + Preconditions.checkNotNull(targetTypeMirror); + + for (Element enclosed : element.getEnclosedElements()) { + if (enclosed.getKind() == ElementKind.FIELD) { + TypeMirror fieldType = null; + try { + fieldType = enclosed.asType(); + } catch (UnsupportedOperationException ignored) { + } + + boolean matches = false; + + if (fieldType != null) { + try { + TypeMirror fieldErasure = types.erasure(fieldType); + TypeMirror targetErasure = types.erasure(targetTypeMirror); + + if (types.isSameType(fieldErasure, targetErasure)) { + matches = true; + } + } catch (Exception ignored) { + } + } + + if (matches) { + fields.add(new SimpleField(enclosed.getSimpleName().toString(), enclosed)); + } + } + } + + if (element instanceof TypeElement typeEl) { + TypeMirror superType = typeEl.getSuperclass(); + if (superType != null && superType.getKind() == TypeKind.DECLARED) { + Element superElem = types.asElement(superType); + discoverFields(superElem, targetFQN, fields); + } + } + } + + + public TypeMirror getGenericType(Element element, int index) { + if (element == null || index < 0) { + return null; + } + + TypeMirror mirror; + try { + mirror = element.asType(); + } catch (UnsupportedOperationException e) { + return null; + } + + if (mirror == null) { + return null; + } + + if (mirror.getKind() == TypeKind.DECLARED && mirror instanceof DeclaredType declared) { + List args = declared.getTypeArguments(); + if (args == null || index >= args.size()) { + return null; + } + return args.get(index); + } + + return null; + } +} diff --git a/javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin b/javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin deleted file mode 100644 index daff7920..00000000 --- a/javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin +++ /dev/null @@ -1 +0,0 @@ -net.staticstudios.data.compiler.javac.StaticDataJavacPlugin \ No newline at end of file diff --git a/javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor index b8722a66..f01241d9 100644 --- a/javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor +++ b/javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -1 +1 @@ -net.staticstudios.data.compiler.javac.DummyProcessor +net.staticstudios.data.compiler.javac.StaticDataProcessor diff --git a/settings.gradle b/settings.gradle index 7e26f8e3..75bf04a8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,4 +5,5 @@ include 'benchmark' include 'core' include 'javac-plugin' include 'intellij-plugin' -include 'utils' \ No newline at end of file +include 'utils' +include 'annotations' \ No newline at end of file From ab8dc9a77de4b0b3eae897da72367f059db7a2a9 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 5 Nov 2025 18:03:25 -0500 Subject: [PATCH 44/75] one to many values collections --- .../net/staticstudios/data/OneToMany.java | 46 ++ .../data/benchmark/StaticDataBenchmark.java | 2 +- .../net/staticstudios/data/DataManager.java | 10 +- .../PersistentOneToManyCollectionImpl.java | 2 +- ...ersistentOneToManyValueCollectionImpl.java | 508 ++++++++++++++++++ .../data/impl/h2/H2DataAccessor.java | 10 +- .../staticstudios/data/parse/SQLBuilder.java | 133 +++-- ...AutoIncrementingIntegerColumnMetadata.java | 33 ++ .../data/util/ColumnMetadata.java | 83 ++- ...stentOneToManyValueCollectionMetadata.java | 69 +++ .../PersistentOneToManyCollectionTest.java | 1 - ...ersistentOneToManyValueCollectionTest.java | 298 ++++++++++ .../data/mock/user/MockUser.java | 8 +- .../compiler/javac/StaticDataProcessor.java | 2 +- 14 files changed, 1150 insertions(+), 55 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyValueCollectionImpl.java create mode 100644 core/src/main/java/net/staticstudios/data/util/AutoIncrementingIntegerColumnMetadata.java create mode 100644 core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java create mode 100644 core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java diff --git a/annotations/src/main/java/net/staticstudios/data/OneToMany.java b/annotations/src/main/java/net/staticstudios/data/OneToMany.java index 3fc0e628..436bb961 100644 --- a/annotations/src/main/java/net/staticstudios/data/OneToMany.java +++ b/annotations/src/main/java/net/staticstudios/data/OneToMany.java @@ -16,4 +16,50 @@ * @return The link format */ String link(); + + /** + * This has no effect if the referenced data type extends UniqueData. + * + * @return The schema name of the foreign table + */ + String schema() default ""; + + /** + * This has no effect if the referenced data type extends UniqueData. + * + * @return The table name of the foreign table + */ + String table() default ""; + + + /** + * This has no effect if the referenced data type extends UniqueData. + * + * @return The column name where the data is stored + */ + String column() default "value"; + + /** + * This has no effect if the referenced data type extends UniqueData. + * Should the value column be indexed? + * + * @return Whether the data column is indexed + */ + boolean indexed() default false; + + /** + * This has no effect if the referenced data type extends UniqueData. + * Can the column be null? + * + * @return Whether the data column is nullable + */ + boolean nullable() default true; + + /** + * This has no effect if the referenced data type extends UniqueData. + * Should the value column be unique? + * + * @return Whether the data column is unique + */ + boolean unique() default false; } diff --git a/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java index bf3df05f..cc4cfacc 100644 --- a/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java +++ b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java @@ -34,7 +34,7 @@ public void testUniqueDataInsertAsync(StaticDataBenchmarkState state) { SkyblockPlayer player = SkyblockPlayer.builder() .id(UUID.randomUUID()) .name("Player" + i) - .insert(InsertMode.ASYNC); + .insert(InsertMode.ASYNC); //todo: this seems broken, the bench takes oddly long. } } diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index a78fd2f0..66a9ebc5 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -2,10 +2,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.MapMaker; -import net.staticstudios.data.impl.data.PersistentManyToManyCollectionImpl; -import net.staticstudios.data.impl.data.PersistentOneToManyCollectionImpl; -import net.staticstudios.data.impl.data.PersistentValueImpl; -import net.staticstudios.data.impl.data.ReferenceImpl; +import net.staticstudios.data.impl.data.*; import net.staticstudios.data.impl.h2.H2DataAccessor; import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.insert.InsertContext; @@ -205,8 +202,9 @@ public void extractMetadata(Class clazz) { String schema = ValueUtils.parseValue(dataAnnotation.schema()); String table = ValueUtils.parseValue(dataAnnotation.table()); Map persistentCollectionMetadataMap = new HashMap<>(); - persistentCollectionMetadataMap.putAll(PersistentOneToManyCollectionImpl.extractMetadata(clazz)); //todo: add other collection types + persistentCollectionMetadataMap.putAll(PersistentOneToManyCollectionImpl.extractMetadata(clazz)); persistentCollectionMetadataMap.putAll(PersistentManyToManyCollectionImpl.extractMetadata(clazz)); + persistentCollectionMetadataMap.putAll(PersistentOneToManyValueCollectionImpl.extractMetadata(clazz, schema)); UniqueDataMetadata metadata = new UniqueDataMetadata(clazz, schema, table, idColumns, PersistentValueImpl.extractMetadata(schema, table, clazz), ReferenceImpl.extractMetadata(clazz), persistentCollectionMetadataMap); uniqueDataMetadataMap.put(clazz, metadata); @@ -409,7 +407,7 @@ public T getInstance(Class clazz, ColumnValuePair... i ReferenceImpl.delegate(instance); PersistentOneToManyCollectionImpl.delegate(instance); PersistentManyToManyCollectionImpl.delegate(instance); - //todo: other collection types + PersistentOneToManyValueCollectionImpl.delegate(instance); uniqueDataInstanceCache.computeIfAbsent(clazz, k -> new MapMaker().weakValues().makeMap()) .put(idColumns, instance); 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 56476a3e..6a693b28 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 @@ -337,7 +337,7 @@ private void removeIds(List ids) { for (ColumnValuePair[] idColumns : ids) { transaction.update(updateStatement, () -> { List values = new ArrayList<>(); - for (Object holderLinkingValue : holderLinkingValues) { + for (Object holderLinkingValue : holderLinkingValues) { //set them to null values.add(null); } for (ColumnValuePair idColumn : idColumns) { 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 new file mode 100644 index 00000000..51efa86e --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyValueCollectionImpl.java @@ -0,0 +1,508 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.*; +import net.staticstudios.data.parse.SQLBuilder; +import net.staticstudios.data.util.*; +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; +import java.sql.SQLException; +import java.util.*; + +public class PersistentOneToManyValueCollectionImpl implements PersistentCollection { + private final UniqueData holder; + private final Class type; + private final String dataSchema; + private final String dataTable; + private final String dataColumn; + private final List link; + + public PersistentOneToManyValueCollectionImpl(UniqueData holder, Class type, String dataSchema, String dataTable, String dataColumn, List link) { + this.holder = holder; + this.type = type; + this.dataSchema = dataSchema; + this.dataTable = dataTable; + this.dataColumn = dataColumn; + this.link = link; + } + + public static void createAndDelegate(ProxyPersistentCollection proxy, String dataSchema, String dataTable, String dataColumn, List link) { + PersistentOneToManyValueCollectionImpl delegate = new PersistentOneToManyValueCollectionImpl<>( + proxy.getHolder(), + proxy.getReferenceType(), + dataSchema, + dataTable, + dataColumn, + link + ); + proxy.setDelegate(delegate); + } + + public static PersistentOneToManyValueCollectionImpl create(UniqueData holder, Class type, String dataSchema, String dataTable, String dataColumn, List link) { + return new PersistentOneToManyValueCollectionImpl<>(holder, type, dataSchema, dataTable, dataColumn, link); + } + + 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() + ); + } else { + pair.field().setAccessible(true); + try { + pair.field().set(instance, create(instance, + oneToManyValueMetadata.getDataType(), + oneToManyValueMetadata.getDataSchema(), + oneToManyValueMetadata.getDataTable(), + oneToManyValueMetadata.getDataColumn(), + oneToManyValueMetadata.getLinks() + )); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + public static Map extractMetadata(Class clazz, String holderSchema) { + Map metadataMap = new HashMap<>(); + for (Field field : ReflectionUtils.getFields(clazz, PersistentCollection.class)) { + OneToMany oneToManyAnnotation = field.getAnnotation(OneToMany.class); + if (oneToManyAnnotation == null) continue; + Class genericType = ReflectionUtils.getGenericType(field); + if (genericType == null || UniqueData.class.isAssignableFrom(genericType)) continue; + String schema = oneToManyAnnotation.schema(); + if (schema.isEmpty()) { + schema = holderSchema; + } + schema = ValueUtils.parseValue(schema); + String table = ValueUtils.parseValue(oneToManyAnnotation.table()); + String column = ValueUtils.parseValue(oneToManyAnnotation.column()); + Preconditions.checkArgument(!schema.isEmpty(), "OneToMany PersistentCollection field %s in class %s must specify a schema name since the data type %s does not extend UniqueData", field.getName(), clazz.getName(), genericType.getName()); + Preconditions.checkArgument(!table.isEmpty(), "OneToMany PersistentCollection field %s in class %s must specify a table name since the data type %s does not extend UniqueData", field.getName(), clazz.getName(), genericType.getName()); + metadataMap.put(field, new PersistentOneToManyValueCollectionMetadata(genericType, schema, table, column, SQLBuilder.parseLinks(oneToManyAnnotation.link()))); + } + + return metadataMap; + } + + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public int size() { + return getValues().size(); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean contains(Object o) { + return containsAll(Collections.singleton(o)); + } + + @Override + public @NotNull Iterator iterator() { + return new IteratorImpl(getValues()); + } + + @Override + public @NotNull Object @NotNull [] toArray() { + return getValues().toArray(); + } + + @Override + public @NotNull T1 @NotNull [] toArray(@NotNull T1 @NotNull [] a) { + return getValues().toArray(a); + } + + @Override + public boolean add(T t) { + return addAll(Collections.singleton(t)); + } + + @Override + public boolean remove(Object o) { + return removeAll(Collections.singleton(o)); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + for (Object o : c) { + if (!type.isInstance(o)) { + return false; + } + } + return getValues().containsAll(c); + } + + @Override + public boolean addAll(@NotNull Collection c) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); + if (c.isEmpty()) { + return false; + } + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + + SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); + SQLTransaction.Statement insertStatement = buildInsertStatement(); + + + List holderLinkingValues = new ArrayList<>(link.size()); + List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (Link entry : link) { + String dataColumn = entry.columnInReferringTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + for (T entry : c) { + transaction.update(insertStatement, () -> { + List values = new ArrayList<>(holderLinkingValues); + values.add(entry); + return values; + }); + } + try { + dataAccessor.executeTransaction(transaction, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return true; + } + + @Override + public boolean removeAll(@NotNull Collection c) { + List toRemove = new ArrayList<>(); + for (Object o : c) { + if (!type.isInstance(o)) { + continue; + } + T data = type.cast(o); + toRemove.add(data); + } + removeValues(toRemove); + + return !toRemove.isEmpty(); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + Set currentValues = getValues(); + Set valuesToRetain = new HashSet<>(); + for (Object o : c) { + if (!type.isInstance(o)) { + continue; + } + T data = type.cast(o); + valuesToRetain.add(data); + } + + List valuesToRemove = new ArrayList<>(); + for (T value : currentValues) { + boolean found = false; + for (T retainValue : valuesToRetain) { + if (Objects.equals(value, retainValue)) { + found = true; + break; + } + } + if (!found) { + valuesToRemove.add(value); + } + } + + if (!valuesToRemove.isEmpty()) { + removeValues(valuesToRemove); + return true; + } + + return false; + } + + @Override + public void clear() { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot clear entries on a deleted UniqueData instance"); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + + SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); + SQLTransaction.Statement clearStatement = buildClearStatement(); + + + List holderLinkingValues = new ArrayList<>(link.size()); + List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (Link entry : link) { + String dataColumn = entry.columnInReferringTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + transaction.update(clearStatement, () -> holderLinkingValues); + try { + dataAccessor.executeTransaction(transaction, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private void removeValues(Collection values) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); + if (values.isEmpty()) { + return; + } + + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + + SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); + SQLTransaction.Statement removeStatement = buildRemoveStatement(); + + + List holderLinkingValues = new ArrayList<>(link.size()); + List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (Link entry : link) { + String dataColumn = entry.columnInReferringTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + for (T value : values) { + transaction.update(removeStatement, () -> { + List statementValues = new ArrayList<>(holderLinkingValues); + statementValues.add(value); + return statementValues; + }); + } + try { + dataAccessor.executeTransaction(transaction, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private SQLTransaction.Statement buildSelectDataIdsStatement() { + UniqueDataMetadata holderMetadata = holder.getMetadata(); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (Link entry : link) { + String dataColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(dataColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); + } + + private SQLTransaction.Statement buildInsertStatement() { + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("INSERT INTO \"").append(dataSchema).append("\".\"").append(dataTable).append("\" ("); + for (Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\", "); + } + sqlBuilder.append("\"").append(dataColumn).append("\""); + sqlBuilder.append(") VALUES ("); + sqlBuilder.append("?, ".repeat(link.size() + 1)); + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(")"); + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); + } + + private SQLTransaction.Statement buildRemoveStatement() { + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("DELETE FROM \"").append(dataSchema).append("\".\"").append(dataTable).append("\" WHERE "); + for (Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = ? AND "); + } + sqlBuilder.append("\"").append(dataColumn).append("\" = ? LIMIT 1"); + @Language("SQL") String h2 = sqlBuilder.toString(); + + sqlBuilder = new StringBuilder(); + sqlBuilder.append("WITH to_delete AS (") + .append("SELECT ctid FROM \"").append(dataSchema).append("\".\"").append(dataTable).append("\" WHERE "); + + for (Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = ? AND "); + } + + sqlBuilder.append("\"").append(dataColumn).append("\" = ? ") + .append("LIMIT 1") + .append(") DELETE FROM \"").append(dataSchema).append("\".\"").append(dataTable) + .append("\" WHERE ctid IN (SELECT ctid FROM to_delete)"); + + @Language("SQL") + String pg = sqlBuilder.toString(); + + return SQLTransaction.Statement.of(h2, pg); + } + + private SQLTransaction.Statement buildClearStatement() { + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("DELETE FROM \"").append(dataSchema).append("\".\"").append(dataTable).append("\" WHERE "); + for (Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); + } + + private Set getValues() { + // 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 values = new HashSet<>(); + UniqueDataMetadata holderMetadata = holder.getMetadata(); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT ").append("\"").append(dataColumn).append("\", "); + for (ColumnMetadata columnMetadata : holderMetadata.idColumns()) { + sqlBuilder.append("_source.\"").append(columnMetadata.name()).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(dataSchema).append("\".\"").append(dataTable).append("\" "); + sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" AS _source ON "); + for (Link entry : link) { + String myColumn = entry.columnInReferringTable(); + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(dataSchema).append("\".\"").append(dataTable).append("\".\"").append(theirColumn).append("\" = _source.\"").append(myColumn).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + sqlBuilder.append(" WHERE "); + + for (Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = _source.\"").append(entry.columnInReferringTable()).append("\" AND "); + } + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("_source.\"").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())) { + while (rs.next()) { + Object value = rs.getObject(dataColumn); + values.add(type.cast(value)); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return values; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof PersistentOneToManyValueCollectionImpl that)) return false; + //due to the nature of how one-to-many collections work, this will == that only if the holder is the same. no need to compare contents. + return Objects.equals(type, that.type) && Objects.equals(holder, that.holder); + } + + @Override + public int hashCode() { + //due to the nature of how one-to-many collections work, this will == that only if the holder is the same. no need to compare contents. + //for this reason, we can use just the type and holder for the hashcode. + return Objects.hash(type, holder); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (T item : this) { + sb.append(item).append(", "); + } + if (!isEmpty()) { + sb.setLength(sb.length() - 2); + } + sb.append("]"); + return sb.toString(); + } + + class IteratorImpl implements Iterator { + private final List values; + private int index = 0; + + public IteratorImpl(Set valuesSet) { + this.values = new ArrayList<>(valuesSet); + } + + @Override + public boolean hasNext() { + return index < values.size(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return values.get(index++); + } + + @Override + public void remove() { + Preconditions.checkState(index > 0, "next() has not been called yet"); + removeValues(Collections.singletonList(values.get(index - 1))); + values.remove(--index); + } + } +} 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 0144dae2..1a96b67a 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 @@ -432,12 +432,20 @@ public void runDDL(DDLStatement ddl) { if (!ddl.postgresqlStatement().isEmpty()) { logger.debug("[DB] {}", ddl.postgresqlStatement()); - connection.createStatement().execute(ddl.postgresqlStatement()); + try { + connection.createStatement().execute(ddl.postgresqlStatement()); + } catch (Exception e) { + logger.error("Error executing DDL on real database: {}", ddl.postgresqlStatement(), e); + throw e; + } } if (ddl.h2Statement().isEmpty()) return; try (Statement statement = getConnection().createStatement()) { logger.trace("[H2] {}", ddl.h2Statement()); statement.execute(ddl.h2Statement()); + } catch (SQLException e) { + logger.error("Error executing DDL on H2 database: {}", ddl.h2Statement(), e); + throw e; } }).join(); 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 55b724fd..1b4cd3e4 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -112,20 +112,30 @@ private List getDefs(Collection schemas) { pgSb = new StringBuilder(); h2Sb.append("CREATE TABLE IF NOT EXISTS \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" (\n"); pgSb.append("CREATE TABLE IF NOT EXISTS \"").append(schema.getName()).append("\".\"").append(table.getName()).append("\" (\n"); + boolean skipPKDef = false; for (ColumnMetadata idColumn : table.getIdColumns()) { - h2Sb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append(SQLUtils.getH2SqlType(idColumn.type())).append(",\n"); - pgSb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append(SQLUtils.getPgSqlType(idColumn.type())).append(" NOT NULL,\n"); + if (idColumn instanceof AutoIncrementingIntegerColumnMetadata) { + Preconditions.checkArgument(table.getIdColumns().size() == 1, "Auto-incrementing ID column can only be used as the sole ID column in referringTable " + table.getName()); + h2Sb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append("BIGINT AUTO_INCREMENT PRIMARY KEY\n"); + pgSb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append("BIGSERIAL PRIMARY KEY\n"); + skipPKDef = true; + } else { + h2Sb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append(SQLUtils.getH2SqlType(idColumn.type())).append(",\n"); + pgSb.append(INDENT).append("\"").append(idColumn.name()).append("\" ").append(SQLUtils.getPgSqlType(idColumn.type())).append(" NOT NULL,\n"); + } } - h2Sb.append(INDENT).append("PRIMARY KEY ("); - pgSb.append(INDENT).append("PRIMARY KEY ("); - for (ColumnMetadata idColumn : table.getIdColumns()) { - h2Sb.append("\"").append(idColumn.name()).append("\", "); - pgSb.append("\"").append(idColumn.name()).append("\", "); + if (!skipPKDef) { + h2Sb.append(INDENT).append("PRIMARY KEY ("); + pgSb.append(INDENT).append("PRIMARY KEY ("); + for (ColumnMetadata idColumn : table.getIdColumns()) { + h2Sb.append("\"").append(idColumn.name()).append("\", "); + pgSb.append("\"").append(idColumn.name()).append("\", "); + } + h2Sb.setLength(h2Sb.length() - 2); + pgSb.setLength(pgSb.length() - 2); + h2Sb.append(")\n"); + pgSb.append(")\n"); } - h2Sb.setLength(h2Sb.length() - 2); - pgSb.setLength(pgSb.length() - 2); - h2Sb.append(")\n"); - pgSb.append(")\n"); h2Sb.append(");"); pgSb.append(");"); statements.add(DDLStatement.of(h2Sb.toString(), pgSb.toString())); @@ -472,7 +482,7 @@ private void parseReference(Class clazz, Map clazz, Map genericType, Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { - //todo: this + String dataSchema = ValueUtils.parseValue(dataAnnotation.schema()); + String dataTable = ValueUtils.parseValue(dataAnnotation.table()); + + String referencedSchemaName = oneToMany.schema(); + if (referencedSchemaName.isEmpty()) { + referencedSchemaName = dataSchema; + } + referencedSchemaName = ValueUtils.parseValue(referencedSchemaName); + String referencedTableName = ValueUtils.parseValue(oneToMany.table()); + String referencedColumnName = ValueUtils.parseValue(oneToMany.column()); + + Preconditions.checkArgument(!referencedTableName.isEmpty(), "OneToMany PersistentCollection field " + field.getName() + " in class " + clazz.getName() + " must specify a table name in the @OneToMany annotation when the generic type is not a UniqueData type."); + Preconditions.checkArgument(!referencedColumnName.isEmpty(), "OneToMany PersistentCollection field " + field.getName() + " in class " + clazz.getName() + " must specify a column name in the @OneToMany annotation when the generic type is not a UniqueData type."); + + SQLSchema schema = Objects.requireNonNull(schemas.get(dataSchema)); + SQLTable table = Objects.requireNonNull(schema.getTable(dataTable)); + + SQLSchema referencedSchema = schemas.computeIfAbsent(referencedSchemaName, SQLSchema::new); + SQLTable referencedTable = referencedSchema.getTable(referencedTableName); + if (referencedTable == null) { + List idColumns = List.of(new AutoIncrementingIntegerColumnMetadata(referencedSchemaName, referencedTableName, referencedTableName + "_id")); + referencedTable = new SQLTable(referencedSchema, referencedTableName, idColumns); + referencedSchema.addTable(referencedTable); + referencedTable.addColumn(new SQLColumn(referencedTable, genericType, referencedColumnName, oneToMany.nullable(), oneToMany.indexed(), oneToMany.unique(), null)); + for (Link link : parseLinks(oneToMany.link())) { + Class columnType = null; + SQLColumn columnInReferringTable = table.getColumn(link.columnInReferringTable()); + if (columnInReferringTable != null) { + 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, columnType, link.columnInReferencedTable(), false, false, false, null); + referencedTable.addColumn(linkingColumn); + } + + } //if non-null don't attempt to create/structure it, assume the user knows what they're doing. + + Delete delete = field.getAnnotation(Delete.class); + DeleteStrategy deleteStrategy = delete != null ? delete.value() : DeleteStrategy.NO_ACTION; + OnDelete onDelete = deleteStrategy == DeleteStrategy.CASCADE ? OnDelete.CASCADE : OnDelete.SET_NULL; + + // unlike a Reference, this foreign key goes on the referenced referringTable, not our referringTable. + // Since the foreign key is on the other referringTable, let the foreign key handle the deletion strategy instead of a trigger. + ForeignKey foreignKey = new ForeignKey(referencedSchemaName, referencedTableName, schema.getName(), table.getName(), onDelete); + try { + parseLinksReversed(foreignKey, oneToMany.link()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error parsing @OneToMany link on field " + field.getName() + " in class " + clazz.getName() + ": " + e.getMessage(), e); + } + referencedTable.addForeignKey(foreignKey); } private void parseOneToManyPersistentCollection(OneToMany oneToMany, Class genericType, Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { @@ -517,13 +576,8 @@ private void parseOneToManyPersistentCollection(OneToMany oneToMany, Class joinTableIdColumns = new ArrayList<>(); for (Link dataLink : joinTableToDataTableLinks) { - ColumnMetadata columnMetadata = metadata.idColumns().stream() - .filter(c -> c.name().equals(dataLink.columnInReferencedTable())) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Column not found in data referringTable! " + dataLink.columnInReferringTable())); - joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, dataTableColumnPrefix + "_" + columnMetadata.name(), columnMetadata.type(), false, false, "")); + SQLColumn foundColumn = null; + for (SQLColumn column : table.getColumns()) { + if (column.getName().equals(dataLink.columnInReferencedTable())) { + foundColumn = column; + break; + } + } + Preconditions.checkNotNull(foundColumn, "Column not found in data referringTable! " + dataLink.columnInReferringTable()); + joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, dataTableColumnPrefix + "_" + foundColumn.getName(), foundColumn.getType(), false, false, "")); } for (Link referencedLink : joinTableToReferencedTableLinks) { - ColumnMetadata columnMetadata = referencedMetadata.idColumns().stream() - .filter(c -> c.name().equals(referencedLink.columnInReferencedTable())) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Column not found in referenced referringTable! " + referencedLink.columnInReferringTable())); - joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, referencedTableColumnPrefix + "_" + columnMetadata.name(), columnMetadata.type(), false, false, "")); + SQLColumn foundColumn = null; + for (SQLColumn column : referencedTable.getColumns()) { + if (column.getName().equals(referencedLink.columnInReferencedTable())) { + foundColumn = column; + break; + } + } + Preconditions.checkNotNull(foundColumn, "Column not found in referenced referringTable! " + referencedLink.columnInReferringTable()); + joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, referencedTableColumnPrefix + "_" + foundColumn.getName(), foundColumn.getType(), false, false, "")); } joinTable = new SQLTable(joinSchema, joinTableName, joinTableIdColumns); joinSchema.addTable(joinTable); diff --git a/core/src/main/java/net/staticstudios/data/util/AutoIncrementingIntegerColumnMetadata.java b/core/src/main/java/net/staticstudios/data/util/AutoIncrementingIntegerColumnMetadata.java new file mode 100644 index 00000000..eb4fe575 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/AutoIncrementingIntegerColumnMetadata.java @@ -0,0 +1,33 @@ +package net.staticstudios.data.util; + +import java.util.Objects; + +public final class AutoIncrementingIntegerColumnMetadata extends ColumnMetadata { + + public AutoIncrementingIntegerColumnMetadata(String schema, String table, String name) { + super(schema, table, name, Integer.class, false, false, ""); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (AutoIncrementingIntegerColumnMetadata) obj; + return super.equals(that); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode()); + } + + @Override + public String toString() { + return "AutoIncrementingIntegerColumnMetadata[" + + "schema=" + schema() + ", " + + "table=" + table() + ", " + + "name=" + name() + + "]"; + } + +} diff --git a/core/src/main/java/net/staticstudios/data/util/ColumnMetadata.java b/core/src/main/java/net/staticstudios/data/util/ColumnMetadata.java index 15f11dd0..c4ef4f23 100644 --- a/core/src/main/java/net/staticstudios/data/util/ColumnMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/ColumnMetadata.java @@ -2,6 +2,85 @@ import org.jetbrains.annotations.NotNull; -public record ColumnMetadata(String schema, String table, String name, Class type, boolean nullable, boolean indexed, - @NotNull String encodedDefaultValue) { +import java.util.Objects; + +public class ColumnMetadata { + private final String schema; + private final String table; + private final String name; + private final Class type; + private final boolean nullable; + private final boolean indexed; + private final @NotNull String encodedDefaultValue; + + public ColumnMetadata(String schema, String table, String name, Class type, boolean nullable, boolean indexed, + @NotNull String encodedDefaultValue) { + this.schema = schema; + this.table = table; + this.name = name; + this.type = type; + this.nullable = nullable; + this.indexed = indexed; + this.encodedDefaultValue = encodedDefaultValue; + } + + public String schema() { + return schema; + } + + public String table() { + return table; + } + + public String name() { + return name; + } + + public Class type() { + return type; + } + + public boolean nullable() { + return nullable; + } + + public boolean indexed() { + return indexed; + } + + public @NotNull String encodedDefaultValue() { + return encodedDefaultValue; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (ColumnMetadata) obj; + return Objects.equals(this.schema, that.schema) && + Objects.equals(this.table, that.table) && + Objects.equals(this.name, that.name) && + Objects.equals(this.type, that.type) && + this.nullable == that.nullable && + this.indexed == that.indexed && + Objects.equals(this.encodedDefaultValue, that.encodedDefaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(schema, table, name, type, nullable, indexed, encodedDefaultValue); + } + + @Override + public String toString() { + return "ColumnMetadata[" + + "schema=" + schema + ", " + + "table=" + table + ", " + + "name=" + name + ", " + + "type=" + type + ", " + + "nullable=" + nullable + ", " + + "indexed=" + indexed + ", " + + "encodedDefaultValue=" + encodedDefaultValue + ']'; + } + } diff --git a/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java new file mode 100644 index 00000000..c746ad12 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java @@ -0,0 +1,69 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.utils.Link; + +import java.util.List; +import java.util.Objects; + +public class PersistentOneToManyValueCollectionMetadata implements PersistentCollectionMetadata { + private final Class dataType; + private final String dataSchema; + private final String dataTable; + private final String dataColumn; + private final List links; + + public PersistentOneToManyValueCollectionMetadata(Class dataType, String dataSchema, String dataTable, String dataColumn, List links) { + this.dataType = dataType; + this.dataSchema = dataSchema; + this.dataTable = dataTable; + this.dataColumn = dataColumn; + this.links = links; + } + + public Class getDataType() { + return dataType; + } + + public String getDataSchema() { + return dataSchema; + } + + public String getDataTable() { + return dataTable; + } + + public String getDataColumn() { + return dataColumn; + } + + public List getLinks() { + return links; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PersistentOneToManyValueCollectionMetadata that = (PersistentOneToManyValueCollectionMetadata) o; + return Objects.equals(dataType, that.dataType) && + Objects.equals(dataSchema, that.dataSchema) && + Objects.equals(dataTable, that.dataTable) && + Objects.equals(dataColumn, that.dataColumn) && + Objects.equals(links, that.links); + } + + @Override + public int hashCode() { + return Objects.hash(dataType, dataSchema, dataTable, dataColumn, links); + } + + @Override + public String toString() { + return "PersistentOneToManyValueCollectionMetadata[" + + "dataType=" + dataType + + ", dataSchema='" + dataSchema + '\'' + + ", dataTable='" + dataTable + '\'' + + ", dataColumn='" + dataColumn + '\'' + + ", links=" + links + + ']'; + } +} diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java index e87b9b28..51c7f461 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -150,7 +150,6 @@ public void testRemove() { } } - @SuppressWarnings("SuspiciousMethodCalls") @Test public void testRemoveAll() { List sessions = createSessions(SESSION_COUNT); diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java new file mode 100644 index 00000000..c41d1254 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java @@ -0,0 +1,298 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.misc.TestUtils; +import net.staticstudios.data.mock.user.MockUser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class PersistentOneToManyValueCollectionTest extends DataTest { + private static final int NUMBER_COUNT = 20; + + private MockUser mockUser; + private DataManager dataManager; + + @BeforeEach + public void setUp() { + dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + mockUser = MockUser.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); + } + + private List createNumbers(int count) { + return createNumbersStartingAt(0, count); + } + + private List createNumbersStartingAt(int start, int count) { + List numbers = new ArrayList<>(); + for (int i = 0; i < count; i++) { + numbers.add(start + i); + } + return numbers; + } + + @Test + public void testAdd() { + List numbers = createNumbers(NUMBER_COUNT); + + int size = mockUser.favoriteNumbers.size(); + for (Integer number : numbers) { + assertTrue(mockUser.favoriteNumbers.add(number)); + assertEquals(++size, mockUser.favoriteNumbers.size()); + } + + for (Integer number : numbers) { + assertTrue(mockUser.favoriteNumbers.contains(number)); + } + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"favorite_numbers\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(numbers.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testAddAll() { + List numbers = createNumbers(NUMBER_COUNT); + + assertTrue(mockUser.favoriteNumbers.addAll(numbers)); + assertEquals(NUMBER_COUNT, mockUser.favoriteNumbers.size()); + + for (Integer number : numbers) { + assertTrue(mockUser.favoriteNumbers.contains(number)); + } + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"favorite_numbers\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(numbers.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testRemove() { + List numbers = createNumbers(NUMBER_COUNT); + mockUser.favoriteNumbers.addAll(numbers); + + for (Integer number : numbers) { + assertTrue(mockUser.favoriteNumbers.contains(number)); + } + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"favorite_numbers\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(numbers.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + int size = mockUser.favoriteNumbers.size(); + for (Integer number : numbers) { + assertTrue(mockUser.favoriteNumbers.remove(number)); + assertEquals(--size, mockUser.favoriteNumbers.size()); + } + + for (Integer number : numbers) { + assertFalse(mockUser.favoriteNumbers.contains(number)); + } + + assertTrue(mockUser.favoriteNumbers.isEmpty()); + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"favorite_numbers\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(0, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testRemoveAll() { + List numbers = createNumbers(NUMBER_COUNT); + mockUser.favoriteNumbers.addAll(numbers); + + for (Integer number : numbers) { + assertTrue(mockUser.favoriteNumbers.contains(number)); + } + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"favorite_numbers\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(numbers.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + assertTrue(mockUser.favoriteNumbers.removeAll(numbers)); + assertEquals(0, mockUser.favoriteNumbers.size()); + + for (Integer number : numbers) { + assertFalse(mockUser.favoriteNumbers.contains(number)); + } + + assertTrue(mockUser.favoriteNumbers.isEmpty()); + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"favorite_numbers\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(0, TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testContains() { + List numbers = createNumbersStartingAt(0, NUMBER_COUNT); + for (Integer number : numbers) { + assertFalse(mockUser.favoriteNumbers.contains(number)); + mockUser.favoriteNumbers.add(number); + assertTrue(mockUser.favoriteNumbers.contains(number)); + } + + Integer notAddedNumber = createNumbersStartingAt(NUMBER_COUNT, 1).get(0); + assertFalse(mockUser.favoriteNumbers.contains(notAddedNumber)); + + for (Integer number : numbers) { + mockUser.favoriteNumbers.remove(number); + assertFalse(mockUser.favoriteNumbers.contains(number)); + } + } + + @SuppressWarnings("SuspiciousMethodCalls") + @Test + public void testContainsAll() { + assertTrue(mockUser.favoriteNumbers.containsAll(new ArrayList<>())); + assertFalse(mockUser.favoriteNumbers.containsAll(List.of("not a number", "another non number"))); + List numbers = createNumbersStartingAt(0, NUMBER_COUNT); + mockUser.favoriteNumbers.addAll(numbers); + assertTrue(mockUser.favoriteNumbers.containsAll(numbers)); + List nonAddedNumbers = createNumbersStartingAt(NUMBER_COUNT, 3); + List mixedNumbers = new ArrayList<>(numbers.subList(0, NUMBER_COUNT - 2)); + mixedNumbers.addAll(nonAddedNumbers); + assertFalse(mockUser.favoriteNumbers.containsAll(mixedNumbers)); + } + + @Test + public void testClear() { + List numbers = createNumbers(NUMBER_COUNT); + mockUser.favoriteNumbers.addAll(numbers); + assertEquals(NUMBER_COUNT, mockUser.favoriteNumbers.size()); + mockUser.favoriteNumbers.clear(); + assertTrue(mockUser.favoriteNumbers.isEmpty()); + } + + @Test + public void testRetainAll() { + List numbers = createNumbersStartingAt(0, NUMBER_COUNT); + mockUser.favoriteNumbers.addAll(numbers); + + List toRetain = new ArrayList<>(numbers.subList(0, NUMBER_COUNT / 2)); + int nonAddedCount = 3; + List junk = createNumbersStartingAt(NUMBER_COUNT, nonAddedCount); + toRetain.addAll(junk); + assertTrue(mockUser.favoriteNumbers.retainAll(toRetain)); + assertEquals(toRetain.size() - nonAddedCount, mockUser.favoriteNumbers.size()); + for (int i = 0; i < toRetain.size() - nonAddedCount; i++) { + assertTrue(mockUser.favoriteNumbers.contains(numbers.get(i))); + } + } + + @Test + public void testToArray() { + List numbers = createNumbers(NUMBER_COUNT); + mockUser.favoriteNumbers.addAll(numbers); + Object[] arr = mockUser.favoriteNumbers.toArray(); + assertEquals(NUMBER_COUNT, arr.length); + for (Object o : arr) { + assertTrue(numbers.contains(o)); + } + + Integer[] typedArr = mockUser.favoriteNumbers.toArray(new Integer[0]); + assertEquals(NUMBER_COUNT, typedArr.length); + for (Integer n : typedArr) { + assertTrue(numbers.contains(n)); + } + } + + @Test + public void testIterator() { + List numbers = createNumbers(NUMBER_COUNT); + mockUser.favoriteNumbers.addAll(numbers); + java.util.Iterator iterator = mockUser.favoriteNumbers.iterator(); + int count = 0; + while (iterator.hasNext()) { + assertTrue(numbers.contains(iterator.next())); + count++; + } + + iterator = mockUser.favoriteNumbers.iterator(); + count = 0; + while (iterator.hasNext()) { + Integer number = iterator.next(); + assertTrue(numbers.contains(number)); + iterator.remove(); + assertFalse(mockUser.favoriteNumbers.contains(number)); + count++; + } + + assertEquals(NUMBER_COUNT, count); + assertTrue(mockUser.favoriteNumbers.isEmpty()); + } + + @Test + public void testEqualsAndHashCode() { + MockUser anotherMockUser = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("another user") + .insert(InsertMode.SYNC); + assertTrue(mockUser.favoriteNumbers.equals(mockUser.favoriteNumbers)); + assertFalse(mockUser.favoriteNumbers.equals(anotherMockUser.favoriteNumbers)); + + } +} \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index ec9d1e23..d5f03251 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -10,7 +10,6 @@ public class MockUser extends UniqueData { //todo: test inheritance properly //todo: cached values - //todo: note - maybe PC's add and remove handlers can be implemented using update handlers @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); @@ -46,16 +45,17 @@ public class MockUser extends UniqueData { @Column(name = "views", nullable = true) public PersistentValue views; - //todo: on delete we need to have an option to set null. No action will handle this actually. @Delete(DeleteStrategy.NO_ACTION) @OneToMany(link = "id=user_id") public PersistentCollection sessions; - @Delete(DeleteStrategy.CASCADE) //todo: impl delete strategy for collections + @Delete(DeleteStrategy.CASCADE) //todo: impl delete strategy for many to many collections @ManyToMany(link = "id=id", joinTable = "user_friends") public PersistentCollection friends; - //todo: support OneToMany Collections where the data type is not a uniquedata. in this case additional info about what referringTable and referringSchema to use will be required, since we will have to create this referringTable. + @Delete(DeleteStrategy.CASCADE) + @OneToMany(link = "id=user_id", table = "favorite_numbers", column = "number") + public PersistentCollection favoriteNumbers; public int getNameUpdates() { return nameUpdates.get(); diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java index 405d09be..bd196d01 100644 --- a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java +++ b/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java @@ -143,7 +143,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) { @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { Set annotated = roundEnv.getElementsAnnotatedWith(Data.class); - annotated.forEach(e -> { + annotated.forEach(e -> { //todo: if abstract, skip TypeElement typeElement = (TypeElement) e; Tree tree = trees.getTree(e); TypeUtils typeUtils = new TypeUtils(processingEnvironment); From 040f6d96a15a9c60fd0687bd42890aefb5d9c4f9 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Thu, 6 Nov 2025 16:20:58 -0500 Subject: [PATCH 45/75] fix bug when updating a foreign pv --- .../net/staticstudios/data/DataAccessor.java | 4 +- .../net/staticstudios/data/DataManager.java | 21 ++- .../net/staticstudios/data/UniqueData.java | 10 +- .../data/impl/data/ReferenceImpl.java | 9 +- .../data/PersistentValueTest.java | 132 ++++++++++++++---- 5 files changed, 137 insertions(+), 39 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/DataAccessor.java b/core/src/main/java/net/staticstudios/data/DataAccessor.java index 7113917f..39c61194 100644 --- a/core/src/main/java/net/staticstudios/data/DataAccessor.java +++ b/core/src/main/java/net/staticstudios/data/DataAccessor.java @@ -12,8 +12,8 @@ public interface DataAccessor { ResultSet executeQuery(@Language("SQL") String sql, List values) throws SQLException; - default void executeUpdate(@Language("SQL") String sql, List values, int delay) throws SQLException { - executeTransaction(new SQLTransaction().update(SQLTransaction.Statement.of(sql, sql), values), delay); + default void executeUpdate(SQLTransaction.Statement statement, List values, int delay) throws SQLException { + executeTransaction(new SQLTransaction().update(statement, values), delay); } void executeTransaction(SQLTransaction transaction, int delay) throws SQLException; diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 66a9ebc5..9f502c6a 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -680,6 +680,7 @@ public void set(String schema, String table, String column, ColumnValuePairs idC } sqlBuilder.setLength(sqlBuilder.length() - 5); } else { // we're dealing with a foreign key + //todo: use update on conclifct bla bla bla for pg sqlBuilder = new StringBuilder().append("MERGE INTO \"").append(schema).append("\".\"").append(table).append("\" target USING (VALUES (?"); sqlBuilder.append(", ?".repeat(idColumns.getPairs().length)); sqlBuilder.append(")) AS source (\"").append(column).append("\""); @@ -729,14 +730,30 @@ public void set(String schema, String table, String column, ColumnValuePairs idC } sqlBuilder.append(")"); } - @Language("SQL") String sql = sqlBuilder.toString(); + @Language("SQL") String h2Sql = sqlBuilder.toString(); + + sqlBuilder.setLength(0); + sqlBuilder.append("UPDATE \"").append(schema).append("\".\"").append(table).append("\" SET \"").append(column).append("\" = ? WHERE "); + for (ColumnValuePair columnValuePair : idColumns) { + String name = columnValuePair.column(); + for (Link link : idColumnLinks) { + if (link.columnInReferringTable().equals(columnValuePair.column())) { + name = link.columnInReferencedTable(); + break; + } + } + sqlBuilder.append("\"").append(name).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + @Language("SQL") String pgSql = sqlBuilder.toString(); + List values = new ArrayList<>(1 + idColumns.getPairs().length); values.add(serialize(value)); for (ColumnValuePair columnValuePair : idColumns) { values.add(columnValuePair.value()); } try { - dataAccessor.executeUpdate(sql, values, delay); + dataAccessor.executeUpdate(SQLTransaction.Statement.of(h2Sql, pgSql), values, delay); } catch (SQLException e) { throw new RuntimeException(e); } diff --git a/core/src/main/java/net/staticstudios/data/UniqueData.java b/core/src/main/java/net/staticstudios/data/UniqueData.java index edb86bdb..9bb8b357 100644 --- a/core/src/main/java/net/staticstudios/data/UniqueData.java +++ b/core/src/main/java/net/staticstudios/data/UniqueData.java @@ -4,6 +4,7 @@ import net.staticstudios.data.util.ColumnValuePair; import net.staticstudios.data.util.ColumnValuePairs; import net.staticstudios.data.util.UniqueDataMetadata; +import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; import java.sql.SQLException; @@ -49,16 +50,17 @@ public synchronized void delete() { Preconditions.checkState(!isDeleted, "This object has already been deleted!"); UniqueDataMetadata metadata = getMetadata(); - StringBuilder sql = new StringBuilder("DELETE FROM \"" + metadata.schema() + "\".\"" + metadata.table() + "\" WHERE "); + StringBuilder stringBuilder = new StringBuilder("DELETE FROM \"" + metadata.schema() + "\".\"" + metadata.table() + "\" WHERE "); List values = new ArrayList<>(); for (ColumnValuePair idColumn : idColumns) { - sql.append("\"").append(idColumn.column()).append("\" = ? AND "); + stringBuilder.append("\"").append(idColumn.column()).append("\" = ? AND "); values.add(idColumn.value()); } - sql.setLength(sql.length() - 5); + stringBuilder.setLength(stringBuilder.length() - 5); + @Language("SQL") String sql = stringBuilder.toString(); try { - dataManager.getDataAccessor().executeUpdate(sql.toString(), values, 0); + dataManager.getDataAccessor().executeUpdate(SQLTransaction.Statement.of(sql, sql), values, 0); } catch (SQLException 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 e5725396..08a20099 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,10 +1,7 @@ package net.staticstudios.data.impl.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.DataAccessor; -import net.staticstudios.data.OneToOne; -import net.staticstudios.data.Reference; -import net.staticstudios.data.UniqueData; +import net.staticstudios.data.*; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; import net.staticstudios.data.utils.Link; @@ -164,8 +161,10 @@ public void set(@Nullable T value) { values.add(columnValuePair.value()); } + @Language("SQL") String sql = sqlBuilder.toString(); + try { - holder.getDataManager().getDataAccessor().executeUpdate(sqlBuilder.toString(), values, 0); + holder.getDataManager().getDataAccessor().executeUpdate(SQLTransaction.Statement.of(sql, sql), values, 0); } catch (SQLException e) { throw new RuntimeException(e); } diff --git a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java index c9ecc365..f44833af 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.lang.ref.WeakReference; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -52,7 +53,7 @@ public void testReadData() throws SQLException { } @Test - public void test() throws SQLException { //todo: this test throws an exception from pg + public void testUniqueDataCache() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); UUID id = UUID.randomUUID(); @@ -61,42 +62,121 @@ public void test() throws SQLException { //todo: this test throws an exception f .name("test user") .nameUpdates(0) .insert(InsertMode.SYNC); - assertEquals("test user", mockUser.name.get()); - mockUser.name.set("updated name"); - assertEquals("updated name", mockUser.name.get()); - assertNull(mockUser.age.get()); - mockUser.age.set(25); - assertEquals(25, mockUser.age.get()); + WeakReference weakRef = new WeakReference<>(mockUser); + + System.gc(); + + assertNotNull(weakRef.get()); mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); + assertSame(mockUser, weakRef.get()); mockUser = null; // remove strong reference System.gc(); + + assertNull(weakRef.get()); + mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss + } + + @Test + public void testUpdate() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUser.builder(dataManager) + .id(id) + .name("test user") + .age(0) + .insert(InsertMode.SYNC); + + assertEquals(0, mockUser.age.get()); + + Connection h2Connection = getH2Connection(dataManager); + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT \"age\" FROM \"public\".\"users\" WHERE \"id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(0, rs.getObject("age")); + } + + waitForDataPropagation(); + + Connection pgConnection = getConnection(); + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT \"age\" FROM \"public\".\"users\" WHERE \"id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(0, rs.getObject("age")); + } + + mockUser.age.set(30); + + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT \"age\" FROM \"public\".\"users\" WHERE \"id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(30, rs.getObject("age")); + } + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT \"age\" FROM \"public\".\"users\" WHERE \"id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals(30, rs.getObject("age")); + } + } + + @Test + public void testUpdateForeignColumn() throws SQLException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUser.builder(dataManager) + .id(id) + .name("test user") + .favoriteColor("blue") + .insert(InsertMode.SYNC); - assertNull(mockUser.favoriteColor.get()); - mockUser.favoriteColor.set("blue"); assertEquals("blue", mockUser.favoriteColor.get()); -/// / long start; -/// / int count = 10_000; -/// / for (int j = 0; j < 5; j++) { -/// / start = System.currentTimeMillis(); -/// / for (int i = 0; i < count; i++) { -/// / mockUser.name.set("name " + i); -/// / } -/// / -/// / System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " updates"); -/// / } -/// / for (int j = 0; j < 5; j++) { -/// / start = System.currentTimeMillis(); -/// / for (int i = 0; i < count; i++) { -/// / mockUser.name.get(); -/// / } -/// / System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to do " + count + " gets"); -/// / } + Connection h2Connection = getH2Connection(dataManager); + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT \"fav_color\" FROM \"public\".\"user_preferences\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals("blue", rs.getObject("fav_color")); + } waitForDataPropagation(); + + Connection pgConnection = getConnection(); + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT \"fav_color\" FROM \"public\".\"user_preferences\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals("blue", rs.getObject("fav_color")); + } + + mockUser.favoriteColor.set("red"); + + try (PreparedStatement preparedStatement = h2Connection.prepareStatement("SELECT \"fav_color\" FROM \"public\".\"user_preferences\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals("red", rs.getObject("fav_color")); + } + + waitForDataPropagation(); + + try (PreparedStatement preparedStatement = pgConnection.prepareStatement("SELECT \"fav_color\" FROM \"public\".\"user_preferences\" WHERE \"user_id\" = ?")) { + preparedStatement.setObject(1, id); + ResultSet rs = preparedStatement.executeQuery(); + assertTrue(rs.next()); + assertEquals("red", rs.getObject("fav_color")); + } } @Test From 84256791c4ed1d20b8a983de75570a2cac889bd7 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 8 Nov 2025 10:50:30 -0500 Subject: [PATCH 46/75] add cached values (simple redis backed values) --- .../net/staticstudios/data/ExpireAfter.java | 21 ++ .../net/staticstudios/data/Identifier.java | 15 ++ .../net/staticstudios/data/CachedValue.java | 117 ++++++++++ .../net/staticstudios/data/DataManager.java | 178 ++++++++++---- .../staticstudios/data/PersistentValue.java | 2 +- .../net/staticstudios/data/StaticData.java | 10 + .../net/staticstudios/data/UniqueData.java | 1 + .../data/{ => impl}/DataAccessor.java | 11 +- .../data/impl/data/CachedValueImpl.java | 137 +++++++++++ .../PersistentManyToManyCollectionImpl.java | 5 +- .../PersistentOneToManyCollectionImpl.java | 5 +- ...ersistentOneToManyValueCollectionImpl.java | 5 +- .../data/impl/data/ReferenceImpl.java | 5 +- .../data/impl/h2/H2DataAccessor.java | 217 +++++++++++++----- .../h2/trigger/H2UpdateHandlerTrigger.java | 2 +- .../data/impl/redis/RedisCacheEntry.java | 6 + .../data/impl/redis/RedisEvent.java | 7 + .../data/impl/redis/RedisEventHandler.java | 9 + .../data/impl/redis/RedisListener.java | 78 +++++++ .../data/primative/Primitives.java | 3 +- .../data/util/CachedValueMetadata.java | 7 + .../util/CachedValueUpdateHandlerWrapper.java | 35 +++ .../data/util/LambdaNonStaticException.java | 7 + .../staticstudios/data/util/LambdaUtils.java | 43 ++++ .../staticstudios/data/util/RedisUtils.java | 69 ++++++ .../data/{ => util}/SQLTransaction.java | 2 +- .../data/util/UniqueDataMetadata.java | 1 + .../ValueUpdateHandlerNonStaticException.java | 7 - .../data/util/ValueUpdateHandlerWrapper.java | 13 +- .../staticstudios/data/CachedValueTest.java | 161 +++++++++++++ .../PersistentOneToManyCollectionTest.java | 1 - .../data/mock/user/MockUser.java | 13 +- 32 files changed, 1060 insertions(+), 133 deletions(-) create mode 100644 annotations/src/main/java/net/staticstudios/data/ExpireAfter.java create mode 100644 annotations/src/main/java/net/staticstudios/data/Identifier.java create mode 100644 core/src/main/java/net/staticstudios/data/CachedValue.java create mode 100644 core/src/main/java/net/staticstudios/data/StaticData.java rename core/src/main/java/net/staticstudios/data/{ => impl}/DataAccessor.java (70%) create mode 100644 core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/redis/RedisCacheEntry.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/redis/RedisEvent.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/redis/RedisEventHandler.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/redis/RedisListener.java create mode 100644 core/src/main/java/net/staticstudios/data/util/CachedValueMetadata.java create mode 100644 core/src/main/java/net/staticstudios/data/util/CachedValueUpdateHandlerWrapper.java create mode 100644 core/src/main/java/net/staticstudios/data/util/LambdaNonStaticException.java create mode 100644 core/src/main/java/net/staticstudios/data/util/LambdaUtils.java create mode 100644 core/src/main/java/net/staticstudios/data/util/RedisUtils.java rename core/src/main/java/net/staticstudios/data/{ => util}/SQLTransaction.java (98%) delete mode 100644 core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java create mode 100644 core/src/test/java/net/staticstudios/data/CachedValueTest.java diff --git a/annotations/src/main/java/net/staticstudios/data/ExpireAfter.java b/annotations/src/main/java/net/staticstudios/data/ExpireAfter.java new file mode 100644 index 00000000..bad90f63 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/ExpireAfter.java @@ -0,0 +1,21 @@ +package net.staticstudios.data; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used for CachedValues. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ExpireAfter { + /** + * How long until the value in redis is deleted? + * In other words, how long until we revert back to the fallback value? + * + * @return The duration in seconds. + */ + int value() default -1; +} diff --git a/annotations/src/main/java/net/staticstudios/data/Identifier.java b/annotations/src/main/java/net/staticstudios/data/Identifier.java new file mode 100644 index 00000000..86fb3772 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/Identifier.java @@ -0,0 +1,15 @@ +package net.staticstudios.data; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used for CachedValues. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Identifier { + String value(); +} diff --git a/core/src/main/java/net/staticstudios/data/CachedValue.java b/core/src/main/java/net/staticstudios/data/CachedValue.java new file mode 100644 index 00000000..af52e133 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/CachedValue.java @@ -0,0 +1,117 @@ +package net.staticstudios.data; + +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import net.staticstudios.data.impl.data.CachedValueImpl; +import net.staticstudios.data.util.*; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * A cached value represents a piece of data in redis. + * + * @param + */ +public interface CachedValue extends Value { + + static CachedValue of(UniqueData holder, Class dataType) { + return new ProxyCachedValue<>(holder, dataType); + } + + UniqueData getHolder(); + + Class getDataType(); + + CachedValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler); + + default CachedValue withFallback(T fallback) { + return supplyFallback(() -> fallback); + } + + CachedValue supplyFallback(Supplier fallback); + + class ProxyCachedValue implements CachedValue { + protected final UniqueData holder; + protected final Class dataType; + private final List> updateHandlers = new ArrayList<>(); + private @Nullable CachedValue delegate; + private Supplier fallback = () -> null; + + public ProxyCachedValue(UniqueData holder, Class dataType) { + this.holder = holder; + this.dataType = dataType; + } + + public void setDelegate(CachedValueMetadata metadata, CachedValueImpl delegate) { + Preconditions.checkNotNull(delegate, "Delegate cannot be null"); + Preconditions.checkState(this.delegate == null, "Delegate is already set"); + delegate.setFallback(this.fallback); + this.delegate = delegate; + + //since an update handler can be registered before the fallback is set, we need to convert them here + List> cachedValueUpdateHandlers = new ArrayList<>(); + for (ValueUpdateHandlerWrapper handler : updateHandlers) { + cachedValueUpdateHandlers.add(asCachedValueHandler(handler)); + } + + holder.getDataManager().registerCachedValueUpdateHandler(metadata, cachedValueUpdateHandlers); + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getDataType() { + return dataType; + } + + @Override + public CachedValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler) { + Preconditions.checkArgument(delegate == null, "Cannot dynamically add an update handler after the holder has been initialized!"); + ValueUpdateHandlerWrapper wrapper = new ValueUpdateHandlerWrapper<>(updateHandler, dataType, holderClass); + this.updateHandlers.add(wrapper); + return this; + } + + @Override + public CachedValue supplyFallback(Supplier fallback) { + if (delegate != null) { + 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; + } + + @Override + public T get() { + if (delegate != null) { + return delegate.get(); + } + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void set(@Nullable T value) { + if (delegate != null) { + delegate.set(value); + return; + } + throw new UnsupportedOperationException("Not implemented"); + } + + private CachedValueUpdateHandlerWrapper asCachedValueHandler(ValueUpdateHandlerWrapper handlerWrapper) { + return new CachedValueUpdateHandlerWrapper<>( + handlerWrapper.getHandler(), + handlerWrapper.getDataType(), + handlerWrapper.getHolderClass(), + this.fallback + ); + } + } +} diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 9f502c6a..8c2b4117 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -2,9 +2,11 @@ import com.google.common.base.Preconditions; import com.google.common.collect.MapMaker; +import net.staticstudios.data.impl.DataAccessor; import net.staticstudios.data.impl.data.*; import net.staticstudios.data.impl.h2.H2DataAccessor; import net.staticstudios.data.impl.pg.PostgresListener; +import net.staticstudios.data.impl.redis.RedisListener; import net.staticstudios.data.insert.InsertContext; import net.staticstudios.data.parse.*; import net.staticstudios.data.primative.Primitives; @@ -14,7 +16,7 @@ import net.staticstudios.utils.ThreadUtils; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,9 +38,13 @@ public class DataManager { private final TaskQueue taskQueue; private final ConcurrentHashMap, UniqueDataMetadata> uniqueDataMetadataMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap, Map> uniqueDataInstanceCache = new ConcurrentHashMap<>(); - private final ConcurrentHashMap, List>>> updateHandlers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap, List>>> persistentValueUpdateHandlers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap, List>>> cachedValueUpdateHandlers = new ConcurrentHashMap<>(); private final PostgresListener postgresListener; - private final Set registeredUpdateHandlersForColumns = Collections.synchronizedSet(new HashSet<>()); + private final RedisListener redisListener; + private final Set registeredUpdateHandlersForColumns = ConcurrentHashMap.newKeySet(); + private final Set registeredUpdateHandlersForRedis = ConcurrentHashMap.newKeySet(); + private final List> valueSerializers = new CopyOnWriteArrayList<>(); //todo: custom types are serialized and deserialized all the time currently, we should have a cache for these. caffeine with time based eviction sounds good. @@ -57,13 +63,12 @@ public DataManager(DataSourceConfig dataSourceConfig, boolean setGlobal) { DataManager.useGlobal = setGlobal; applicationName = "static_data_manager_v3-" + UUID.randomUUID(); postgresListener = new PostgresListener(this, dataSourceConfig); - sqlBuilder = new SQLBuilder(this); this.taskQueue = new TaskQueue(dataSourceConfig, applicationName); - dataAccessor = new H2DataAccessor(this, postgresListener, taskQueue); + redisListener = new RedisListener(dataSourceConfig, this.taskQueue); + sqlBuilder = new SQLBuilder(this); + dataAccessor = new H2DataAccessor(this, postgresListener, redisListener, taskQueue); //todo: when we reconnect to postgres, refresh the internal cache from the source - - //todo: support for CachedValues } public static DataManager getInstance() { @@ -89,15 +94,21 @@ public InsertContext createInsertContext() { public void addUpdateHandler(String schema, String table, String column, ValueUpdateHandlerWrapper handler) {//todo: allow us to specify what data type to convert the data to. this is useful when this method is called externally String key = schema + "." + table + "." + column; - updateHandlers.computeIfAbsent(key, k -> new ConcurrentHashMap<>()) + persistentValueUpdateHandlers.computeIfAbsent(key, k -> new ConcurrentHashMap<>()) + .computeIfAbsent(handler.getHolderClass(), k -> new CopyOnWriteArrayList<>()) + .add(handler); + } + + private void addRedisUpdateHandler(String partialKey, CachedValueUpdateHandlerWrapper handler) { + cachedValueUpdateHandlers.computeIfAbsent(partialKey, k -> new ConcurrentHashMap<>()) .computeIfAbsent(handler.getHolderClass(), k -> new CopyOnWriteArrayList<>()) .add(handler); } @ApiStatus.Internal - public void callUpdateHandlers(List columnNames, String schema, String table, String column, Object[] oldSerializedValues, Object[] newSerializedValues) { + public void callPersistentValueUpdateHandlers(List columnNames, String schema, String table, String column, Object[] oldSerializedValues, Object[] newSerializedValues) { logger.trace("Calling update handlers for {}.{}.{} with old values {} and new values {}", schema, table, column, Arrays.toString(oldSerializedValues), Arrays.toString(newSerializedValues)); - Map, List>> handlersForColumn = updateHandlers.get(schema + "." + table + "." + column); + Map, List>> handlersForColumn = persistentValueUpdateHandlers.get(schema + "." + table + "." + column); if (handlersForColumn == null) { return; } @@ -136,7 +147,59 @@ public void callUpdateHandlers(List columnNames, String schema, String t } @ApiStatus.Internal - public void registerUpdateHandler(PersistentValueMetadata metadata, Collection> handlers) { + public void callCachedValueUpdateHandlers(String partialKey, List encodedIdNames, List encodedIdValues, @Nullable String oldValue, @Nullable String newValue) { + if (Objects.equals(oldValue, newValue)) { + return; + } + logger.trace("Calling Redis update handlers for {} with old value {} and new value {}", partialKey, oldValue, newValue); + Map, List>> handlersForKey = cachedValueUpdateHandlers.get(partialKey); + if (handlersForKey == null) { + return; + } + for (Map.Entry, List>> entry : handlersForKey.entrySet()) { + Class holderClass = entry.getKey(); + UniqueDataMetadata metadata = getMetadata(holderClass); + List> handlers = entry.getValue(); + ColumnValuePair[] idColumns = new ColumnValuePair[encodedIdValues.size()]; + for (int i = 0; i < encodedIdNames.size(); i++) { + Class valueType = null; + for (ColumnMetadata idColumn : metadata.idColumns()) { + if (idColumn.name().equals(encodedIdNames.get(i))) { + valueType = idColumn.type(); + break; + } + } + Preconditions.checkNotNull(valueType, "Could not find ID column %s for UniqueData class %s", encodedIdNames.get(i), holderClass.getName()); + Object decodedValue = Primitives.decode(getSerializedType(valueType), encodedIdValues.get(i)); + Object deserializedValue = deserialize(valueType, decodedValue); + idColumns[i] = new ColumnValuePair(encodedIdNames.get(i), deserializedValue); + } + UniqueData instance = getInstance(holderClass, idColumns); + for (CachedValueUpdateHandlerWrapper wrapper : handlers) { + Class serializedType = getSerializedType(wrapper.getDataType()); + Object deserializedOldValue; + Object deserializedNewValue; + + if (oldValue == null) { + deserializedOldValue = wrapper.getFallback(); + } else { + Object decodedOldValue = Primitives.decode(serializedType, oldValue); + deserializedOldValue = deserialize(wrapper.getDataType(), decodedOldValue); + } + + if (newValue == null) { + deserializedNewValue = wrapper.getFallback(); + } else { + Object decodedNewValue = Primitives.decode(serializedType, newValue); + deserializedNewValue = deserialize(wrapper.getDataType(), decodedNewValue); + } + ThreadUtils.submit(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); //todo: submit somewher based on config + } + } + } + + @ApiStatus.Internal + public void registerPersistentValueUpdateHandler(PersistentValueMetadata metadata, Collection> handlers) { if (registeredUpdateHandlersForColumns.add(metadata)) { for (ValueUpdateHandlerWrapper handler : handlers) { addUpdateHandler(metadata.getSchema(), metadata.getTable(), metadata.getColumn(), handler); @@ -144,18 +207,30 @@ public void registerUpdateHandler(PersistentValueMetadata metadata, Collection> handlers) { + if (registeredUpdateHandlersForRedis.add(metadata)) { + UniqueDataMetadata holderMetadata = getMetadata(metadata.holderClass()); + String partialKey = RedisUtils.buildPartialRedisKey(metadata.holderSchema(), metadata.holderTable(), metadata.identifier(), holderMetadata.idColumns()); + for (CachedValueUpdateHandlerWrapper handler : handlers) { + addRedisUpdateHandler(partialKey, handler); + } + } + } + public List> getUpdateHandlers(String schema, String table, String column, Class holderClass) { String key = schema + "." + table + "." + column; - if (updateHandlers.containsKey(key) && updateHandlers.get(key).containsKey(holderClass)) { - return updateHandlers.get(key).get(holderClass); + if (persistentValueUpdateHandlers.containsKey(key) && persistentValueUpdateHandlers.get(key).containsKey(holderClass)) { + return persistentValueUpdateHandlers.get(key).get(holderClass); } return Collections.emptyList(); } @SafeVarargs public final void load(Class... classes) { + List extracted = new ArrayList<>(); for (Class clazz : classes) { - extractMetadata(clazz); + extracted.add(extractMetadata(clazz)); } List defs = new ArrayList<>(); for (Class clazz : classes) { @@ -175,9 +250,17 @@ public final void load(Class... classes) { } catch (SQLException e) { throw new RuntimeException(e); } + + List partialRedisKeys = new ArrayList<>(); + for (UniqueDataMetadata metadata : extracted) { + for (CachedValueMetadata cachedValueMetadata : metadata.cachedValueMetadata().values()) { + partialRedisKeys.add(RedisUtils.buildPartialRedisKey(cachedValueMetadata.holderSchema(), cachedValueMetadata.holderTable(), cachedValueMetadata.identifier(), metadata.idColumns())); + } + } + dataAccessor.discoverRedisKeys(partialRedisKeys); } - public void extractMetadata(Class clazz) { + public UniqueDataMetadata extractMetadata(Class clazz) { Preconditions.checkArgument(!uniqueDataMetadataMap.containsKey(clazz), "UniqueData class %s has already been parsed", clazz.getName()); Data dataAnnotation = clazz.getAnnotation(Data.class); Preconditions.checkNotNull(dataAnnotation, "UniqueData class %s is missing @Data annotation", clazz.getName()); @@ -205,7 +288,16 @@ public void extractMetadata(Class clazz) { persistentCollectionMetadataMap.putAll(PersistentOneToManyCollectionImpl.extractMetadata(clazz)); persistentCollectionMetadataMap.putAll(PersistentManyToManyCollectionImpl.extractMetadata(clazz)); persistentCollectionMetadataMap.putAll(PersistentOneToManyValueCollectionImpl.extractMetadata(clazz, schema)); - UniqueDataMetadata metadata = new UniqueDataMetadata(clazz, schema, table, idColumns, PersistentValueImpl.extractMetadata(schema, table, clazz), ReferenceImpl.extractMetadata(clazz), persistentCollectionMetadataMap); + UniqueDataMetadata metadata = new UniqueDataMetadata( + clazz, + schema, + table, + idColumns, + CachedValueImpl.extractMetadata(schema, table, clazz), + PersistentValueImpl.extractMetadata(schema, table, clazz), + ReferenceImpl.extractMetadata(clazz), + persistentCollectionMetadataMap + ); uniqueDataMetadataMap.put(clazz, metadata); for (Field field : ReflectionUtils.getFields(clazz, Relation.class)) { @@ -217,6 +309,8 @@ public void extractMetadata(Class clazz) { } } } + + return metadata; } public UniqueDataMetadata getMetadata(Class clazz) { @@ -359,7 +453,7 @@ public T getInstance(Class clazz, ColumnValuePair... i } for (ColumnValuePair providedIdColumn : idColumns) { - Preconditions.checkNotNull(providedIdColumn.value(), "ID name value for name %s in UniqueData class %s cannot be null", providedIdColumn.column(), clazz.getName()); + Preconditions.checkNotNull(providedIdColumn.value(), "ID column value for column %s in UniqueData class %s cannot be null", providedIdColumn.column(), clazz.getName()); } Preconditions.checkArgument(hasAllIdColumns, "Not all @IdColumn columnsInReferringTable were provided for UniqueData class %s. Required: %s, Provided: %s", clazz.getName(), metadata.idColumns(), idColumns); @@ -404,6 +498,7 @@ public T getInstance(Class clazz, ColumnValuePair... i } PersistentValueImpl.delegate(instance); + CachedValueImpl.delegate(instance); ReferenceImpl.delegate(instance); PersistentOneToManyCollectionImpl.delegate(instance); PersistentManyToManyCollectionImpl.delegate(instance); @@ -417,37 +512,6 @@ public T getInstance(Class clazz, ColumnValuePair... i return instance; } - /** - * Submit a blocking task to the priority task queue. - * This method is blocking and will wait for the task to complete before returning. - * - * @param task The task to submit - */ - @Blocking - public void submitBlockingTask(ConnectionConsumer task) { - taskQueue.submitTask(task).join(); - } - - /** - * Submit a blocking task to the priority task queue. - * This method is blocking and will wait for the task to complete before returning. - * - * @param task The task to submit - */ - @Blocking - public void submitBlockingTask(ConnectionJedisConsumer task) { - taskQueue.submitTask(task).join(); - } - - public void submitAsyncTask(ConnectionConsumer task) { - taskQueue.submitTask(task); - } - - - public void submitAsyncTask(ConnectionJedisConsumer task) { - taskQueue.submitTask(task); - } - public void insert(InsertContext insertContext, InsertMode insertMode) { //todo: when inserting validate all id and required values are present - this will be enforced by h2, but we should do it here for better logging/errors. Set tables = new HashSet<>(); @@ -759,6 +823,24 @@ 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); + if (encoded == null) { + return null; + } + Object serialized = Primitives.decode(getSerializedType(type), encoded); + 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); + Object serialized = serialize(value); + String encoded = Primitives.encode(serialized); + dataAccessor.setRedisValue(key, encoded, expireAfterSeconds); + } + private boolean hasCycle(SQLTable table, Map> dependencyGraph, Set visited, Set stack) { if (stack.contains(table)) { return true; diff --git a/core/src/main/java/net/staticstudios/data/PersistentValue.java b/core/src/main/java/net/staticstudios/data/PersistentValue.java index 0687bf0d..a64868af 100644 --- a/core/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/core/src/main/java/net/staticstudios/data/PersistentValue.java @@ -43,7 +43,7 @@ public void setDelegate(PersistentValueMetadata metadata, PersistentValue del Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); this.delegate = delegate; - holder.getDataManager().registerUpdateHandler(metadata, updateHandlers); + holder.getDataManager().registerPersistentValueUpdateHandler(metadata, updateHandlers); } @Override diff --git a/core/src/main/java/net/staticstudios/data/StaticData.java b/core/src/main/java/net/staticstudios/data/StaticData.java new file mode 100644 index 00000000..af6f8203 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/StaticData.java @@ -0,0 +1,10 @@ +package net.staticstudios.data; + +public class StaticData { + + public static void init() { + //TODO: this should be the entry point for static-data. consumers should be able to pass in some sort of datasource config to this method. + // a global dm will then be initialized. also loading and any other public methods would be cool to have here. basically a consumer will never touch the datamanager directly, + // they will only need to use static methods here + } +} diff --git a/core/src/main/java/net/staticstudios/data/UniqueData.java b/core/src/main/java/net/staticstudios/data/UniqueData.java index 9bb8b357..51451e75 100644 --- a/core/src/main/java/net/staticstudios/data/UniqueData.java +++ b/core/src/main/java/net/staticstudios/data/UniqueData.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.util.ColumnValuePair; import net.staticstudios.data.util.ColumnValuePairs; +import net.staticstudios.data.util.SQLTransaction; import net.staticstudios.data.util.UniqueDataMetadata; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; diff --git a/core/src/main/java/net/staticstudios/data/DataAccessor.java b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java similarity index 70% rename from core/src/main/java/net/staticstudios/data/DataAccessor.java rename to core/src/main/java/net/staticstudios/data/impl/DataAccessor.java index 39c61194..b292047b 100644 --- a/core/src/main/java/net/staticstudios/data/DataAccessor.java +++ b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java @@ -1,8 +1,11 @@ -package net.staticstudios.data; +package net.staticstudios.data.impl; +import net.staticstudios.data.InsertMode; import net.staticstudios.data.parse.DDLStatement; +import net.staticstudios.data.util.SQLTransaction; import net.staticstudios.data.util.SQlStatement; import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Nullable; import java.sql.ResultSet; import java.sql.SQLException; @@ -23,4 +26,10 @@ default void executeUpdate(SQLTransaction.Statement statement, List valu void runDDL(DDLStatement ddl) throws SQLException; void postDDL() throws SQLException; + + @Nullable String getRedisValue(String key); + + void setRedisValue(String key, String value, int expirationSeconds); + + void discoverRedisKeys(List partialRedisKeys); } 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 new file mode 100644 index 00000000..0f01b6da --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java @@ -0,0 +1,137 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import net.staticstudios.data.CachedValue; +import net.staticstudios.data.ExpireAfter; +import net.staticstudios.data.Identifier; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.util.*; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class CachedValueImpl implements CachedValue { + private final UniqueData holder; + private final Class dataType; + private final CachedValueMetadata metadata; + private Supplier fallback; + + private CachedValueImpl(UniqueData holder, Class dataType, CachedValueMetadata metadata) { + this.holder = holder; + this.dataType = dataType; + this.metadata = metadata; + } + + public static void createAndDelegate(ProxyCachedValue proxy, CachedValueMetadata metadata) { + CachedValueImpl delegate = new CachedValueImpl<>( + proxy.getHolder(), + proxy.getDataType(), + metadata + ); + + proxy.setDelegate(metadata, delegate); + } + + public static CachedValueImpl create(UniqueData holder, Class dataType, CachedValueMetadata metadata) { + return new CachedValueImpl<>(holder, dataType, metadata); + } + + 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); + } + } + } + } + + public static Map extractMetadata(String holderSchema, String holderTable, Class clazz) { + Map metadataMap = new HashMap<>(); + for (Field field : ReflectionUtils.getFields(clazz, CachedValue.class)) { + metadataMap.put(field, extractMetadata(holderSchema, holderTable, clazz, field)); + } + return metadataMap; + } + + public static CachedValueMetadata extractMetadata(String holderSchema, String holderTable, Class clazz, Field field) { + Identifier identifierAnnotation = field.getAnnotation(Identifier.class); + ExpireAfter expireAfterAnnotation = field.getAnnotation(ExpireAfter.class); + + + Preconditions.checkNotNull(identifierAnnotation, "CachedValue field %s is missing @Identifier annotation".formatted(field.getName())); + + int expireAfterSeconds = -1; + if (expireAfterAnnotation != null) { + expireAfterSeconds = expireAfterAnnotation.value(); + } + + return new CachedValueMetadata(clazz, holderSchema, holderTable, ValueUtils.parseValue(identifierAnnotation.value()), expireAfterSeconds); + } + + public void setFallback(Supplier fallback) { + this.fallback = fallback; + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getDataType() { + return dataType; + } + + @Override + public CachedValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler) { + throw new UnsupportedOperationException("Dynamically adding update handlers is not supported"); + } + + @Override + public CachedValue supplyFallback(Supplier fallback) { + throw new UnsupportedOperationException("Cannot set fallback after initialization"); + } + + @Override + public T get() { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot get value from a deleted UniqueData instance"); + T value = holder.getDataManager().getRedis(metadata.holderSchema(), metadata.holderTable(), metadata.identifier(), holder.getIdColumns(), dataType); + if (value == null) { + return fallback.get(); + } + return value; + } + + @Override + public void set(@Nullable T value) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set value on a deleted UniqueData instance"); + T fallback = this.fallback.get(); + T toSet; + if (Objects.equals(fallback, value)) { + toSet = null; + } else { + toSet = value; + } + holder.getDataManager().setRedis(metadata.holderSchema(), metadata.holderTable(), metadata.identifier(), holder.getIdColumns(), metadata.expireAfterSeconds(), toSet); + } + + @Override + public String toString() { + if (holder.isDeleted()) { + return "[DELETED]"; + } + return String.valueOf(get()); + } +} 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 77e698f4..931e161e 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 @@ -1,7 +1,10 @@ package net.staticstudios.data.impl.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.*; +import net.staticstudios.data.ManyToMany; +import net.staticstudios.data.PersistentCollection; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.impl.DataAccessor; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; import net.staticstudios.data.utils.Link; 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 6a693b28..699a7d88 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 @@ -1,7 +1,10 @@ package net.staticstudios.data.impl.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.*; +import net.staticstudios.data.OneToMany; +import net.staticstudios.data.PersistentCollection; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.impl.DataAccessor; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; import net.staticstudios.data.utils.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 51efa86e..651e9dd2 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 @@ -1,7 +1,10 @@ package net.staticstudios.data.impl.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.*; +import net.staticstudios.data.OneToMany; +import net.staticstudios.data.PersistentCollection; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.impl.DataAccessor; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; import net.staticstudios.data.utils.Link; 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 08a20099..6167c8db 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,7 +1,10 @@ package net.staticstudios.data.impl.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.*; +import net.staticstudios.data.OneToOne; +import net.staticstudios.data.Reference; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.impl.DataAccessor; import net.staticstudios.data.parse.SQLBuilder; import net.staticstudios.data.util.*; import net.staticstudios.data.utils.Link; 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 1a96b67a..f28fa961 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 @@ -2,33 +2,37 @@ import com.google.common.base.Preconditions; import com.impossibl.postgres.api.jdbc.PGConnection; -import net.staticstudios.data.DataAccessor; import net.staticstudios.data.DataManager; import net.staticstudios.data.InsertMode; -import net.staticstudios.data.SQLTransaction; +import net.staticstudios.data.impl.DataAccessor; import net.staticstudios.data.impl.h2.trigger.H2UpdateHandlerTrigger; import net.staticstudios.data.impl.pg.PostgresListener; +import net.staticstudios.data.impl.redis.RedisCacheEntry; +import net.staticstudios.data.impl.redis.RedisEvent; +import net.staticstudios.data.impl.redis.RedisListener; import net.staticstudios.data.parse.DDLStatement; 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.SQlStatement; -import net.staticstudios.data.util.SchemaTable; +import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; import net.staticstudios.utils.Pair; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import redis.clients.jedis.params.ScanParams; +import redis.clients.jedis.resps.ScanResult; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.sql.*; +import java.time.Instant; import java.util.*; import java.util.concurrent.*; import java.util.function.Consumer; @@ -55,10 +59,14 @@ public class H2DataAccessor implements DataAccessor { t.setName(H2DataAccessor.class.getSimpleName() + "-ScheduledExecutor"); return t; }); + private final RedisListener redisListener; + private final Map redisCache = new ConcurrentHashMap<>(); + private final Set knownRedisPartialKeys = ConcurrentHashMap.newKeySet(); - public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener, TaskQueue taskQueue) { + public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener, RedisListener redisListener, TaskQueue taskQueue) { this.taskQueue = taskQueue; this.postgresListener = postgresListener; + this.redisListener = redisListener; 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; @@ -210,68 +218,94 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener }); } - public synchronized void sync(List schemaTables) throws SQLException { + public synchronized void resync() { //todo: i would ideally like to support periodic resyncing of data. even if this means we pause everything until then. not exactly sure how this would look tho. //todo: when we resync we should clear the task queue and steal the connection //todo: if possible, id like to pause everything else until we are done syncing - dataManager.submitBlockingTask(realDbConnection -> { - Connection h2Connection = getConnection(); - boolean autoCommit = h2Connection.getAutoCommit(); - try ( - Statement h2Statement = h2Connection.createStatement() - ) { - h2Connection.setAutoCommit(false); - logger.trace("[H2] {}", SET_REFERENTIAL_INTEGRITY_FALSE); - h2Statement.execute(SET_REFERENTIAL_INTEGRITY_FALSE); - - for (SchemaTable schemaTable : schemaTables) { - String schema = schemaTable.schema(); - String table = schemaTable.table(); - Path tmpFile = Paths.get(System.getProperty("java.io.tmpdir"), UUID.randomUUID() + "_" + schema + "_" + table + ".csv"); - String absolutePath = tmpFile.toAbsolutePath().toString(); - - List columns = getColumnsInTable(schema, table); - - StringBuilder sqlBuilder = new StringBuilder("COPY (SELECT "); - for (String column : columns) { - sqlBuilder.append("\"").append(column).append("\", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(" FROM \"").append(schema).append("\".\"").append(table).append("\") TO STDOUT WITH CSV HEADER"); - @Language("SQL") String copySql = sqlBuilder.toString(); - @Language("SQL") String truncateSql = "TRUNCATE TABLE \"" + schema + "\".\"" + table + "\""; - StringBuilder insertSqlBuilder = new StringBuilder("INSERT INTO \"").append(schema).append("\".\"").append(table).append("\" ("); - for (String column : columns) { - insertSqlBuilder.append("\"").append(column).append("\", "); + } + + public synchronized void sync(List schemaTables, List redisPartialKeys) throws SQLException { + taskQueue.submitTask((realDbConnection, jedis) -> { + if (!schemaTables.isEmpty()) { + Connection h2Connection = getConnection(); + boolean autoCommit = h2Connection.getAutoCommit(); + try ( + Statement h2Statement = h2Connection.createStatement() + ) { + h2Connection.setAutoCommit(false); + logger.trace("[H2] {}", SET_REFERENTIAL_INTEGRITY_FALSE); + h2Statement.execute(SET_REFERENTIAL_INTEGRITY_FALSE); + + for (SchemaTable schemaTable : schemaTables) { + String schema = schemaTable.schema(); + String table = schemaTable.table(); + Path tmpFile = Paths.get(System.getProperty("java.io.tmpdir"), UUID.randomUUID() + "_" + schema + "_" + table + ".csv"); + String absolutePath = tmpFile.toAbsolutePath().toString(); + + List columns = getColumnsInTable(schema, table); + + StringBuilder sqlBuilder = new StringBuilder("COPY (SELECT "); + for (String column : columns) { + sqlBuilder.append("\"").append(column).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(schema).append("\".\"").append(table).append("\") TO STDOUT WITH CSV HEADER"); + @Language("SQL") String copySql = sqlBuilder.toString(); + @Language("SQL") String truncateSql = "TRUNCATE TABLE \"" + schema + "\".\"" + table + "\""; + StringBuilder insertSqlBuilder = new StringBuilder("INSERT INTO \"").append(schema).append("\".\"").append(table).append("\" ("); + for (String column : columns) { + insertSqlBuilder.append("\"").append(column).append("\", "); + } + insertSqlBuilder.setLength(insertSqlBuilder.length() - 2); + insertSqlBuilder.append(") SELECT * FROM CSVREAD('").append(absolutePath).append("')"); + String insertSql = insertSqlBuilder.toString(); + PGConnection pgConnection = realDbConnection.unwrap(PGConnection.class); + FileOutputStream fileOutputStream; + try { + fileOutputStream = new FileOutputStream(absolutePath); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + logger.debug("[DB] {}", copySql); + pgConnection.copyTo(copySql, fileOutputStream); + logger.trace("[H2] {}", truncateSql); + h2Statement.execute(truncateSql); + logger.trace("[H2] {}", insertSql); + h2Statement.execute(insertSql); } - insertSqlBuilder.setLength(insertSqlBuilder.length() - 2); - insertSqlBuilder.append(") SELECT * FROM CSVREAD('").append(absolutePath).append("')"); - String insertSql = insertSqlBuilder.toString(); - PGConnection pgConnection = realDbConnection.unwrap(PGConnection.class); - FileOutputStream fileOutputStream; - try { - fileOutputStream = new FileOutputStream(absolutePath); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); + logger.trace("[H2] {}", SET_REFERENTIAL_INTEGRITY_TRUE); + h2Statement.execute(SET_REFERENTIAL_INTEGRITY_TRUE); + } finally { + if (autoCommit) { + h2Connection.setAutoCommit(true); + } else { + h2Connection.commit(); } - logger.debug("[DB] {}", copySql); - pgConnection.copyTo(copySql, fileOutputStream); - logger.trace("[H2] {}", truncateSql); - h2Statement.execute(truncateSql); - logger.trace("[H2] {}", insertSql); - h2Statement.execute(insertSql); } - logger.trace("[H2] {}", SET_REFERENTIAL_INTEGRITY_TRUE); - h2Statement.execute(SET_REFERENTIAL_INTEGRITY_TRUE); - } finally { - if (autoCommit) { - h2Connection.setAutoCommit(true); - } else { - h2Connection.commit(); + } + if (!redisPartialKeys.isEmpty()) { + for (String partialKey : redisPartialKeys) { + if (knownRedisPartialKeys.contains(partialKey)) { + continue; + } + + String cursor = ScanParams.SCAN_POINTER_START; + ScanParams scanParams = new ScanParams().match(partialKey).count(1000); + + do { + ScanResult scanResult = jedis.scan(cursor, scanParams); + cursor = scanResult.getCursor(); + + for (String key : scanResult.getResult()) { + redisCache.put(key, new RedisCacheEntry(jedis.get(key), Instant.now())); + } + } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); + + redisListener.listen(partialKey, this::handleRedisEvent); } } - }); + }).join(); //todo: start listening to changes from pg // then log them @@ -456,6 +490,53 @@ public void postDDL() throws SQLException { updateKnownTables(); } + @Override + public @Nullable String getRedisValue(String key) { + RedisCacheEntry cacheEntry = redisCache.get(key); + if (cacheEntry != null) { + return cacheEntry.value(); + } + return null; + } + + @Override + public void setRedisValue(String key, String value, int expirationSeconds) { + RedisCacheEntry prev; + if (value == null) { + prev = redisCache.remove(key); + taskQueue.submitTask((connection, jedis) -> { + jedis.del(key); + }); + } else { + prev = redisCache.put(key, new RedisCacheEntry(value, Instant.now())); + taskQueue.submitTask((connection, jedis) -> { + if (expirationSeconds > 0) { + jedis.setex(key, expirationSeconds, value); + } else { + jedis.set(key, value); + } + }); + } + + String prevValue = null; + + if (prev != null) { + prevValue = prev.value(); + } + RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); + dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), prevValue, value); + } + + @Override + public void discoverRedisKeys(List partialRedisKeys) { + try { + sync(Collections.emptyList(), partialRedisKeys); + knownRedisPartialKeys.addAll(partialRedisKeys); + } catch (SQLException e) { + logger.error("Error discovering redis keys", e); + } + } + private synchronized void updateKnownTables() throws SQLException { Set currentTables = new HashSet<>(); Connection connection = getConnection(); @@ -480,11 +561,11 @@ private synchronized void updateKnownTables() throws SQLException { createTrigger.execute(formatted); } - dataManager.submitBlockingTask(realDbConnection -> postgresListener.ensureTableHasTrigger(realDbConnection, schema, table)); + taskQueue.submitTask(realDbConnection -> postgresListener.ensureTableHasTrigger(realDbConnection, schema, table)).join(); toSync.add(new SchemaTable(schema, table)); } } - sync(toSync); + sync(toSync, Collections.emptyList()); } knownTables.clear(); knownTables.addAll(currentTables); @@ -557,4 +638,18 @@ private void runDatabaseTask(SQLTransaction transaction, int delay) { }, delay, TimeUnit.MILLISECONDS); } } + + private void handleRedisEvent(RedisEvent event, String key, @Nullable String value) { + if (event == RedisEvent.SET) { + RedisCacheEntry entry = redisCache.get(key); + if (entry != null && (entry.entryInstant().isAfter(Instant.now()) || Objects.equals(entry.value(), value))) { + return; + } + redisCache.put(key, new RedisCacheEntry(value, Instant.now())); + RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); + dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), entry == null ? null : entry.value(), value); + } else if (event == RedisEvent.DEL || event == RedisEvent.EXPIRED) { + redisCache.remove(key); + } + } } 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 a64f07ba..c827de10 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 @@ -85,7 +85,7 @@ private void handleUpdate(Object[] oldRow, Object[] newRow) { } for (String changedColumn : changedColumns) { - dataManager.callUpdateHandlers(columnNames, schema, table, changedColumn, oldRow, newRow); + dataManager.callPersistentValueUpdateHandlers(columnNames, schema, table, changedColumn, oldRow, newRow); } } diff --git a/core/src/main/java/net/staticstudios/data/impl/redis/RedisCacheEntry.java b/core/src/main/java/net/staticstudios/data/impl/redis/RedisCacheEntry.java new file mode 100644 index 00000000..0fb082d7 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/redis/RedisCacheEntry.java @@ -0,0 +1,6 @@ +package net.staticstudios.data.impl.redis; + +import java.time.Instant; + +public record RedisCacheEntry(String value, Instant entryInstant) { +} diff --git a/core/src/main/java/net/staticstudios/data/impl/redis/RedisEvent.java b/core/src/main/java/net/staticstudios/data/impl/redis/RedisEvent.java new file mode 100644 index 00000000..dcd02865 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/redis/RedisEvent.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.impl.redis; + +public enum RedisEvent { + SET, + DEL, + EXPIRED +} diff --git a/core/src/main/java/net/staticstudios/data/impl/redis/RedisEventHandler.java b/core/src/main/java/net/staticstudios/data/impl/redis/RedisEventHandler.java new file mode 100644 index 00000000..fc6455ff --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/redis/RedisEventHandler.java @@ -0,0 +1,9 @@ +package net.staticstudios.data.impl.redis; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface RedisEventHandler { + + void handle(RedisEvent event, @NotNull String key, @Nullable String value); +} 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 new file mode 100644 index 00000000..85f063d0 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/redis/RedisListener.java @@ -0,0 +1,78 @@ +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.utils.ShutdownStage; +import net.staticstudios.utils.ThreadUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +public class RedisListener extends JedisPubSub { + private static final Logger logger = LoggerFactory.getLogger(RedisListener.class); + private final Set listenedPartialKeys = ConcurrentHashMap.newKeySet(); + private final Map handlers = new ConcurrentHashMap<>(); + private final TaskQueue taskQueue; + + public RedisListener(DataSourceConfig ds, TaskQueue taskQueue) { + this.taskQueue = taskQueue; + Thread listenerThread = new Thread(() -> { + try (Jedis jedis = new Jedis(ds.redisHost(), ds.redisPort())) { + jedis.psubscribe(this, Arrays.stream(RedisEvent.values()).map(e -> "__keyevent@0__:" + e.name().toLowerCase()).toArray(String[]::new)); + } catch (JedisConnectionException e) { + if (ThreadUtils.isShuttingDown()) { + return; + } + logger.error("Redis connection lost in listener thread", e); + } + }); + listenerThread.start(); + + ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, () -> { + this.punsubscribe(); + listenerThread.interrupt(); + }); + } + + + public void listen(String partialKey, RedisEventHandler handler) { + if (listenedPartialKeys.add(partialKey)) { + handlers.put(RedisUtils.globToRegex(partialKey), handler); + } + } + + @Override + public void onPMessage(String pattern, String channel, String key) { + logger.trace("Received message: {} on channel: {} with pattern: {}", key, channel, pattern); + String eventString = channel.split(":")[1]; + RedisEvent event = RedisEvent.valueOf(eventString.toUpperCase()); + if (!key.startsWith("static-data:")) { + return; + } + + for (Map.Entry entry : handlers.entrySet()) { + if (entry.getKey().matcher(key).matches()) { + switch (event) { + case SET -> taskQueue.submitTask((connection, jedis) -> { + String encoded = jedis.get(key); + if (encoded == null) { + return; + } + entry.getValue().handle(event, key, encoded); + }); + case DEL, EXPIRED -> entry.getValue().handle(event, key, null); + } + return; + } + } + } +} 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 d5c8cb8e..05b92608 100644 --- a/core/src/main/java/net/staticstudios/data/primative/Primitives.java +++ b/core/src/main/java/net/staticstudios/data/primative/Primitives.java @@ -2,6 +2,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.util.PostgresUtils; +import org.jetbrains.annotations.Nullable; import java.sql.Timestamp; import java.time.OffsetDateTime; @@ -94,7 +95,7 @@ public static T decode(Class type, String value) { return getPrimitive(type).decode(value); } - public static String encode(Object value) { + public static String encode(@Nullable Object value) { if (value == null) { return null; } diff --git a/core/src/main/java/net/staticstudios/data/util/CachedValueMetadata.java b/core/src/main/java/net/staticstudios/data/util/CachedValueMetadata.java new file mode 100644 index 00000000..ac3491ea --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/CachedValueMetadata.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +public record CachedValueMetadata(Class holderClass, String holderSchema, String holderTable, + String identifier, int expireAfterSeconds) { +} diff --git a/core/src/main/java/net/staticstudios/data/util/CachedValueUpdateHandlerWrapper.java b/core/src/main/java/net/staticstudios/data/util/CachedValueUpdateHandlerWrapper.java new file mode 100644 index 00000000..18228e5a --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/CachedValueUpdateHandlerWrapper.java @@ -0,0 +1,35 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +import java.util.function.Supplier; + +public class CachedValueUpdateHandlerWrapper extends ValueUpdateHandlerWrapper { + private final Supplier fallbackSupplier; + + public CachedValueUpdateHandlerWrapper(ValueUpdateHandler handler, Class dataType, Class holderClass, Supplier fallbackSupplier) { + super(handler, dataType, holderClass); + this.fallbackSupplier = fallbackSupplier; + } + + public T getFallback() { + return fallbackSupplier.get(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + CachedValueUpdateHandlerWrapper that = (CachedValueUpdateHandlerWrapper) obj; + return super.equals(that); + } + + @Override + public String toString() { + return "CachedValueUpdateHandlerWrapper{" + + "handler=" + getHandler() + + ", dataType=" + getDataType() + + ", holderClass=" + getHolderClass() + + '}'; + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/LambdaNonStaticException.java b/core/src/main/java/net/staticstudios/data/util/LambdaNonStaticException.java new file mode 100644 index 00000000..d1de5344 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/LambdaNonStaticException.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.util; + +public class LambdaNonStaticException extends RuntimeException { + public LambdaNonStaticException(String message) { + super(message); + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/LambdaUtils.java b/core/src/main/java/net/staticstudios/data/util/LambdaUtils.java new file mode 100644 index 00000000..0c1ec92f --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/LambdaUtils.java @@ -0,0 +1,43 @@ +package net.staticstudios.data.util; + +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.List; + +public class LambdaUtils { + + public static void assertLambdaDoesntCapture(Object lambda, @Nullable List> disallowedTypes, @Nullable String hint) { + Class lambdaClass = lambda.getClass(); + String message; + boolean allowed = true; + if (disallowedTypes == null) { + allowed = lambdaClass.getDeclaredFields().length == 0; + message = "Lambda expression captures variables from its enclosing scope. It must act as a static function. Did you reference 'this' or a member variable?"; + } else { + message = "Lambda expression captures disallowed variable types from its enclosing scope. Type that was captured: "; + for (Class disallowedType : disallowedTypes) { + for (Field field : lambdaClass.getDeclaredFields()) { + if (disallowedType.isAssignableFrom(field.getType())) { + allowed = false; + message += field.getType().getSimpleName(); + break; + } + } + if (!allowed) { + break; + } + } + } + if (!allowed) { + if (hint != null && !hint.isEmpty()) { + message += " Hint: " + hint; + } + throw new IllegalArgumentException(message); + } + } + + public static void assertLambdaDoesntCapture(Object lambda, @Nullable String hint) { + assertLambdaDoesntCapture(lambda, null, hint); + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/RedisUtils.java b/core/src/main/java/net/staticstudios/data/util/RedisUtils.java new file mode 100644 index 00000000..00ffb0dc --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/RedisUtils.java @@ -0,0 +1,69 @@ +package net.staticstudios.data.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class RedisUtils { + public static Pattern globToRegex(String redisPattern) { + StringBuilder regex = new StringBuilder(); + + for (int i = 0; i < redisPattern.length(); i++) { + char c = redisPattern.charAt(i); + switch (c) { + case '*' -> regex.append(".*"); + case '?' -> regex.append('.'); + case '[', ']' -> regex.append(c); + case '\\' -> regex.append("\\\\"); + default -> { + if ("+()^$.{}|".indexOf(c) != -1) { + regex.append('\\'); + } + regex.append(c); + } + } + } + + return Pattern.compile(regex.toString()); + } + + public static String buildRedisKey(String holderSchema, String holderTable, String identifier, ColumnValuePairs icColumns) { + //static-data:[schema]:[table]:[id column-value pairs, seperated by ':']:[identifier] + StringBuilder sb = new StringBuilder("static-data:"); + sb.append(holderSchema).append(":").append(holderTable).append(":"); + for (ColumnValuePair pair : icColumns) { + sb.append(pair.column()).append(":").append(pair.value()).append(":"); + } + sb.append(identifier); + return sb.toString(); + } + + public static String buildPartialRedisKey(String holderSchema, String holderTable, String identifier, List idColumnMetadata) { + StringBuilder sb = new StringBuilder("static-data:"); + sb.append(holderSchema).append(":").append(holderTable).append(":"); + for (ColumnMetadata idColumn : idColumnMetadata) { + sb.append(idColumn.name()).append(":").append("*").append(":"); + } + sb.append(identifier); + return sb.toString(); + } + + public static DeconstructedKey deconstruct(String key) { + List encodedIdValues = new ArrayList<>(); + List encodedIdNames = new ArrayList<>(); + String[] parts = key.split(":"); + StringBuilder sb = new StringBuilder(); + sb.append(parts[0]).append(":").append(parts[1]).append(":").append(parts[2]).append(":"); + for (int i = 3; i < parts.length - 1; i += 2) { + sb.append(parts[i]).append(":").append("*").append(":"); + encodedIdNames.add(parts[i]); + encodedIdValues.add(parts[i + 1]); + } + sb.append(parts[parts.length - 1]); + return new DeconstructedKey(sb.toString(), encodedIdNames, encodedIdValues); + } + + public record DeconstructedKey(String partialKey, List encodedIdNames, List encodedIdValues) { + + } +} diff --git a/core/src/main/java/net/staticstudios/data/SQLTransaction.java b/core/src/main/java/net/staticstudios/data/util/SQLTransaction.java similarity index 98% rename from core/src/main/java/net/staticstudios/data/SQLTransaction.java rename to core/src/main/java/net/staticstudios/data/util/SQLTransaction.java index abf6ba87..be187127 100644 --- a/core/src/main/java/net/staticstudios/data/SQLTransaction.java +++ b/core/src/main/java/net/staticstudios/data/util/SQLTransaction.java @@ -1,4 +1,4 @@ -package net.staticstudios.data; +package net.staticstudios.data.util; import com.google.common.base.Preconditions; import org.intellij.lang.annotations.Language; diff --git a/core/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java b/core/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java index 9f1f05d5..04465c7e 100644 --- a/core/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java @@ -8,6 +8,7 @@ public record UniqueDataMetadata(Class clazz, String schema, String table, List idColumns, + Map cachedValueMetadata, Map persistentValueMetadata, Map referenceMetadata, Map persistentCollectionMetadata) { diff --git a/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java deleted file mode 100644 index 71d73db9..00000000 --- a/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerNonStaticException.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.staticstudios.data.util; - -public class ValueUpdateHandlerNonStaticException extends RuntimeException { - public ValueUpdateHandlerNonStaticException(String message) { - super(message); - } -} 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 5526ff9b..9215590f 100644 --- a/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java +++ b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java @@ -10,17 +10,20 @@ public class ValueUpdateHandlerWrapper { private final Class holderClass; public ValueUpdateHandlerWrapper(ValueUpdateHandler handler, Class dataType, Class holderClass) { - if (handler.getClass().getDeclaredFields().length > 0) { - throw new ValueUpdateHandlerNonStaticException("Value update handler must not capture any variables! It must act as a static function. Did you reference 'this' or a member variable? Use the provided instance instead!"); - // 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. - } + 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; } + + public ValueUpdateHandler getHandler() { + return handler; + } + public Class getDataType() { return dataType; } diff --git a/core/src/test/java/net/staticstudios/data/CachedValueTest.java b/core/src/test/java/net/staticstudios/data/CachedValueTest.java new file mode 100644 index 00000000..ed5b145b --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/CachedValueTest.java @@ -0,0 +1,161 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +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 org.junit.jupiter.api.Test; +import redis.clients.jedis.Jedis; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class CachedValueTest extends DataTest { + + @Test + public void testBasic() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("john doe") + .insert(InsertMode.ASYNC); + + user.onCooldown.set(false); + assertEquals(false, user.onCooldown.get()); + + user.onCooldown.set(true); + assertEquals(true, user.onCooldown.get()); + + user.onCooldown.set(false); + assertEquals(false, user.onCooldown.get()); + } + + @Test + public void testFallback() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("john doe") + .insert(InsertMode.ASYNC); + + assertEquals(false, user.onCooldown.get()); + assertEquals(0, user.cooldownUpdates.get()); + + waitForDataPropagation(); + + Jedis jedis = getJedis(); + + String onCooldownKey = RedisUtils.buildRedisKey("public", "users", "on_cooldown", user.getIdColumns()); + String cooldownUpdatesKey = RedisUtils.buildRedisKey("public", "users", "cooldown_updates", user.getIdColumns()); + + assertNull(jedis.get(onCooldownKey)); + assertNull(jedis.get(cooldownUpdatesKey)); + } + + @Test + public void testUpdateHandler() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("john doe") + .insert(InsertMode.ASYNC); + + waitForUpdateHandlers(); + assertEquals(0, user.cooldownUpdates.get()); + + user.onCooldown.set(false); + assertEquals(false, user.onCooldown.get()); + + //the fallback value is false, so we didn't change anything. update handlers shouldn't be fired + waitForUpdateHandlers(); + assertEquals(0, user.cooldownUpdates.get()); + + + user.onCooldown.set(true); + assertEquals(true, user.onCooldown.get()); + + waitForUpdateHandlers(); + assertEquals(1, user.cooldownUpdates.get()); + + + user.onCooldown.set(true); + assertEquals(true, user.onCooldown.get()); + + //didnt change, so no update + waitForUpdateHandlers(); + assertEquals(1, user.cooldownUpdates.get()); + + + user.onCooldown.set(false); + assertEquals(false, user.onCooldown.get()); + + waitForUpdateHandlers(); + assertEquals(2, user.cooldownUpdates.get()); + } + + @Test + public void testUpdateRedis() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("john doe") + .insert(InsertMode.ASYNC); + + Jedis jedis = getJedis(); + + String onCooldownKey = RedisUtils.buildRedisKey("public", "users", "on_cooldown", user.getIdColumns()); + String cooldownUpdatesKey = RedisUtils.buildRedisKey("public", "users", "cooldown_updates", user.getIdColumns()); + + user.onCooldown.set(true); + waitForUpdateHandlers(); + user.cooldownUpdates.set(1); + waitForDataPropagation(); + assertEquals("true", jedis.get(onCooldownKey)); + assertEquals("1", jedis.get(cooldownUpdatesKey)); + + user.onCooldown.set(null); + waitForUpdateHandlers(); + user.cooldownUpdates.set(null); + waitForDataPropagation(); + assertNull(jedis.get(onCooldownKey)); + assertNull(jedis.get(cooldownUpdatesKey)); + + user.onCooldown.set(false); //fallback + waitForUpdateHandlers(); + user.cooldownUpdates.set(0); //fallback + waitForDataPropagation(); + assertNull(jedis.get(onCooldownKey)); + assertNull(jedis.get(cooldownUpdatesKey)); + } + + @Test + public void testLoadCachedValues() { + UUID userId = UUID.randomUUID(); + ColumnValuePairs columnValuePairs = new ColumnValuePairs(new ColumnValuePair("id", userId)); + + Jedis jedis = getJedis(); + + String onCooldownKey = RedisUtils.buildRedisKey("public", "users", "on_cooldown", columnValuePairs); + String cooldownUpdatesKey = RedisUtils.buildRedisKey("public", "users", "cooldown_updates", columnValuePairs); + + jedis.set(onCooldownKey, "true"); + jedis.set(cooldownUpdatesKey, "5"); + + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + MockUser user = MockUser.builder(dataManager) + .id(userId) + .name("john doe") + .insert(InsertMode.ASYNC); + + assertEquals(true, user.onCooldown.get()); + assertEquals(5, user.cooldownUpdates.get()); + } +} \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java index 51c7f461..cd5cc0f4 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -303,6 +303,5 @@ public void testEqualsAndHashCode() { } - //todo: test other collection types (one to many valued collections is all thats left. this ont to many valued collection is important since it is the simplest to use, and makes things like contains very straightforward.) //todo: test add/remove handlers } \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index d5f03251..9ecf55a2 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -8,8 +8,7 @@ // if the super class provides a data annotation, ignore it and use the child's annotation. it would be cool tho to allow the super class to use a @data annotation. the former is whats implemented now. if changed, update the processor. @Data(schema = "public", table = "users") public class MockUser extends UniqueData { - //todo: test inheritance properly - //todo: cached values + //todo: test inheritance properly. test the ij plugin and AP too. @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); @@ -56,6 +55,16 @@ public class MockUser extends UniqueData { @Delete(DeleteStrategy.CASCADE) @OneToMany(link = "id=user_id", table = "favorite_numbers", column = "number") public PersistentCollection favoriteNumbers; + @Identifier("cooldown_updates") + public CachedValue cooldownUpdates = CachedValue.of(this, Integer.class) + .withFallback(0); + @Identifier("on_cooldown") + @ExpireAfter(5) + public CachedValue onCooldown = CachedValue.of(this, Boolean.class) + .onUpdate(MockUser.class, (user, update) -> { + user.cooldownUpdates.set(user.cooldownUpdates.get() + 1); + }) + .withFallback(false); public int getNameUpdates() { return nameUpdates.get(); From 82f141e02f1fc8df7386dc9c7eb42326be4a4c41 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 11 Nov 2025 14:05:41 -0500 Subject: [PATCH 47/75] added collection add & remove handlers, added snapshots --- .../net/staticstudios/data/CachedValue.java | 6 +- .../net/staticstudios/data/DataManager.java | 449 +++++++++++++++++- .../data/PersistentCollection.java | 54 ++- .../staticstudios/data/PersistentValue.java | 2 +- .../net/staticstudios/data/Reference.java | 2 + .../net/staticstudios/data/UniqueData.java | 14 +- .../data/impl/data/AbstractCachedValue.java | 20 + .../data/impl/data/CachedValueImpl.java | 10 +- .../PersistentManyToManyCollectionImpl.java | 231 ++++----- .../PersistentOneToManyCollectionImpl.java | 93 ++-- ...ersistentOneToManyValueCollectionImpl.java | 20 +- .../data/impl/data/ReadOnlyCachedValue.java | 87 ++++ .../impl/data/ReadOnlyPersistentValue.java | 81 ++++ .../data/impl/data/ReadOnlyReference.java | 78 +++ .../data/ReadOnlyReferenceCollection.java | 222 +++++++++ .../impl/data/ReadOnlyValuedCollection.java | 142 ++++++ .../data/impl/data/ReferenceImpl.java | 10 +- .../data/impl/h2/H2DataAccessor.java | 11 +- .../h2/trigger/H2UpdateHandlerTrigger.java | 6 + .../staticstudios/data/parse/SQLBuilder.java | 10 + .../data/primative/Primitive.java | 12 +- .../data/primative/PrimitiveBuilder.java | 9 +- .../data/primative/Primitives.java | 17 + .../data/util/CollectionChangeHandler.java | 13 + .../util/CollectionChangeHandlerWrapper.java | 71 +++ .../util/PersistentCollectionMetadata.java | 3 + ...ersistentManyToManyCollectionMetadata.java | 73 ++- ...PersistentOneToManyCollectionMetadata.java | 24 +- ...stentOneToManyValueCollectionMetadata.java | 10 +- .../staticstudios/data/util/TriggerCause.java | 7 + .../PersistentManyToManyCollectionTest.java | 96 ++++ .../PersistentOneToManyCollectionTest.java | 85 +++- ...ersistentOneToManyValueCollectionTest.java | 122 +++++ .../net/staticstudios/data/SnapshotTest.java | 54 +++ .../data/mock/user/MockUser.java | 33 +- 35 files changed, 1914 insertions(+), 263 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/impl/data/AbstractCachedValue.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyPersistentValue.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReference.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReferenceCollection.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyValuedCollection.java create mode 100644 core/src/main/java/net/staticstudios/data/util/CollectionChangeHandler.java create mode 100644 core/src/main/java/net/staticstudios/data/util/CollectionChangeHandlerWrapper.java create mode 100644 core/src/main/java/net/staticstudios/data/util/TriggerCause.java create mode 100644 core/src/test/java/net/staticstudios/data/SnapshotTest.java diff --git a/core/src/main/java/net/staticstudios/data/CachedValue.java b/core/src/main/java/net/staticstudios/data/CachedValue.java index af52e133..224d4780 100644 --- a/core/src/main/java/net/staticstudios/data/CachedValue.java +++ b/core/src/main/java/net/staticstudios/data/CachedValue.java @@ -2,7 +2,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Supplier; -import net.staticstudios.data.impl.data.CachedValueImpl; +import net.staticstudios.data.impl.data.AbstractCachedValue; import net.staticstudios.data.util.*; import org.jetbrains.annotations.Nullable; @@ -44,7 +44,7 @@ public ProxyCachedValue(UniqueData holder, Class dataType) { this.dataType = dataType; } - public void setDelegate(CachedValueMetadata metadata, CachedValueImpl delegate) { + public void setDelegate(CachedValueMetadata metadata, AbstractCachedValue delegate) { Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); delegate.setFallback(this.fallback); @@ -56,7 +56,7 @@ public void setDelegate(CachedValueMetadata metadata, CachedValueImpl delegat cachedValueUpdateHandlers.add(asCachedValueHandler(handler)); } - holder.getDataManager().registerCachedValueUpdateHandler(metadata, cachedValueUpdateHandlers); + holder.getDataManager().registerCachedValueUpdateHandlers(metadata, cachedValueUpdateHandlers); } @Override diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 8c2b4117..e26c62b0 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -16,6 +16,7 @@ import net.staticstudios.utils.ThreadUtils; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,10 +41,12 @@ public class DataManager { private final ConcurrentHashMap, Map> uniqueDataInstanceCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap, List>>> persistentValueUpdateHandlers = new ConcurrentHashMap<>(); private final ConcurrentHashMap, List>>> cachedValueUpdateHandlers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap, List>>> collectionChangeHandlers = new ConcurrentHashMap<>(); private final PostgresListener postgresListener; private final RedisListener redisListener; private final Set registeredUpdateHandlersForColumns = ConcurrentHashMap.newKeySet(); private final Set registeredUpdateHandlersForRedis = ConcurrentHashMap.newKeySet(); + private final Set registeredChangeHandlersForCollection = ConcurrentHashMap.newKeySet(); private final List> valueSerializers = new CopyOnWriteArrayList<>(); //todo: custom types are serialized and deserialized all the time currently, we should have a cache for these. caffeine with time based eviction sounds good. @@ -139,9 +142,7 @@ public void callPersistentValueUpdateHandlers(List columnNames, String s Class dataType = wrapper.getDataType(); Object deserializedOldValue = deserialize(dataType, oldSerializedValues[columnIndex]); Object deserializedNewValue = deserialize(dataType, newSerializedValues[columnIndex]); - - //todo: allow configuring where to submit update handlers to. note that we cannot call them immediately since we are inside a transaction. - ThreadUtils.submit(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); + submitUpdateHandler(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); } } } @@ -193,13 +194,331 @@ public void callCachedValueUpdateHandlers(String partialKey, List encode Object decodedNewValue = Primitives.decode(serializedType, newValue); deserializedNewValue = deserialize(wrapper.getDataType(), decodedNewValue); } - ThreadUtils.submit(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); //todo: submit somewher based on config + submitUpdateHandler(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); } } } @ApiStatus.Internal - public void registerPersistentValueUpdateHandler(PersistentValueMetadata metadata, Collection> handlers) { + public void callCollectionChangeHandlers(List columnNames, String schema, String table, List changedColumns, Object[] oldSerializedValues, Object[] newSerializedValues, TriggerCause cause) { + logger.trace("Calling collection change handlers for {}.{} on changed columns {} with old values {} and new values {}", schema, table, changedColumns, Arrays.toString(oldSerializedValues), Arrays.toString(newSerializedValues)); + Map, List>> handlersForTable = collectionChangeHandlers.get(schema + "." + table); + if (handlersForTable == null) { + return; + } + + for (Map.Entry, List>> entry : handlersForTable.entrySet()) { + List> handlers = entry.getValue(); + for (CollectionChangeHandlerWrapper wrapper : handlers) { + PersistentCollectionMetadata collectionMetadata = wrapper.getCollectionMetadata(); + Preconditions.checkNotNull(collectionMetadata, "Collection metadata not set for collection change handler"); + switch (collectionMetadata) { + case PersistentOneToManyCollectionMetadata oneToManyCollectionMetadata -> + handleOneToManyCollectionChange(wrapper, oneToManyCollectionMetadata, columnNames, oldSerializedValues, newSerializedValues, cause); + case PersistentOneToManyValueCollectionMetadata oneToManyValueCollectionMetadata -> + handleOneToManyValuedCollectionChange(wrapper, oneToManyValueCollectionMetadata, columnNames, oldSerializedValues, newSerializedValues); + case PersistentManyToManyCollectionMetadata manyToManyCollectionMetadata -> + handleManyToManyCollectionChange(wrapper, manyToManyCollectionMetadata, columnNames, oldSerializedValues, newSerializedValues, cause); + default -> + throw new IllegalStateException("Unknown collection metadata type: " + collectionMetadata.getClass().getName()); + } + } + } + } + + private void handleOneToManyCollectionChange(CollectionChangeHandlerWrapper handler, PersistentOneToManyCollectionMetadata metadata, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues, TriggerCause cause) { + List links = metadata.getLinks(); + Object[] oldLinkValues = new Object[links.size()]; + Object[] newLinkValues = new Object[links.size()]; + boolean differenceFound = false; + for (int i = 0; i < links.size(); i++) { + Link link = links.get(i); + int columnIndex = columnNames.indexOf(link.columnInReferencedTable()); + Preconditions.checkArgument(columnIndex != -1, "Column %s not found in provided name names %s", link.columnInReferencedTable(), columnNames); + oldLinkValues[i] = oldSerializedValues[columnIndex]; + newLinkValues[i] = newSerializedValues[columnIndex]; + if (!Objects.equals(oldLinkValues[i], newLinkValues[i])) { + differenceFound = true; + } + } + + if (!differenceFound) { + return; + } + + UniqueDataMetadata referencedMetadata = getMetadata(metadata.getReferencedType()); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + sqlBuilder.append("\"").append(idColumn.name()).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(referencedMetadata.schema()).append("\".\"").append(referencedMetadata.table()).append("\" WHERE "); + List oldValues = new ArrayList<>(); + List newValues = new ArrayList<>(); + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + if (!oldValues.isEmpty()) { + sqlBuilder.append(" AND "); + } + sqlBuilder.append("\"").append(idColumn.name()).append("\" = ? "); + int columnIndex = columnNames.indexOf(idColumn.name()); + Object oldDeserializedValue = deserialize(idColumn.type(), oldSerializedValues[columnIndex]); + oldValues.add(oldDeserializedValue); + Object newDeserializedValue = deserialize(idColumn.type(), newSerializedValues[columnIndex]); + newValues.add(newDeserializedValue); + } + + UniqueData instance = getInstanceForCollectionChangeHandler(metadata.getHolderClass(), links, columnNames, oldSerializedValues, newSerializedValues, handler); + if (instance == null) { + return; + } + + if (handler.getType() == CollectionChangeHandlerWrapper.Type.REMOVE) { + try (ResultSet rs = dataAccessor.executeQuery(sqlBuilder.toString(), oldValues)) { + if (rs.next()) { + ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + Object serializedValue = rs.getObject(idColumn.name()); + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + UniqueData oldInstance; + if (cause == TriggerCause.DELETE) { + //the handler probably cares about the data that was removed, so we create a snapshot since the actual data is no longer available + oldInstance = createSnapshot(referencedMetadata.clazz(), new ColumnValuePairs(idColumns)); + } else { + oldInstance = getInstance(referencedMetadata.clazz(), idColumns); + } + if (oldInstance != null) { + submitUpdateHandler(() -> handler.unsafeHandle(instance, oldInstance)); + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + if (handler.getType() == CollectionChangeHandlerWrapper.Type.ADD) { + try (ResultSet rs = dataAccessor.executeQuery(sqlBuilder.toString(), newValues)) { + if (rs.next()) { + ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + Object serializedValue = rs.getObject(idColumn.name()); + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + UniqueData newInstance = getInstance(referencedMetadata.clazz(), idColumns); + if (newInstance != null) { + submitUpdateHandler(() -> handler.unsafeHandle(instance, newInstance)); + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } + + private void handleOneToManyValuedCollectionChange(CollectionChangeHandlerWrapper handler, PersistentOneToManyValueCollectionMetadata metadata, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues) { + List links = metadata.getLinks(); + Object[] oldLinkValues = new Object[links.size()]; + Object[] newLinkValues = new Object[links.size()]; + boolean differenceFound = false; + for (int i = 0; i < links.size(); i++) { + Link link = links.get(i); + int columnIndex = columnNames.indexOf(link.columnInReferencedTable()); + Preconditions.checkArgument(columnIndex != -1, "Column %s not found in provided name names %s", link.columnInReferencedTable(), columnNames); + oldLinkValues[i] = oldSerializedValues[columnIndex]; + newLinkValues[i] = newSerializedValues[columnIndex]; + if (!Objects.equals(oldLinkValues[i], newLinkValues[i])) { + differenceFound = true; + } + } + + if (!differenceFound) { + return; + } + + UniqueData instance = getInstanceForCollectionChangeHandler(metadata.getHolderClass(), links, columnNames, oldSerializedValues, newSerializedValues, handler); + + if (instance == null) { + return; + } + + int columnIndex = columnNames.indexOf(metadata.getDataColumn()); + if (handler.getType() == CollectionChangeHandlerWrapper.Type.REMOVE) { + Object oldSerializedValue = oldSerializedValues[columnIndex]; + Object deserializedOldValue = deserialize(metadata.getDataType(), oldSerializedValue); + submitUpdateHandler(() -> handler.unsafeHandle(instance, deserializedOldValue)); + } + + if (handler.getType() == CollectionChangeHandlerWrapper.Type.ADD) { + Object newSerializedValue = newSerializedValues[columnIndex]; + Object deserializedNewValue = deserialize(metadata.getDataType(), newSerializedValue); + submitUpdateHandler(() -> handler.unsafeHandle(instance, deserializedNewValue)); + } + } + + private void handleManyToManyCollectionChange(CollectionChangeHandlerWrapper handler, PersistentManyToManyCollectionMetadata metadata, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues, TriggerCause cause) { + List links = metadata.getJoinTableToReferencedTableLinks(this); + Object[] oldLinkValues = new Object[links.size()]; + Object[] newLinkValues = new Object[links.size()]; + boolean differenceFound = false; + for (int i = 0; i < links.size(); i++) { + Link link = links.get(i); + int columnIndex = columnNames.indexOf(link.columnInReferringTable()); + Preconditions.checkArgument(columnIndex != -1, "Column %s not found in provided name names %s", link.columnInReferringTable(), columnNames); + oldLinkValues[i] = oldSerializedValues[columnIndex]; + newLinkValues[i] = newSerializedValues[columnIndex]; + if (!Objects.equals(oldLinkValues[i], newLinkValues[i])) { + differenceFound = true; + } + } + + if (!differenceFound) { + return; + } + + UniqueDataMetadata referencedMetadata = getMetadata(metadata.getReferencedType()); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + sqlBuilder.append("\"").append(idColumn.name()).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(referencedMetadata.schema()).append("\".\"").append(referencedMetadata.table()).append("\" WHERE "); + List oldValues = new ArrayList<>(); + List newValues = new ArrayList<>(); + SQLTable referencedTable = Objects.requireNonNull(this.sqlBuilder.getSchema(referencedMetadata.schema())).getTable(referencedMetadata.table()); + Preconditions.checkNotNull(referencedTable, "Referenced table %s.%s not found", referencedMetadata.schema(), referencedMetadata.table()); + for (Link link : metadata.getJoinTableToReferencedTableLinks(this)) { + if (!oldValues.isEmpty()) { + sqlBuilder.append(" AND "); + } + String columnInJoinTable = link.columnInReferringTable(); + String columnInReferencedTable = link.columnInReferencedTable(); + sqlBuilder.append("\"").append(columnInReferencedTable).append("\" = ? "); + Class columnType = null; + for (SQLColumn column : referencedTable.getColumns()) { + if (column.getName().equals(columnInReferencedTable)) { + columnType = column.getType(); + break; + } + } + Preconditions.checkNotNull(columnType, "Could not find column %s in referenced table %s.%s", columnInReferencedTable, referencedMetadata.schema(), referencedMetadata.table()); + int columnIndex = columnNames.indexOf(columnInJoinTable); + Object oldDeserializedValue = deserialize(columnType, oldSerializedValues[columnIndex]); + oldValues.add(oldDeserializedValue); + Object newDeserializedValue = deserialize(columnType, newSerializedValues[columnIndex]); + newValues.add(newDeserializedValue); + } + + UniqueData instance = getInstanceForCollectionChangeHandler(metadata.getHolderClass(), + metadata.getJoinTableToDataTableLinks(this).stream().map(link -> new Link(link.columnInReferringTable(), link.columnInReferencedTable())).toList(), //reverse since the method expects the referenced table to be the join table + columnNames, oldSerializedValues, newSerializedValues, handler); + if (instance == null) { + return; + } + + if (handler.getType() == CollectionChangeHandlerWrapper.Type.REMOVE) { + try (ResultSet rs = dataAccessor.executeQuery(sqlBuilder.toString(), oldValues)) { + if (rs.next()) { + ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + Object serializedValue = rs.getObject(idColumn.name()); + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + UniqueData oldInstance; + if (cause == TriggerCause.DELETE) { + //the handler probably cares about the data that was removed, so we create a snapshot since the actual data is no longer available + oldInstance = createSnapshot(referencedMetadata.clazz(), new ColumnValuePairs(idColumns)); + } else { + oldInstance = getInstance(referencedMetadata.clazz(), idColumns); + } + if (oldInstance != null) { + submitUpdateHandler(() -> handler.unsafeHandle(instance, oldInstance)); + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + if (handler.getType() == CollectionChangeHandlerWrapper.Type.ADD) { + try (ResultSet rs = dataAccessor.executeQuery(sqlBuilder.toString(), newValues)) { + if (rs.next()) { + ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + Object serializedValue = rs.getObject(idColumn.name()); + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + UniqueData newInstance = getInstance(referencedMetadata.clazz(), idColumns); + if (newInstance != null) { + submitUpdateHandler(() -> handler.unsafeHandle(instance, newInstance)); + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } + + private UniqueData getInstanceForCollectionChangeHandler(Class holderClass, List links, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues, CollectionChangeHandlerWrapper handler) { + UniqueData instance = null; + UniqueDataMetadata uniqueDataMetadata = getMetadata(holderClass); + + StringBuilder instanceSqlBuilder = new StringBuilder(); + instanceSqlBuilder.append("SELECT "); + for (ColumnMetadata idColumn : uniqueDataMetadata.idColumns()) { + instanceSqlBuilder.append("\"").append(idColumn.name()).append("\", "); + } + instanceSqlBuilder.setLength(instanceSqlBuilder.length() - 2); + instanceSqlBuilder.append(" FROM \"").append(uniqueDataMetadata.schema()).append("\".\"").append(uniqueDataMetadata.table()).append("\" WHERE "); + List instanceValues = new ArrayList<>(); + for (Link link : links) { + int columnIndex = columnNames.indexOf(link.columnInReferencedTable()); + Preconditions.checkArgument(columnIndex != -1, "Column %s not found in provided name names %s", link.columnInReferencedTable(), columnNames); + if (!instanceValues.isEmpty()) { + instanceSqlBuilder.append(" AND "); + } + instanceSqlBuilder.append("\"").append(link.columnInReferringTable()).append("\" = ? "); + Class valueType = null; + for (ColumnMetadata idColumn : uniqueDataMetadata.idColumns()) { + if (idColumn.name().equals(link.columnInReferringTable())) { + valueType = idColumn.type(); + break; + } + } + Preconditions.checkNotNull(valueType, "Could not find column %s in holder UniqueData class %s", link.columnInReferringTable(), uniqueDataMetadata.clazz().getName()); + Object deserializedValue = handler.getType() == CollectionChangeHandlerWrapper.Type.ADD + ? deserialize(valueType, newSerializedValues[columnNames.indexOf(link.columnInReferencedTable())]) + : deserialize(valueType, oldSerializedValues[columnNames.indexOf(link.columnInReferencedTable())]); + instanceValues.add(deserializedValue); + } + try (ResultSet rs = dataAccessor.executeQuery(instanceSqlBuilder.toString(), instanceValues)) { + if (rs.next()) { + ColumnValuePair[] idColumns = new ColumnValuePair[uniqueDataMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : uniqueDataMetadata.idColumns()) { + Object serializedValue = rs.getObject(idColumn.name()); + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[uniqueDataMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + instance = getInstance(uniqueDataMetadata.clazz(), idColumns); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return instance; + } + + private void submitUpdateHandler(Runnable runnable) { + ThreadUtils.submit(runnable); //todo: submit somewhere based on config + } + + @ApiStatus.Internal + public void registerPersistentValueUpdateHandlers(PersistentValueMetadata metadata, Collection> handlers) { if (registeredUpdateHandlersForColumns.add(metadata)) { for (ValueUpdateHandlerWrapper handler : handlers) { addUpdateHandler(metadata.getSchema(), metadata.getTable(), metadata.getColumn(), handler); @@ -208,7 +527,7 @@ public void registerPersistentValueUpdateHandler(PersistentValueMetadata metadat } @ApiStatus.Internal - public void registerCachedValueUpdateHandler(CachedValueMetadata metadata, Collection> handlers) { + public void registerCachedValueUpdateHandlers(CachedValueMetadata metadata, Collection> handlers) { if (registeredUpdateHandlersForRedis.add(metadata)) { UniqueDataMetadata holderMetadata = getMetadata(metadata.holderClass()); String partialKey = RedisUtils.buildPartialRedisKey(metadata.holderSchema(), metadata.holderTable(), metadata.identifier(), holderMetadata.idColumns()); @@ -218,6 +537,28 @@ public void registerCachedValueUpdateHandler(CachedValueMetadata metadata, Colle } } + @ApiStatus.Internal + public void registerCollectionChangeHandlers(PersistentCollectionMetadata metadata, Collection> handlers) { + if (registeredChangeHandlersForCollection.add(metadata)) { + handlers.forEach(h -> h.setCollectionMetadata(metadata)); + String key = switch (metadata) { + case PersistentOneToManyCollectionMetadata oneToManyCollectionMetadata -> { + UniqueDataMetadata referencedMetadata = getMetadata(oneToManyCollectionMetadata.getReferencedType()); + yield referencedMetadata.schema() + "." + referencedMetadata.table(); + } + case PersistentOneToManyValueCollectionMetadata oneToManyValueCollectionMetadata -> + oneToManyValueCollectionMetadata.getDataSchema() + "." + oneToManyValueCollectionMetadata.getDataTable(); + case PersistentManyToManyCollectionMetadata manyToManyCollectionMetadata -> + manyToManyCollectionMetadata.getJoinTableSchema(this) + "." + manyToManyCollectionMetadata.getJoinTableName(this); + default -> + throw new IllegalStateException("Unknown PersistentCollectionMetadata type: " + metadata.getClass().getName()); + }; + collectionChangeHandlers.computeIfAbsent(key, k -> new ConcurrentHashMap<>()) + .computeIfAbsent(metadata.getHolderClass(), k -> new CopyOnWriteArrayList<>()) + .addAll(handlers); + } + } + public List> getUpdateHandlers(String schema, String table, String column, Class holderClass) { String key = schema + "." + table + "." + column; if (persistentValueUpdateHandlers.containsKey(key) && persistentValueUpdateHandlers.get(key).containsKey(holderClass)) { @@ -285,7 +626,7 @@ public UniqueDataMetadata extractMetadata(Class clazz) { String schema = ValueUtils.parseValue(dataAnnotation.schema()); String table = ValueUtils.parseValue(dataAnnotation.table()); Map persistentCollectionMetadataMap = new HashMap<>(); - persistentCollectionMetadataMap.putAll(PersistentOneToManyCollectionImpl.extractMetadata(clazz)); + persistentCollectionMetadataMap.putAll(PersistentOneToManyCollectionImpl.extractMetadata(this, clazz)); persistentCollectionMetadataMap.putAll(PersistentManyToManyCollectionImpl.extractMetadata(clazz)); persistentCollectionMetadataMap.putAll(PersistentOneToManyValueCollectionImpl.extractMetadata(clazz, schema)); UniqueDataMetadata metadata = new UniqueDataMetadata( @@ -432,9 +773,12 @@ public List query(Class clazz, String where, List T getInstance(Class clazz, ColumnValuePair... idColumnValues) { - ColumnValuePairs idColumns = new ColumnValuePairs(idColumnValues); + return getInstance(clazz, new ColumnValuePairs(idColumnValues)); + } + + @SuppressWarnings("unchecked") + public T getInstance(Class clazz, @NotNull ColumnValuePairs idColumns) { UniqueDataMetadata metadata = getMetadata(clazz); Preconditions.checkNotNull(metadata, "UniqueData class %s has not been parsed yet", clazz.getName()); boolean hasAllIdColumns = true; @@ -476,7 +820,7 @@ public T getInstance(Class clazz, ColumnValuePair... i throw new RuntimeException(e); } - instance.setDataManager(this); + instance.setDataManager(this, false); instance.setIdColumns(idColumns); String schema = metadata.schema(); @@ -512,6 +856,84 @@ public T getInstance(Class clazz, ColumnValuePair... i return instance; } + /** + * Creates a snapshot of the given UniqueData instance. + * The snapshot instance will have the same ID columns as the original instance, + * but it's values will be read-only and represent the state of the data at the time of snapshot creation. + * + * @param instance the UniqueData instance to create a snapshot of + * @param the type of UniqueData + * @return a snapshot UniqueData instance + */ + @SuppressWarnings("unchecked") + public T createSnapshot(T instance) { + return createSnapshot((Class) instance.getClass(), instance.getIdColumns()); + } + + private T createSnapshot(Class clazz, ColumnValuePairs idColumns) { + UniqueDataMetadata metadata = getMetadata(clazz); + Preconditions.checkNotNull(metadata, "UniqueData class %s has not been parsed yet", clazz.getName()); + boolean hasAllIdColumns = true; + for (ColumnMetadata idColumn : metadata.idColumns()) { + boolean found = false; + for (ColumnValuePair providedIdColumn : idColumns) { + if (idColumn.name().equals(providedIdColumn.column())) { + found = true; + break; + } + } + if (!found) { + hasAllIdColumns = false; + break; + } + } + + for (ColumnValuePair providedIdColumn : idColumns) { + Preconditions.checkNotNull(providedIdColumn.value(), "ID column value for column %s in UniqueData class %s cannot be null", providedIdColumn.column(), clazz.getName()); + } + + Preconditions.checkArgument(hasAllIdColumns, "Not all @IdColumn columnsInReferringTable were provided for UniqueData class %s. Required: %s, Provided: %s", clazz.getName(), metadata.idColumns(), idColumns); + T instance; + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + instance = constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + instance.setDataManager(this, true); + instance.setIdColumns(idColumns); + + String schema = metadata.schema(); + String table = metadata.table(); + + 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 "); + } + 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; + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + ReadOnlyPersistentValue.delegate(instance); + ReadOnlyCachedValue.delegate(instance); + ReadOnlyReference.delegate(instance); + ReadOnlyValuedCollection.delegate(instance); + ReadOnlyReferenceCollection.delegate(instance); + + logger.trace("Created snapshot for UniqueData class {} with ID columnsInReferringTable {}", clazz.getName(), idColumns); + + return instance; + } + public void insert(InsertContext insertContext, InsertMode insertMode) { //todo: when inserting validate all id and required values are present - this will be enforced by h2, but we should do it here for better logging/errors. Set tables = new HashSet<>(); @@ -928,6 +1350,13 @@ public Class getSerializedType(Class clazz) { return serializer.getSerializedType(); } + public T copy(T value, Class dataType) { + if (Primitives.isPrimitive(dataType)) { + return Primitives.copy(value, dataType); + } + return deserialize(dataType, serialize(value)); + } + // /** // * For internal use only. A dummy instance has no DataManager, no id columnsInReferringTable, and is marked as deleted. // * diff --git a/core/src/main/java/net/staticstudios/data/PersistentCollection.java b/core/src/main/java/net/staticstudios/data/PersistentCollection.java index 02ed20a9..ca04779c 100644 --- a/core/src/main/java/net/staticstudios/data/PersistentCollection.java +++ b/core/src/main/java/net/staticstudios/data/PersistentCollection.java @@ -1,13 +1,18 @@ 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 org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.AccessFlag; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; public interface PersistentCollection extends Collection, Relation { @@ -15,13 +20,16 @@ static PersistentCollection of(UniqueData holder, Class referenceType) return new ProxyPersistentCollection<>(holder, referenceType); } - //todo: add and remove handlers + PersistentCollection onAdd(Class holderClass, CollectionChangeHandler addHandler); + + PersistentCollection onRemove(Class holderClass, CollectionChangeHandler removeHandler); UniqueData getHolder(); class ProxyPersistentCollection implements PersistentCollection { private final UniqueData holder; private final Class referenceType; + private final List> changeHandlers = new ArrayList<>(); private @Nullable PersistentCollection delegate; public ProxyPersistentCollection(UniqueData holder, Class referenceType) { @@ -30,18 +38,31 @@ public ProxyPersistentCollection(UniqueData holder, Class referenceType) { this.referenceType = referenceType; } + @Override + public PersistentCollection onAdd(Class holderClass, CollectionChangeHandler addHandler) { + changeHandlers.add(new CollectionChangeHandlerWrapper<>(addHandler, referenceType, holder.getClass(), CollectionChangeHandlerWrapper.Type.ADD)); + return this; + } + + @Override + public PersistentCollection onRemove(Class holderClass, CollectionChangeHandler removeHandler) { + changeHandlers.add(new CollectionChangeHandlerWrapper<>(removeHandler, referenceType, holder.getClass(), CollectionChangeHandlerWrapper.Type.REMOVE)); + return this; + } + @Override public UniqueData getHolder() { return holder; } - public Class getReferenceType() { + public Class getDataType() { return referenceType; } - public void setDelegate(PersistentCollection delegate) { + public void setDelegate(PersistentCollectionMetadata metadata, PersistentCollection delegate) { Preconditions.checkState(this.delegate == null, "Delegate has already been set"); this.delegate = delegate; + holder.getDataManager().registerCollectionChangeHandlers(metadata, changeHandlers); } @Override @@ -121,5 +142,32 @@ public void clear() { Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); delegate.clear(); } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + PersistentCollection delegate = null; + if (obj instanceof PersistentCollection.ProxyPersistentCollection proxyPersistentCollection) { + delegate = proxyPersistentCollection.delegate; + } else if (obj instanceof PersistentCollection persistentCollection) { + delegate = persistentCollection; + } + + Preconditions.checkState(this.delegate != null, "PersistentCollection has not been initialized yet"); + + return this.delegate.equals(delegate); + } + + @Override + public int hashCode() { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.hashCode(); + } + + @Override + public String toString() { + Preconditions.checkState(delegate != null, "PersistentCollection has not been initialized yet"); + return delegate.toString(); + } } } diff --git a/core/src/main/java/net/staticstudios/data/PersistentValue.java b/core/src/main/java/net/staticstudios/data/PersistentValue.java index a64868af..a56328c6 100644 --- a/core/src/main/java/net/staticstudios/data/PersistentValue.java +++ b/core/src/main/java/net/staticstudios/data/PersistentValue.java @@ -43,7 +43,7 @@ public void setDelegate(PersistentValueMetadata metadata, PersistentValue del Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); this.delegate = delegate; - holder.getDataManager().registerPersistentValueUpdateHandler(metadata, updateHandlers); + holder.getDataManager().registerPersistentValueUpdateHandlers(metadata, updateHandlers); } @Override diff --git a/core/src/main/java/net/staticstudios/data/Reference.java b/core/src/main/java/net/staticstudios/data/Reference.java index 40c4aad1..e03c9ebb 100644 --- a/core/src/main/java/net/staticstudios/data/Reference.java +++ b/core/src/main/java/net/staticstudios/data/Reference.java @@ -20,6 +20,8 @@ static Reference of(UniqueData holder, Class refere void set(@Nullable T value); + //todo: support update handlers + class ProxyReference implements Reference { private final UniqueData holder; private final Class referenceType; diff --git a/core/src/main/java/net/staticstudios/data/UniqueData.java b/core/src/main/java/net/staticstudios/data/UniqueData.java index 51451e75..996b5d7c 100644 --- a/core/src/main/java/net/staticstudios/data/UniqueData.java +++ b/core/src/main/java/net/staticstudios/data/UniqueData.java @@ -11,15 +11,18 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public abstract class UniqueData { private ColumnValuePairs idColumns; private DataManager dataManager; private volatile boolean isDeleted = false; + private boolean isSnapshot = false; @ApiStatus.Internal - protected final void setDataManager(DataManager dataManager) { + protected final void setDataManager(DataManager dataManager, boolean isSnapshot) { this.dataManager = dataManager; + this.isSnapshot = isSnapshot; } @ApiStatus.Internal @@ -67,6 +70,10 @@ public synchronized void delete() { } } + public final boolean isSnapshot() { + return isSnapshot; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -79,6 +86,9 @@ public String toString() { if (isDeleted) { sb.append("deleted=true, "); } + if (isSnapshot) { + sb.append("snapshot=true, "); + } sb.append("dataManager=").append(dataManager); sb.append("}"); return sb.toString(); @@ -86,7 +96,7 @@ public String toString() { @Override public final int hashCode() { - return idColumns.hashCode(); + return Objects.hash(dataManager, idColumns); } @Override diff --git a/core/src/main/java/net/staticstudios/data/impl/data/AbstractCachedValue.java b/core/src/main/java/net/staticstudios/data/impl/data/AbstractCachedValue.java new file mode 100644 index 00000000..1a95d688 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/AbstractCachedValue.java @@ -0,0 +1,20 @@ +package net.staticstudios.data.impl.data; + +import net.staticstudios.data.CachedValue; + +import java.util.function.Supplier; + +public abstract class AbstractCachedValue implements CachedValue { + private Supplier fallbackSupplier; + + public void setFallback(Supplier fallbackSupplier) { + this.fallbackSupplier = fallbackSupplier; + } + + protected T getFallback() { + if (fallbackSupplier != null) { + return fallbackSupplier.get(); + } + return null; + } +} 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 0f01b6da..7d09804a 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 @@ -14,11 +14,10 @@ import java.util.Map; import java.util.Objects; -public class CachedValueImpl implements CachedValue { +public class CachedValueImpl extends AbstractCachedValue { private final UniqueData holder; private final Class dataType; private final CachedValueMetadata metadata; - private Supplier fallback; private CachedValueImpl(UniqueData holder, Class dataType, CachedValueMetadata metadata) { this.holder = holder; @@ -80,9 +79,6 @@ public static CachedValueMetadata extractMetadata(String return new CachedValueMetadata(clazz, holderSchema, holderTable, ValueUtils.parseValue(identifierAnnotation.value()), expireAfterSeconds); } - public void setFallback(Supplier fallback) { - this.fallback = fallback; - } @Override public UniqueData getHolder() { @@ -109,7 +105,7 @@ public T get() { Preconditions.checkArgument(!holder.isDeleted(), "Cannot get value from a deleted UniqueData instance"); T value = holder.getDataManager().getRedis(metadata.holderSchema(), metadata.holderTable(), metadata.identifier(), holder.getIdColumns(), dataType); if (value == null) { - return fallback.get(); + return getFallback(); } return value; } @@ -117,7 +113,7 @@ public T get() { @Override public void set(@Nullable T value) { Preconditions.checkArgument(!holder.isDeleted(), "Cannot set value on a deleted UniqueData instance"); - T fallback = this.fallback.get(); + T fallback = getFallback(); T toSet; if (Objects.equals(fallback, value)) { toSet = null; 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 931e161e..8dbbfc6f 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 @@ -21,28 +21,22 @@ public class PersistentManyToManyCollectionImpl implements PersistentCollection { private final UniqueData holder; private final Class type; - private final String parsedJoinTableSchema; - private final String parsedJoinTableName; - private final String links; // since we need information about the column prefixes in the join referringTable, we have to compute these at runtime - private @Nullable List cachedJoinTableToDataTableLinks = null; - private @Nullable List cachedJoinTableToReferencedTableLinks = null; + private final PersistentManyToManyCollectionMetadata metadata; - public PersistentManyToManyCollectionImpl(UniqueData holder, Class type, String parsedJoinTableSchema, String parsedJoinTableName, String links) { + @SuppressWarnings("unchecked") + public PersistentManyToManyCollectionImpl(UniqueData holder, PersistentManyToManyCollectionMetadata metadata) { this.holder = holder; - this.type = type; - this.parsedJoinTableSchema = parsedJoinTableSchema; - this.parsedJoinTableName = parsedJoinTableName; - this.links = links; + this.type = (Class) metadata.getReferencedType(); + this.metadata = metadata; } public static void createAndDelegate(ProxyPersistentCollection proxy, PersistentManyToManyCollectionMetadata metadata) { - PersistentManyToManyCollectionImpl impl = new PersistentManyToManyCollectionImpl<>(proxy.getHolder(), proxy.getReferenceType(), metadata.getParsedJoinTableSchema(), metadata.getParsedJoinTableName(), metadata.getLinks()); - proxy.setDelegate(impl); + PersistentManyToManyCollectionImpl impl = new PersistentManyToManyCollectionImpl<>(proxy.getHolder(), metadata); + proxy.setDelegate(metadata, impl); } - @SuppressWarnings("unchecked") public static PersistentManyToManyCollectionImpl create(UniqueData holder, PersistentManyToManyCollectionMetadata metadata) { - return new PersistentManyToManyCollectionImpl<>(holder, (Class) metadata.getDataType(), metadata.getParsedJoinTableSchema(), metadata.getParsedJoinTableName(), metadata.getLinks()); + return new PersistentManyToManyCollectionImpl<>(holder, metadata); } public static void delegate(T instance) { @@ -75,7 +69,7 @@ public static Map getJoinTableToReferencedTableLinks(String dataTable, St return joinTableToReferencedTableLinks; } + @Override + public PersistentCollection onAdd(Class holderClass, CollectionChangeHandler addHandler) { + throw new UnsupportedOperationException("Dynamically adding change handlers is not supported for PersistentCollections"); + } + + @Override + public PersistentCollection onRemove(Class holderClass, CollectionChangeHandler removeHandler) { + throw new UnsupportedOperationException("Dynamically adding change handlers is not supported for PersistentCollections"); + } + @Override public UniqueData getHolder() { return holder; @@ -154,15 +158,9 @@ public boolean contains(Object o) { return false; } T data = type.cast(o); - Set ids = getIds(); - ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); - for (ColumnValuePair[] idColumns : ids) { - if (Arrays.equals(idColumns, thatIdColumns)) { - return true; - } - } - - return false; + Set ids = getIds(); + ColumnValuePairs thatIdColumns = data.getIdColumns(); + return ids.contains(thatIdColumns); } @Override @@ -172,10 +170,10 @@ public boolean contains(Object o) { @Override public @NotNull Object @NotNull [] toArray() { - Set ids = getIds(); + Set ids = getIds(); Object[] array = new Object[ids.size()]; int i = 0; - for (ColumnValuePair[] idColumns : ids) { + for (ColumnValuePairs idColumns : ids) { T instance = holder.getDataManager().getInstance(type, idColumns); array[i++] = instance; } @@ -185,12 +183,12 @@ public boolean contains(Object o) { @SuppressWarnings("unchecked") @Override public @NotNull T1 @NotNull [] toArray(@NotNull T1 @NotNull [] a) { - Set ids = getIds(); + Set ids = getIds(); if (a.length < ids.size()) { a = (T1[]) Array.newInstance(a.getClass().getComponentType(), ids.size()); } int i = 0; - for (ColumnValuePair[] idColumns : ids) { + for (ColumnValuePairs idColumns : ids) { T instance = holder.getDataManager().getInstance(type, idColumns); T1 element = (T1) instance; a[i++] = element; @@ -215,21 +213,15 @@ public boolean containsAll(@NotNull Collection c) { return false; } } - Set ids = getIds(); + Set ids = getIds(); for (Object o : c) { T data = type.cast(o); - ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); - boolean found = false; - for (ColumnValuePair[] idColumns : ids) { - if (Arrays.equals(idColumns, thatIdColumns)) { - found = true; - break; - } - } - if (!found) { + ColumnValuePairs thatIdColumns = data.getIdColumns(); + if (!ids.contains(thatIdColumns)) { return false; } } + return true; } @@ -244,8 +236,8 @@ public boolean addAll(@NotNull Collection c) { SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); SQLTransaction.Statement selectReferencedIdsStatement = buildSelectReferencedIdsStatement(); SQLTransaction.Statement updateStatement = buildUpdateStatement(); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); + List joinTableToReferencedTableLinks = metadata.getJoinTableToReferencedTableLinks(holder.getDataManager()); List holderLinkingValues = new ArrayList<>(joinTableToDataTableLinks.size()); List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); @@ -300,13 +292,13 @@ public boolean addAll(@NotNull Collection c) { @Override public boolean removeAll(@NotNull Collection c) { - List idsToRemove = new ArrayList<>(); + List idsToRemove = new ArrayList<>(); for (Object o : c) { if (!type.isInstance(o)) { continue; } T data = type.cast(o); - ColumnValuePair[] idColumns = data.getIdColumns().getPairs(); + ColumnValuePairs idColumns = data.getIdColumns(); idsToRemove.add(idColumns); } return removeIds(idsToRemove); @@ -314,27 +306,20 @@ public boolean removeAll(@NotNull Collection c) { @Override public boolean retainAll(@NotNull Collection c) { - Set currentIds = getIds(); - Set idsToRetain = new HashSet<>(); + Set currentIds = getIds(); + Set idsToRetain = new HashSet<>(); for (Object o : c) { if (!type.isInstance(o)) { continue; } T data = type.cast(o); - ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + ColumnValuePairs thatIdColumns = data.getIdColumns(); idsToRetain.add(thatIdColumns); } - List idsToRemove = new ArrayList<>(); - for (ColumnValuePair[] idColumns : currentIds) { - boolean found = false; - for (ColumnValuePair[] retainIdColumns : idsToRetain) { - if (Arrays.equals(idColumns, retainIdColumns)) { - found = true; - break; - } - } - if (!found) { + List idsToRemove = new ArrayList<>(); + for (ColumnValuePairs idColumns : currentIds) { + if (!idsToRetain.contains(idColumns)) { idsToRemove.add(idColumns); } } @@ -352,7 +337,7 @@ public void clear() { DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); SQLTransaction.Statement clearStatement = buildClearStatement(); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); List holderLinkingValues = new ArrayList<>(joinTableToDataTableLinks.size()); List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); @@ -381,7 +366,7 @@ public void clear() { } - public boolean removeIds(List idsToRemove) { + public boolean removeIds(List idsToRemove) { if (idsToRemove.isEmpty()) { //this operation isn't cheap, so we should avoid it if we can return false; } @@ -390,8 +375,8 @@ public boolean removeIds(List idsToRemove) { SQLTransaction.Statement selectDataIdsStatement = buildSelectDataIdsStatement(); SQLTransaction.Statement selectReferencedIdsStatement = buildSelectReferencedIdsStatement(); SQLTransaction.Statement removeStatement = buildRemoveStatement(); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); + List joinTableToReferencedTableLinks = metadata.getJoinTableToReferencedTableLinks(holder.getDataManager()); List holderLinkingValues = new ArrayList<>(joinTableToDataTableLinks.size()); List holderIdValues = holder.getIdColumns().stream().map(ColumnValuePair::value).toList(); @@ -411,8 +396,8 @@ public boolean removeIds(List idsToRemove) { }); - for (ColumnValuePair[] idColumns : idsToRemove) { - List referencedIdValues = Arrays.stream(idColumns).map(ColumnValuePair::value).toList(); + for (ColumnValuePairs idColumns : idsToRemove) { + List referencedIdValues = idColumns.stream().map(ColumnValuePair::value).toList(); List referencedLinkingValues = new ArrayList<>(joinTableToReferencedTableLinks.size()); transaction.query(selectReferencedIdsStatement, () -> referencedIdValues, rs -> { try { @@ -449,18 +434,18 @@ public boolean removeIds(List idsToRemove) { * * @return set of id column value pairs for the referenced type */ - private Set getIds() { + 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<>(); + Set ids = new HashSet<>(); UniqueDataMetadata holderMetadata = holder.getMetadata(); UniqueDataMetadata target = holder.getDataManager().getMetadata(type); DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); - String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); - String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), target.table()); - List joinTableToDataTableLinks = getJoinTableToDataTableLinks(holderMetadata.table(), links); - List joinTableToReferencedTableLinks = getJoinTableToReferencedTableLinks(holderMetadata.table(), target.table(), links); + String joinTableSchema = metadata.getJoinTableSchema(holder.getDataManager()); + String joinTableName = metadata.getJoinTableName(holder.getDataManager()); + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); + List joinTableToReferencedTableLinks = metadata.getJoinTableToReferencedTableLinks(holder.getDataManager()); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); @@ -500,7 +485,7 @@ private Set getIds() { Object value = rs.getObject(columnMetadata.name()); idColumns[i++] = new ColumnValuePair(columnMetadata.name(), value); } - ids.add(idColumns); + ids.add(new ColumnValuePairs(idColumns)); } } catch (SQLException e) { throw new RuntimeException(e); @@ -512,7 +497,7 @@ private Set getIds() { private SQLTransaction.Statement buildSelectDataIdsStatement() { UniqueDataMetadata holderMetadata = holder.getMetadata(); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); @@ -532,7 +517,7 @@ private SQLTransaction.Statement buildSelectDataIdsStatement() { private SQLTransaction.Statement buildSelectReferencedIdsStatement() { UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToReferencedTableLinks = metadata.getJoinTableToReferencedTableLinks(holder.getDataManager()); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); @@ -552,13 +537,11 @@ private SQLTransaction.Statement buildSelectReferencedIdsStatement() { private SQLTransaction.Statement buildUpdateStatement() { - UniqueDataMetadata holderMetadata = holder.getMetadata(); - UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); + List joinTableToReferencedTableLinks = metadata.getJoinTableToReferencedTableLinks(holder.getDataManager()); - String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); - String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()); + String joinTableSchema = metadata.getJoinTableSchema(holder.getDataManager()); + String joinTableName = metadata.getJoinTableName(holder.getDataManager()); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("MERGE INTO \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" AS _target USING (VALUES ("); sqlBuilder.append("?, ".repeat(Math.max(0, joinTableToDataTableLinks.size() + joinTableToReferencedTableLinks.size()))); @@ -627,13 +610,11 @@ private SQLTransaction.Statement buildUpdateStatement() { } private SQLTransaction.Statement buildRemoveStatement() { - UniqueDataMetadata holderMetadata = holder.getMetadata(); - UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); - List joinTableToReferencedTableLinks = getCachedJoinTableToReferencedTableLinks(); + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); + List joinTableToReferencedTableLinks = metadata.getJoinTableToReferencedTableLinks(holder.getDataManager()); - String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); - String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()); + String joinTableSchema = metadata.getJoinTableSchema(holder.getDataManager()); + String joinTableName = metadata.getJoinTableName(holder.getDataManager()); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("DELETE FROM \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" WHERE "); for (Link entry : joinTableToDataTableLinks) { @@ -650,12 +631,10 @@ private SQLTransaction.Statement buildRemoveStatement() { } private SQLTransaction.Statement buildClearStatement() { - UniqueDataMetadata holderMetadata = holder.getMetadata(); - UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - List joinTableToDataTableLinks = getCachedJoinTableToDataTableLinks(); + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); - String joinTableSchema = getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); - String joinTableName = getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()); + String joinTableSchema = metadata.getJoinTableSchema(holder.getDataManager()); + String joinTableName = metadata.getJoinTableName(holder.getDataManager()); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("DELETE FROM \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" WHERE "); for (Link entry : joinTableToDataTableLinks) { @@ -667,79 +646,41 @@ private SQLTransaction.Statement buildClearStatement() { return SQLTransaction.Statement.of(sql, sql); } - private List getCachedJoinTableToDataTableLinks() { - if (cachedJoinTableToDataTableLinks == null) { - UniqueDataMetadata holderMetadata = holder.getMetadata(); - cachedJoinTableToDataTableLinks = getJoinTableToDataTableLinks(holderMetadata.table(), links); - } - return cachedJoinTableToDataTableLinks; - } - - private List getCachedJoinTableToReferencedTableLinks() { - if (cachedJoinTableToReferencedTableLinks == null) { - UniqueDataMetadata holderMetadata = holder.getMetadata(); - UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); - cachedJoinTableToReferencedTableLinks = getJoinTableToReferencedTableLinks(holderMetadata.table(), typeMetadata.table(), links); - } - return cachedJoinTableToReferencedTableLinks; - } - @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof PersistentManyToManyCollectionImpl that)) return false; - UniqueDataMetadata holderMetadata = holder.getMetadata(); - UniqueDataMetadata thatHolderMetadata = that.holder.getMetadata(); - UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); boolean equals = Objects.equals(type, that.type) && - Objects.equals(getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()), - getJoinTableSchema(that.parsedJoinTableSchema, thatHolderMetadata.schema())) && - Objects.equals(getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()), - getJoinTableName(that.parsedJoinTableName, thatHolderMetadata.table(), typeMetadata.table())) && - Objects.equals(getJoinTableToDataTableLinks(holderMetadata.table(), links), - getJoinTableToDataTableLinks(thatHolderMetadata.table(), that.links)) && - Objects.equals(getJoinTableToReferencedTableLinks(holderMetadata.table(), typeMetadata.table(), links), - getJoinTableToReferencedTableLinks(thatHolderMetadata.table(), typeMetadata.table(), that.links)); + Objects.equals(metadata.getJoinTableSchema(holder.getDataManager()), + that.metadata.getJoinTableSchema(that.holder.getDataManager())) && + Objects.equals(metadata.getJoinTableName(holder.getDataManager()), + that.metadata.getJoinTableName(that.holder.getDataManager())) && + Objects.equals(metadata.getJoinTableToDataTableLinks(holder.getDataManager()), + that.metadata.getJoinTableToDataTableLinks(that.holder.getDataManager())) && + Objects.equals(metadata.getJoinTableToReferencedTableLinks(holder.getDataManager()), + that.metadata.getJoinTableToReferencedTableLinks(that.holder.getDataManager())); if (!equals) { return false; } - Set ids = getIds(); - Set thatIds = that.getIds(); - if (ids.size() != thatIds.size()) { - return false; - } - - for (ColumnValuePair[] idColumns : ids) { - boolean found = false; - for (ColumnValuePair[] thatIdColumns : thatIds) { - if (Arrays.equals(idColumns, thatIdColumns)) { - found = true; - break; - } - } - if (!found) { - return false; - } - } + Set ids = getIds(); + Set thatIds = that.getIds(); - return true; + return ids.equals(thatIds); } @Override public int hashCode() { - UniqueDataMetadata holderMetadata = holder.getMetadata(); - UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); int hash = Objects.hash(type, - getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()), - getJoinTableName(parsedJoinTableName, holderMetadata.table(), typeMetadata.table()), - getJoinTableToDataTableLinks(holderMetadata.table(), links), - getJoinTableToReferencedTableLinks(holderMetadata.table(), typeMetadata.table(), links)); + metadata.getJoinTableSchema(holder.getDataManager()), + metadata.getJoinTableName(holder.getDataManager()), + metadata.getJoinTableToDataTableLinks(holder.getDataManager()), + metadata.getJoinTableToReferencedTableLinks(holder.getDataManager())); int arrayHash = 0; // the ids will not always be in the same order, so ensure this is commutative - for (ColumnValuePair[] idColumns : getIds()) { - arrayHash += Arrays.hashCode(idColumns); + for (ColumnValuePairs idColumns : getIds()) { + arrayHash += idColumns.hashCode(); } hash = 31 * hash + arrayHash; @@ -761,10 +702,10 @@ public String toString() { } class IteratorImpl implements Iterator { - private final List ids; + private final List ids; private int index = 0; - public IteratorImpl(Set ids) { + public IteratorImpl(Set ids) { this.ids = new ArrayList<>(ids); } @@ -778,7 +719,7 @@ public T next() { if (!hasNext()) { throw new NoSuchElementException(); } - ColumnValuePair[] idColumns = ids.get(index++); + ColumnValuePairs idColumns = ids.get(index++); return holder.getDataManager().getInstance(type, idColumns); } 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 699a7d88..2474f507 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 @@ -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.OneToMany; import net.staticstudios.data.PersistentCollection; import net.staticstudios.data.UniqueData; @@ -29,13 +30,13 @@ public PersistentOneToManyCollectionImpl(UniqueData holder, Class type, List< this.link = link; } - public static void createAndDelegate(PersistentCollection.ProxyPersistentCollection proxy, List link) { + public static void createAndDelegate(PersistentCollection.ProxyPersistentCollection proxy, List link, PersistentOneToManyCollectionMetadata metadata) { PersistentOneToManyCollectionImpl delegate = new PersistentOneToManyCollectionImpl<>( proxy.getHolder(), - proxy.getReferenceType(), + proxy.getDataType(), link ); - proxy.setDelegate(delegate); + proxy.setDelegate(metadata, delegate); } public static PersistentOneToManyCollectionImpl create(UniqueData holder, Class type, List link) { @@ -49,11 +50,11 @@ public static void delegate(T instance) { if (!(collectionMetadata instanceof PersistentOneToManyCollectionMetadata oneToManyMetadata)) continue; if (pair.instance() instanceof PersistentCollection.ProxyPersistentCollection proxyCollection) { - createAndDelegate((PersistentCollection.ProxyPersistentCollection) proxyCollection, oneToManyMetadata.getLinks()); + createAndDelegate((PersistentCollection.ProxyPersistentCollection) proxyCollection, oneToManyMetadata.getLinks(), oneToManyMetadata); } else { pair.field().setAccessible(true); try { - pair.field().set(instance, create(instance, oneToManyMetadata.getDataType(), oneToManyMetadata.getLinks())); + pair.field().set(instance, create(instance, oneToManyMetadata.getReferencedType(), oneToManyMetadata.getLinks())); } catch (IllegalAccessException e) { throw new RuntimeException(e); } @@ -61,7 +62,7 @@ public static void delegate(T instance) { } } - public static Map extractMetadata(Class clazz) { + public static Map extractMetadata(DataManager dataManager, Class clazz) { Map metadataMap = new HashMap<>(); for (Field field : ReflectionUtils.getFields(clazz, PersistentCollection.class)) { OneToMany oneToManyAnnotation = field.getAnnotation(OneToMany.class); @@ -69,12 +70,21 @@ public static Map genericType = ReflectionUtils.getGenericType(field); if (genericType == null || !UniqueData.class.isAssignableFrom(genericType)) continue; Class referencedClass = genericType.asSubclass(UniqueData.class); - metadataMap.put(field, new PersistentOneToManyCollectionMetadata(referencedClass, SQLBuilder.parseLinks(oneToManyAnnotation.link()))); + metadataMap.put(field, new PersistentOneToManyCollectionMetadata(dataManager, clazz, referencedClass, SQLBuilder.parseLinks(oneToManyAnnotation.link()))); } return metadataMap; } + @Override + public PersistentCollection onAdd(Class holderClass, CollectionChangeHandler addHandler) { + throw new UnsupportedOperationException("Dynamically adding change handlers is not supported for PersistentCollections"); + } + + @Override + public PersistentCollection onRemove(Class holderClass, CollectionChangeHandler removeHandler) { + throw new UnsupportedOperationException("Dynamically adding change handlers is not supported for PersistentCollections"); + } @Override public UniqueData getHolder() { @@ -97,15 +107,7 @@ public boolean contains(Object o) { return false; } T data = type.cast(o); - Set ids = getIds(); - ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); - for (ColumnValuePair[] idColumns : ids) { - if (Arrays.equals(idColumns, thatIdColumns)) { - return true; - } - } - - return false; + return getIds().contains(data.getIdColumns()); } @Override @@ -115,10 +117,10 @@ public boolean contains(Object o) { @Override public @NotNull Object @NotNull [] toArray() { - Set ids = getIds(); + Set ids = getIds(); Object[] array = new Object[ids.size()]; int i = 0; - for (ColumnValuePair[] idColumns : ids) { + for (ColumnValuePairs idColumns : ids) { T instance = holder.getDataManager().getInstance(type, idColumns); array[i++] = instance; } @@ -128,12 +130,12 @@ public boolean contains(Object o) { @SuppressWarnings("unchecked") @Override public @NotNull T1 @NotNull [] toArray(@NotNull T1 @NotNull [] a) { - Set ids = getIds(); + Set ids = getIds(); if (a.length < ids.size()) { a = (T1[]) Array.newInstance(a.getClass().getComponentType(), ids.size()); } int i = 0; - for (ColumnValuePair[] idColumns : ids) { + for (ColumnValuePairs idColumns : ids) { T instance = holder.getDataManager().getInstance(type, idColumns); T1 element = (T1) instance; a[i++] = element; @@ -158,18 +160,10 @@ public boolean containsAll(@NotNull Collection c) { return false; } } - Set ids = getIds(); + Set ids = getIds(); for (Object o : c) { T data = type.cast(o); - ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); - boolean found = false; - for (ColumnValuePair[] idColumns : ids) { - if (Arrays.equals(idColumns, thatIdColumns)) { - found = true; - break; - } - } - if (!found) { + if (!ids.contains(data.getIdColumns())) { return false; } } @@ -225,13 +219,13 @@ public boolean addAll(@NotNull Collection c) { @Override public boolean removeAll(@NotNull Collection c) { - List ids = new ArrayList<>(); + List ids = new ArrayList<>(); for (Object o : c) { if (!type.isInstance(o)) { continue; } T data = type.cast(o); - ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + ColumnValuePairs thatIdColumns = data.getIdColumns(); ids.add(thatIdColumns); } removeIds(ids); @@ -241,27 +235,20 @@ public boolean removeAll(@NotNull Collection c) { @Override public boolean retainAll(@NotNull Collection c) { - Set currentIds = getIds(); - Set idsToRetain = new HashSet<>(); + Set currentIds = getIds(); + Set idsToRetain = new HashSet<>(); for (Object o : c) { if (!type.isInstance(o)) { continue; } T data = type.cast(o); - ColumnValuePair[] thatIdColumns = data.getIdColumns().getPairs(); + ColumnValuePairs thatIdColumns = data.getIdColumns(); idsToRetain.add(thatIdColumns); } - List idsToRemove = new ArrayList<>(); - for (ColumnValuePair[] idColumns : currentIds) { - boolean found = false; - for (ColumnValuePair[] retainIdColumns : idsToRetain) { - if (Arrays.equals(idColumns, retainIdColumns)) { - found = true; - break; - } - } - if (!found) { + List idsToRemove = new ArrayList<>(); + for (ColumnValuePairs idColumns : currentIds) { + if (!idsToRetain.contains(idColumns)) { idsToRemove.add(idColumns); } } @@ -308,7 +295,7 @@ public void clear() { } } - private void removeIds(List ids) { + private void removeIds(List ids) { Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); if (ids.isEmpty()) { return; @@ -337,7 +324,7 @@ private void removeIds(List ids) { } }); - for (ColumnValuePair[] idColumns : ids) { + for (ColumnValuePairs idColumns : ids) { transaction.update(updateStatement, () -> { List values = new ArrayList<>(); for (Object holderLinkingValue : holderLinkingValues) { //set them to null @@ -414,10 +401,10 @@ private SQLTransaction.Statement buildClearStatement() { return SQLTransaction.Statement.of(sql, sql); } - private Set getIds() { + 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<>(); + Set ids = new HashSet<>(); UniqueDataMetadata holderMetadata = holder.getMetadata(); UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); @@ -458,7 +445,7 @@ private Set getIds() { Object value = rs.getObject(columnMetadata.name()); idColumns[i++] = new ColumnValuePair(columnMetadata.name(), value); } - ids.add(idColumns); + ids.add(new ColumnValuePairs(idColumns)); } } catch (SQLException e) { throw new RuntimeException(e); @@ -497,10 +484,10 @@ public String toString() { } class IteratorImpl implements Iterator { - private final List ids; + private final List ids; private int index = 0; - public IteratorImpl(Set ids) { + public IteratorImpl(Set ids) { this.ids = new ArrayList<>(ids); } @@ -514,7 +501,7 @@ public T next() { if (!hasNext()) { throw new NoSuchElementException(); } - ColumnValuePair[] idColumns = ids.get(index++); + ColumnValuePairs idColumns = ids.get(index++); return holder.getDataManager().getInstance(type, idColumns); } 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 651e9dd2..5df38f2b 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 @@ -34,16 +34,16 @@ public PersistentOneToManyValueCollectionImpl(UniqueData holder, Class type, this.link = link; } - public static void createAndDelegate(ProxyPersistentCollection proxy, String dataSchema, String dataTable, String dataColumn, List link) { + public static void createAndDelegate(ProxyPersistentCollection proxy, String dataSchema, String dataTable, String dataColumn, List link, PersistentOneToManyValueCollectionMetadata metadata) { PersistentOneToManyValueCollectionImpl delegate = new PersistentOneToManyValueCollectionImpl<>( proxy.getHolder(), - proxy.getReferenceType(), + proxy.getDataType(), dataSchema, dataTable, dataColumn, link ); - proxy.setDelegate(delegate); + proxy.setDelegate(metadata, delegate); } public static PersistentOneToManyValueCollectionImpl create(UniqueData holder, Class type, String dataSchema, String dataTable, String dataColumn, List link) { @@ -62,7 +62,8 @@ public static void delegate(T instance) { oneToManyValueMetadata.getDataSchema(), oneToManyValueMetadata.getDataTable(), oneToManyValueMetadata.getDataColumn(), - oneToManyValueMetadata.getLinks() + oneToManyValueMetadata.getLinks(), + oneToManyValueMetadata ); } else { pair.field().setAccessible(true); @@ -97,12 +98,21 @@ public static Map PersistentCollection onAdd(Class holderClass, CollectionChangeHandler addHandler) { + throw new UnsupportedOperationException("Dynamically adding change handlers is not supported for PersistentCollections"); + } + + @Override + public PersistentCollection onRemove(Class holderClass, CollectionChangeHandler removeHandler) { + throw new UnsupportedOperationException("Dynamically adding change handlers is not supported for PersistentCollections"); + } @Override public UniqueData getHolder() { 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 new file mode 100644 index 00000000..0a27919d --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java @@ -0,0 +1,87 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Supplier; +import net.staticstudios.data.CachedValue; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.util.*; +import org.jetbrains.annotations.Nullable; + +public class ReadOnlyCachedValue extends AbstractCachedValue { + private final T value; + private final UniqueData holder; + private final Class dataType; + + public ReadOnlyCachedValue(UniqueData holder, Class dataType, T value) { + this.holder = holder; + this.dataType = dataType; + this.value = holder.getDataManager().copy(value, dataType); + } + + private static void createAndDelegate(CachedValue.ProxyCachedValue proxy, CachedValueMetadata metadata) { + ReadOnlyCachedValue delegate = new ReadOnlyCachedValue<>( + proxy.getHolder(), + proxy.getDataType(), + CachedValueImpl.create(proxy.getHolder(), proxy.getDataType(), metadata).get() + ); + + proxy.setDelegate(metadata, delegate); + } + + private static CachedValue create(UniqueData holder, Class dataType, CachedValueMetadata metadata) { + return new ReadOnlyCachedValue<>(holder, dataType, CachedValueImpl.create(holder, dataType, metadata).get()); + } + + 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); + } + } + } + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getDataType() { + return dataType; + } + + @Override + public CachedValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler) { + throw new UnsupportedOperationException("Read-only value cannot have update handlers"); + } + + @Override + public CachedValue supplyFallback(Supplier fallback) { + throw new UnsupportedOperationException("Cannot set fallback on a read-only CachedValue"); + } + + @Override + public T get() { + return value; + } + + @Override + public void set(T value) { + throw new UnsupportedOperationException("Cannot set value on a read-only CachedValue"); + } + + @Override + public String toString() { + return "ReadOnlyCachedValue{" + + "value=" + value + + '}'; + } +} 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 new file mode 100644 index 00000000..c28946f1 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyPersistentValue.java @@ -0,0 +1,81 @@ +package net.staticstudios.data.impl.data; + +import net.staticstudios.data.PersistentValue; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.util.*; +import org.jetbrains.annotations.Nullable; + +public class ReadOnlyPersistentValue implements PersistentValue { + private final T value; + private final UniqueData holder; + private final Class dataType; + + public ReadOnlyPersistentValue(UniqueData holder, Class dataType, T value) { + this.holder = holder; + this.dataType = dataType; + this.value = holder.getDataManager().copy(value, dataType); + } + + private static void createAndDelegate(PersistentValue.ProxyPersistentValue proxy, PersistentValueMetadata metadata) { + ReadOnlyPersistentValue delegate = new ReadOnlyPersistentValue<>( + proxy.getHolder(), + proxy.getDataType(), + PersistentValueImpl.create(proxy.getHolder(), proxy.getDataType(), metadata).get() + ); + + proxy.setDelegate(metadata, delegate); + } + + private static PersistentValue create(UniqueData holder, Class dataType, PersistentValueMetadata metadata) { + return new ReadOnlyPersistentValue<>(holder, dataType, PersistentValueImpl.create(holder, dataType, metadata).get()); + } + + 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); + } + } + } + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getDataType() { + return dataType; + } + + @Override + public PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler) { + throw new UnsupportedOperationException("Read-only value cannot have update handlers"); + } + + @Override + public T get() { + return value; + } + + @Override + public void set(T value) { + throw new UnsupportedOperationException("Cannot set value on a read-only PersistentValue"); + } + + @Override + public String toString() { + return "ReadOnlyPersistentValue{" + + "value=" + value + + '}'; + } +} 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 new file mode 100644 index 00000000..31e20589 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReference.java @@ -0,0 +1,78 @@ +package net.staticstudios.data.impl.data; + +import net.staticstudios.data.Reference; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.util.*; +import org.jetbrains.annotations.Nullable; + +public class ReadOnlyReference implements Reference { + private final ColumnValuePairs referencedColumnValuePairs; + private final UniqueData holder; + private final Class referenceType; + + public ReadOnlyReference(UniqueData holder, Class referenceType, ColumnValuePairs referencedColumnValuePairs) { + this.holder = holder; + this.referenceType = referenceType; + this.referencedColumnValuePairs = referencedColumnValuePairs; + } + + private static void createAndDelegate(Reference.ProxyReference proxy, ReferenceMetadata metadata) { + ReadOnlyReference delegate = new ReadOnlyReference<>( + proxy.getHolder(), + proxy.getReferenceType(), + ReferenceImpl.create(proxy.getHolder(), proxy.getReferenceType(), metadata.getLinks()).getReferencedColumnValuePairs() + ); + + proxy.setDelegate(delegate); + } + + private static Reference create(UniqueData holder, Class referenceType, ReferenceMetadata metadata) { + return new ReadOnlyReference<>(holder, referenceType, ReferenceImpl.create(holder, referenceType, metadata.getLinks()).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.getReferencedClass(), refMetadata)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getReferenceType() { + return referenceType; + } + + @Override + public T get() { + return holder.getDataManager().getInstance(referenceType, referencedColumnValuePairs.getPairs()); + } + + @Override + public void set(T value) { + throw new UnsupportedOperationException("Cannot set value on a read-only Reference"); + } + + @Override + public String toString() { + return "ReadOnlyReference{" + + "holder=" + holder + + ", referenceType=" + referenceType + + ", referencedColumnValuePairs=" + referencedColumnValuePairs + + '}'; + } +} 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 new file mode 100644 index 00000000..6570ff81 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReferenceCollection.java @@ -0,0 +1,222 @@ +package net.staticstudios.data.impl.data; + +import net.staticstudios.data.PersistentCollection; +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.util.Collection; +import java.util.Iterator; + +public class ReadOnlyReferenceCollection implements PersistentCollection { + private final Collection referencedColumnValuePairsCollection; + private final UniqueData holder; + private final Class referenceType; + + public ReadOnlyReferenceCollection(UniqueData holder, Class referenceType, Collection referencedColumnValuePairsCollection) { + this.holder = holder; + this.referenceType = referenceType; + this.referencedColumnValuePairsCollection = referencedColumnValuePairsCollection; + } + + private static void createAndDelegate(PersistentCollection.ProxyPersistentCollection proxy, PersistentOneToManyCollectionMetadata metadata) { + ReadOnlyReferenceCollection delegate = new ReadOnlyReferenceCollection<>( + proxy.getHolder(), + proxy.getDataType(), + PersistentOneToManyCollectionImpl.create(proxy.getHolder(), proxy.getDataType(), metadata.getLinks()).getIds() + ); + + proxy.setDelegate(metadata, delegate); + } + + private static void createAndDelegate(PersistentCollection.ProxyPersistentCollection proxy, PersistentManyToManyCollectionMetadata metadata) { + ReadOnlyReferenceCollection delegate = new ReadOnlyReferenceCollection<>( + proxy.getHolder(), + proxy.getDataType(), + PersistentManyToManyCollectionImpl.create(proxy.getHolder(), metadata).getIds() + ); + + proxy.setDelegate(metadata, delegate); + } + + private static ReadOnlyReferenceCollection create(UniqueData holder, Class dataType, PersistentOneToManyCollectionMetadata metadata) { + return new ReadOnlyReferenceCollection<>(holder, dataType, PersistentOneToManyCollectionImpl.create(holder, dataType, metadata.getLinks()).getIds()); + } + + private static ReadOnlyReferenceCollection create(UniqueData holder, Class dataType, PersistentManyToManyCollectionMetadata metadata) { + return new ReadOnlyReferenceCollection<>(holder, dataType, PersistentManyToManyCollectionImpl.create(holder, metadata).getIds()); + } + + 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); + } + } + } 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); + } + } + } + } + } + + @Override + public PersistentCollection onAdd(Class holderClass, CollectionChangeHandler addHandler) { + throw new UnsupportedOperationException("Read-only collection cannot have add handlers"); + } + + @Override + public PersistentCollection onRemove(Class holderClass, CollectionChangeHandler removeHandler) { + throw new UnsupportedOperationException("Read-only collection cannot have add handlers"); + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public int size() { + return referencedColumnValuePairsCollection.size(); + } + + @Override + public boolean isEmpty() { + return referencedColumnValuePairsCollection.isEmpty(); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof UniqueData data)) { + return false; + } + return referencedColumnValuePairsCollection.contains(data.getIdColumns()); + } + + @Override + public @NotNull Iterator iterator() { + return new IteratorImpl<>(holder, referenceType, referencedColumnValuePairsCollection.iterator()); + } + + @Override + public @NotNull Object[] toArray() { + Object[] array = new Object[referencedColumnValuePairsCollection.size()]; + int index = 0; + for (ColumnValuePairs pairs : referencedColumnValuePairsCollection) { + array[index++] = holder.getDataManager().getInstance(referenceType, pairs.getPairs()); + } + return array; + } + + @Override + public @NotNull T1[] toArray(@NotNull T1[] a) { + if (a.length < referencedColumnValuePairsCollection.size()) { + a = (T1[]) Array.newInstance(a.getClass().getComponentType(), referencedColumnValuePairsCollection.size()); + } + int index = 0; + for (ColumnValuePairs pairs : referencedColumnValuePairsCollection) { + a[index++] = (T1) holder.getDataManager().getInstance(referenceType, pairs.getPairs()); + } + if (a.length > referencedColumnValuePairsCollection.size()) { + a[referencedColumnValuePairsCollection.size()] = null; + } + return a; + } + + @Override + public boolean add(T t) { + throw new UnsupportedOperationException("Cannot add to a read-only collection"); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException("Cannot remove from a read-only collection"); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + for (Object o : c) { + if (!contains(o)) { + return false; + } + } + return true; + } + + @Override + public boolean addAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Cannot add to a read-only collection"); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Cannot remove from a read-only collection"); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Cannot retain on a read-only collection"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Cannot clear a read-only collection"); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("["); + Iterator iterator = iterator(); + while (iterator.hasNext()) { + T item = iterator.next(); + sb.append(item); + if (iterator.hasNext()) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + + static class IteratorImpl implements Iterator { + private final Iterator internalIterator; + private final UniqueData holder; + private final Class valueType; + + public IteratorImpl(UniqueData holder, Class valueType, Iterator internalIterator) { + this.holder = holder; + this.valueType = valueType; + this.internalIterator = internalIterator; + } + + @Override + public boolean hasNext() { + return internalIterator.hasNext(); + } + + @Override + public T next() { + ColumnValuePairs pairs = internalIterator.next(); + return holder.getDataManager().getInstance(valueType, pairs.getPairs()); + } + } +} 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 new file mode 100644 index 00000000..32845228 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyValuedCollection.java @@ -0,0 +1,142 @@ +package net.staticstudios.data.impl.data; + +import net.staticstudios.data.PersistentCollection; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.util.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class ReadOnlyValuedCollection implements PersistentCollection { + private final Collection values; + private final UniqueData holder; + + public ReadOnlyValuedCollection(UniqueData holder, Class valueType, Collection values) { + this.holder = holder; + List copy = new ArrayList<>(); + for (T value : values) { + copy.add(holder.getDataManager().copy(value, valueType)); + } + this.values = Collections.unmodifiableCollection(copy); + } + + private static void createAndDelegate(PersistentCollection.ProxyPersistentCollection proxy, PersistentOneToManyValueCollectionMetadata metadata) { + ReadOnlyValuedCollection delegate = new ReadOnlyValuedCollection<>( + proxy.getHolder(), + proxy.getDataType(), + PersistentOneToManyValueCollectionImpl.create(proxy.getHolder(), proxy.getDataType(), metadata.getDataSchema(), metadata.getDataTable(), metadata.getDataColumn(), metadata.getLinks()) + ); + + proxy.setDelegate(metadata, delegate); + } + + private static ReadOnlyValuedCollection create(UniqueData holder, Class dataType, PersistentOneToManyValueCollectionMetadata metadata) { + return new ReadOnlyValuedCollection<>(holder, dataType, PersistentOneToManyValueCollectionImpl.create(holder, dataType, metadata.getDataSchema(), metadata.getDataTable(), metadata.getDataColumn(), metadata.getLinks())); + } + + 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 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); + } + } + } + } + + @Override + public PersistentCollection onAdd(Class holderClass, CollectionChangeHandler addHandler) { + throw new UnsupportedOperationException("Read-only collection cannot have add handlers"); + } + + @Override + public PersistentCollection onRemove(Class holderClass, CollectionChangeHandler removeHandler) { + throw new UnsupportedOperationException("Read-only collection cannot have add handlers"); + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public int size() { + return values.size(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return values.contains(o); + } + + @Override + public @NotNull Iterator iterator() { + return values.iterator(); + } + + @Override + public @NotNull Object[] toArray() { + return values.toArray(); + } + + @Override + public @NotNull T1[] toArray(@NotNull T1[] a) { + return values.toArray(a); + } + + @Override + public boolean add(T t) { + throw new UnsupportedOperationException("Cannot add to a read-only collection"); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException("Cannot remove from a read-only collection"); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + return values.containsAll(c); + } + + @Override + public boolean addAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Cannot add to a read-only collection"); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Cannot remove from a read-only collection"); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Cannot retain on a read-only collection"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Cannot clear a read-only collection"); + } + + @Override + public String toString() { + return values.toString(); + } +} 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 6167c8db..bdbb4314 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 @@ -86,6 +86,14 @@ public Class getReferenceType() { @Override public @Nullable T get() { + ColumnValuePairs referencedColumnValuePairs = getReferencedColumnValuePairs(); + if (referencedColumnValuePairs == null) { + return null; + } + return holder.getDataManager().getInstance(type, referencedColumnValuePairs); + } + + public ColumnValuePairs getReferencedColumnValuePairs() { Preconditions.checkArgument(!holder.isDeleted(), "Cannot get reference on a deleted UniqueData instance"); ColumnValuePair[] idColumns = new ColumnValuePair[link.size()]; int i = 0; @@ -123,7 +131,7 @@ public Class getReferenceType() { throw new RuntimeException(e); } - return holder.getDataManager().getInstance(type, idColumns); + return new ColumnValuePairs(idColumns); } @Override 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 f28fa961..f66449b9 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 @@ -552,7 +552,16 @@ private synchronized void updateKnownTables() throws SQLException { if (!knownTables.contains(schema + "." + table)) { logger.trace("Discovered new referringTable {}.{}", schema, table); UUID randomId = UUID.randomUUID(); - @Language("SQL") String sql = "CREATE TRIGGER IF NOT EXISTS \"trg_%s_%s\" AFTER INSERT, UPDATE, DELETE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; + @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'"; + + try (Statement createTrigger = connection.createStatement()) { + String formatted = sql.formatted(table, randomId.toString().replace('-', '_'), schema, table, H2UpdateHandlerTrigger.class.getName()); + logger.trace("[H2] {}", formatted); + H2UpdateHandlerTrigger.registerDataManager(randomId, dataManager); + createTrigger.execute(formatted); + } + + sql = "CREATE TRIGGER IF NOT EXISTS \"delete_trg_%s_%s\" BEFORE DELETE ON \"%s\".\"%s\" FOR EACH ROW CALL '%s'"; try (Statement createTrigger = connection.createStatement()) { String formatted = sql.formatted(table, randomId.toString().replace('-', '_'), schema, table, H2UpdateHandlerTrigger.class.getName()); 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 c827de10..ff391124 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 @@ -1,6 +1,7 @@ package net.staticstudios.data.impl.h2.trigger; import net.staticstudios.data.DataManager; +import net.staticstudios.data.util.TriggerCause; import org.h2.api.Trigger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,6 +69,7 @@ public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws } private void handleInsert(Object[] newRow) { + dataManager.callCollectionChangeHandlers(columnNames, schema, table, columnNames, new Object[newRow.length], newRow, TriggerCause.INSERT); } private void handleUpdate(Object[] oldRow, Object[] newRow) { @@ -87,9 +89,13 @@ private void handleUpdate(Object[] oldRow, Object[] newRow) { for (String changedColumn : changedColumns) { dataManager.callPersistentValueUpdateHandlers(columnNames, schema, table, changedColumn, oldRow, newRow); } + + dataManager.callCollectionChangeHandlers(columnNames, schema, table, changedColumns, oldRow, newRow, TriggerCause.UPDATE); } private void handleDelete(Object[] oldRow) { + dataManager.callCollectionChangeHandlers(columnNames, schema, table, columnNames, oldRow, new Object[oldRow.length], TriggerCause.DELETE); + dataManager.handleDelete(columnNames, schema, table, oldRow); } } 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 1b4cd3e4..6ecfcd87 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -539,6 +539,11 @@ private void parseOneToManyValuePersistentCollection(OneToMany oneToMany, Class< if (referencedTable == null) { List idColumns = List.of(new AutoIncrementingIntegerColumnMetadata(referencedSchemaName, referencedTableName, referencedTableName + "_id")); 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, idCol.type(), idCol.name(), false, false, true, null); + referencedTable.addColumn(sqlColumn); + } referencedSchema.addTable(referencedTable); referencedTable.addColumn(new SQLColumn(referencedTable, genericType, referencedColumnName, oneToMany.nullable(), oneToMany.indexed(), oneToMany.unique(), null)); for (Link link : parseLinks(oneToMany.link())) { @@ -653,6 +658,11 @@ private void parseManyToManyPersistentCollection(ManyToMany manyToMany, Class { private final Class runtimeType; 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 String h2SQLType; private final String pgSQLType; - public Primitive(Class runtimeType, Function<@NotNull String, @NotNull T> decoder, Function<@NotNull T, @NotNull String> encoder, String h2SQLType, String pgSQLType) { + 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.h2SQLType = h2SQLType; this.pgSQLType = pgSQLType; } @@ -38,6 +41,13 @@ public static PrimitiveBuilder builder(Class runtimeType) { return encoder.apply(value); } + public @Nullable T copy(@Nullable T value) { + if (value == null) { + return null; + } + return copier.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 cec09e5d..9effd493 100644 --- a/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java +++ b/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java @@ -10,6 +10,7 @@ public class PrimitiveBuilder { private final Class runtimeType; private Function decoder; private Function encoder; + private Function copier; private String h2SQLType; private String pgSQLType; @@ -33,6 +34,11 @@ public PrimitiveBuilder encoder(Function<@NotNull T, @NotNull String> encoder return this; } + public PrimitiveBuilder copier(Function<@NotNull T, @NotNull T> copier) { + this.copier = copier; + return this; + } + public PrimitiveBuilder h2SQLType(String h2SQLType) { this.h2SQLType = h2SQLType; return this; @@ -47,11 +53,12 @@ public PrimitiveBuilder pgSQLType(String pgSQLType) { 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(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, 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 05b92608..be491ab9 100644 --- a/core/src/main/java/net/staticstudios/data/primative/Primitives.java +++ b/core/src/main/java/net/staticstudios/data/primative/Primitives.java @@ -26,11 +26,13 @@ public class Primitives { .pgSQLType("TEXT") .encoder(s -> s) .decoder(s -> s) + .copier(s -> s) .build(Primitives::register); public static final Primitive INTEGER = Primitive.builder(Integer.class) .h2SQLType("INTEGER") .pgSQLType("INTEGER") .encoder(i -> Integer.toString(i)) + .copier(i -> i) .decoder(Integer::parseInt) .build(Primitives::register); public static final Primitive LONG = Primitive.builder(Long.class) @@ -38,42 +40,53 @@ public class Primitives { .pgSQLType("BIGINT") .encoder(l -> Long.toString(l)) .decoder(Long::parseLong) + .copier(l -> l) .build(Primitives::register); public static final Primitive FLOAT = Primitive.builder(Float.class) .h2SQLType("REAL") .pgSQLType("REAL") .encoder(f -> Float.toString(f)) .decoder(Float::parseFloat) + .copier(f -> f) .build(Primitives::register); public static final Primitive DOUBLE = Primitive.builder(Double.class) .h2SQLType("DOUBLE PRECISION") .pgSQLType("DOUBLE PRECISION") .encoder(d -> Double.toString(d)) .decoder(Double::parseDouble) + .copier(d -> d) .build(Primitives::register); public static final Primitive BOOLEAN = Primitive.builder(Boolean.class) .h2SQLType("BOOLEAN") .pgSQLType("BOOLEAN") .encoder(b -> Boolean.toString(b)) .decoder(Boolean::parseBoolean) + .copier(b -> b) .build(Primitives::register); public static final Primitive UUID = Primitive.builder(java.util.UUID.class) .h2SQLType("UUID") .pgSQLType("UUID") .encoder(java.util.UUID::toString) .decoder(java.util.UUID::fromString) + .copier(uuid -> uuid) .build(Primitives::register); public static final Primitive TIMESTAMP = Primitive.builder(Timestamp.class) .h2SQLType("TIMESTAMP WITH TIME ZONE") .pgSQLType("TIMESTAMPTZ") .encoder(timestamp -> TIMESTAMP_FORMATTER.format(timestamp.toInstant())) .decoder(s -> Timestamp.from(OffsetDateTime.parse(s, TIMESTAMP_FORMATTER).toInstant())) + .copier(timestamp -> new Timestamp(timestamp.getTime())) .build(Primitives::register); public static final Primitive BYTE_ARRAY = Primitive.builder(byte[].class) .h2SQLType("BINARY LARGE OBJECT") .pgSQLType("BYTEA") .encoder(PostgresUtils::toHex) .decoder(PostgresUtils::toBytes) + .copier(bytes -> { + byte[] copy = new byte[bytes.length]; + System.arraycopy(bytes, 0, copy, 0, bytes.length); + return copy; + }) .build(Primitives::register); @SuppressWarnings("unchecked") @@ -102,6 +115,10 @@ public static String encode(@Nullable Object value) { return encode(value, value.getClass()); } + public static T copy(T value, Class type) { + return getPrimitive(type).copy(value); + } + private static String encode(Object value, Class type) { return getPrimitive(type).encode(type.cast(value)); } diff --git a/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandler.java b/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandler.java new file mode 100644 index 00000000..43346133 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandler.java @@ -0,0 +1,13 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +public interface CollectionChangeHandler { + + void handle(U holder, T value); + + @SuppressWarnings("unchecked") + default void unsafeHandle(UniqueData holder, Object value) { + handle((U) holder, (T) value); + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandlerWrapper.java b/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandlerWrapper.java new file mode 100644 index 00000000..55397327 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/CollectionChangeHandlerWrapper.java @@ -0,0 +1,71 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +import java.util.Objects; + +public class CollectionChangeHandlerWrapper { + private final CollectionChangeHandler handler; + private final Class dataType; + private final Class holderClass; + private final Type type; + 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; + this.type = type; + } + + + public CollectionChangeHandler getHandler() { + return handler; + } + + public Class getDataType() { + return dataType; + } + + public Class getHolderClass() { + return holderClass; + } + + public void unsafeHandle(UniqueData holder, Object value) { + handler.unsafeHandle(holder, value); + } + + public Type getType() { + return type; + } + + public void setCollectionMetadata(PersistentCollectionMetadata collectionMetadata) { + this.collectionMetadata = collectionMetadata; + } + + public PersistentCollectionMetadata getCollectionMetadata() { + return collectionMetadata; + } + + @Override + public int hashCode() { + return Objects.hash(handler, dataType, holderClass, type); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + CollectionChangeHandlerWrapper that = (CollectionChangeHandlerWrapper) obj; + return dataType.equals(that.dataType) && holderClass.equals(that.holderClass) && handler.equals(that.handler) && type == that.type; + } + + public enum Type { + ADD, + REMOVE + } +} 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 61dd2a5f..98eb27ad 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java @@ -1,4 +1,7 @@ package net.staticstudios.data.util; +import net.staticstudios.data.UniqueData; + public interface PersistentCollectionMetadata { + Class getHolderClass(); } 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 0e409856..c9140e2f 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java @@ -1,60 +1,97 @@ package net.staticstudios.data.util; +import net.staticstudios.data.DataManager; import net.staticstudios.data.UniqueData; +import net.staticstudios.data.impl.data.PersistentManyToManyCollectionImpl; +import net.staticstudios.data.utils.Link; +import java.util.List; import java.util.Objects; public class PersistentManyToManyCollectionMetadata implements PersistentCollectionMetadata { - private final Class dataType; + private final Class holderClass; + private final Class referencedType; private final String parsedJoinTableSchema; private final String parsedJoinTableName; - private final String links; + private final String rawLinks; + private String joinTableSchema; + private String joinTableName; + private List joinTableToDataTableLinks; + private List joinTableToReferencedTableLinks; - public PersistentManyToManyCollectionMetadata(Class dataType, String parsedJoinTableSchema, String parsedJoinTableName, String links) { - this.dataType = dataType; + public PersistentManyToManyCollectionMetadata(Class holderClass, Class referencedType, String parsedJoinTableSchema, String parsedJoinTableName, String rawLinks) { + this.holderClass = holderClass; + this.referencedType = referencedType; this.parsedJoinTableSchema = parsedJoinTableSchema; this.parsedJoinTableName = parsedJoinTableName; - this.links = links; + this.rawLinks = rawLinks; } - public Class getDataType() { - return dataType; + public Class getReferencedType() { + return referencedType; } - public String getParsedJoinTableSchema() { - return parsedJoinTableSchema; + public synchronized String getJoinTableSchema(DataManager dataManager) { + if (joinTableSchema == null) { + UniqueDataMetadata holderMetadata = dataManager.getMetadata(holderClass); + joinTableSchema = PersistentManyToManyCollectionImpl.getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); + } + return joinTableSchema; } - public String getParsedJoinTableName() { - return parsedJoinTableName; + public synchronized String getJoinTableName(DataManager dataManager) { + if (joinTableName == null) { + UniqueDataMetadata holderMetadata = dataManager.getMetadata(holderClass); + UniqueDataMetadata referencedMetadata = dataManager.getMetadata(referencedType); + joinTableName = PersistentManyToManyCollectionImpl.getJoinTableName(parsedJoinTableName, holderMetadata.table(), referencedMetadata.table()); + } + return joinTableName; } - public String getLinks() { - return links; + public synchronized List getJoinTableToDataTableLinks(DataManager dataManager) { + if (joinTableToDataTableLinks == null) { + UniqueDataMetadata holderMetadata = dataManager.getMetadata(holderClass); + joinTableToDataTableLinks = PersistentManyToManyCollectionImpl.getJoinTableToDataTableLinks(holderMetadata.table(), rawLinks); + } + return joinTableToDataTableLinks; + } + + public synchronized List getJoinTableToReferencedTableLinks(DataManager dataManager) { + if (joinTableToReferencedTableLinks == null) { + UniqueDataMetadata holderMetadata = dataManager.getMetadata(holderClass); + UniqueDataMetadata referencedMetadata = dataManager.getMetadata(referencedType); + joinTableToReferencedTableLinks = PersistentManyToManyCollectionImpl.getJoinTableToReferencedTableLinks(holderMetadata.table(), referencedMetadata.table(), rawLinks); + } + return joinTableToReferencedTableLinks; + } + + @Override + public Class getHolderClass() { + return holderClass; } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; PersistentManyToManyCollectionMetadata that = (PersistentManyToManyCollectionMetadata) o; - return Objects.equals(dataType, that.dataType) && + return Objects.equals(referencedType, that.referencedType) && Objects.equals(parsedJoinTableSchema, that.parsedJoinTableSchema) && Objects.equals(parsedJoinTableName, that.parsedJoinTableName) && - Objects.equals(links, that.links); + Objects.equals(rawLinks, that.rawLinks); } @Override public int hashCode() { - return Objects.hash(dataType, parsedJoinTableSchema, parsedJoinTableName, links); + return Objects.hash(referencedType, parsedJoinTableSchema, parsedJoinTableName, rawLinks); } @Override public String toString() { return "PersistentManyToManyCollectionMetadata{" + - "dataType=" + dataType + + "dataType=" + referencedType + ", parsedJoinTableSchema='" + parsedJoinTableSchema + '\'' + ", parsedJoinTableName='" + parsedJoinTableName + '\'' + - ", links='" + links + '\'' + + ", links='" + rawLinks + '\'' + '}'; } } 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 b87c3cee..067518d7 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java @@ -1,5 +1,6 @@ package net.staticstudios.data.util; +import net.staticstudios.data.DataManager; import net.staticstudios.data.UniqueData; import net.staticstudios.data.utils.Link; @@ -7,32 +8,41 @@ import java.util.Objects; public class PersistentOneToManyCollectionMetadata implements PersistentCollectionMetadata { - private final Class dataType; + private final Class holderClass; + private final DataManager dataManager; + private final Class referencedType; private final List links; - public PersistentOneToManyCollectionMetadata(Class dataType, List links) { - this.dataType = dataType; + public PersistentOneToManyCollectionMetadata(DataManager dataManager, Class holderClass, Class referencedType, List links) { + this.dataManager = dataManager; + this.holderClass = holderClass; + this.referencedType = referencedType; this.links = links; } - public Class getDataType() { - return dataType; + public Class getReferencedType() { + return referencedType; } public List getLinks() { return links; } + @Override + public Class getHolderClass() { + return holderClass; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; PersistentOneToManyCollectionMetadata that = (PersistentOneToManyCollectionMetadata) o; - return Objects.equals(dataType, that.dataType) && Objects.equals(links, that.links); + return Objects.equals(referencedType, that.referencedType) && Objects.equals(links, that.links); } @Override public int hashCode() { - return Objects.hash(dataType, links); + return Objects.hash(referencedType, links); } @Override 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 c746ad12..71ccc2ff 100644 --- a/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java @@ -1,18 +1,21 @@ package net.staticstudios.data.util; +import net.staticstudios.data.UniqueData; import net.staticstudios.data.utils.Link; import java.util.List; import java.util.Objects; public class PersistentOneToManyValueCollectionMetadata implements PersistentCollectionMetadata { + private final Class holderClass; private final Class dataType; private final String dataSchema; private final String dataTable; private final String dataColumn; private final List links; - public PersistentOneToManyValueCollectionMetadata(Class dataType, String dataSchema, String dataTable, String dataColumn, List links) { + public PersistentOneToManyValueCollectionMetadata(Class holderClass, Class dataType, String dataSchema, String dataTable, String dataColumn, List links) { + this.holderClass = holderClass; this.dataType = dataType; this.dataSchema = dataSchema; this.dataTable = dataTable; @@ -20,6 +23,11 @@ public PersistentOneToManyValueCollectionMetadata(Class dataType, String data this.links = links; } + @Override + public Class getHolderClass() { + return holderClass; + } + public Class getDataType() { return dataType; } diff --git a/core/src/main/java/net/staticstudios/data/util/TriggerCause.java b/core/src/main/java/net/staticstudios/data/util/TriggerCause.java new file mode 100644 index 00000000..4af42397 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/TriggerCause.java @@ -0,0 +1,7 @@ +package net.staticstudios.data.util; + +public enum TriggerCause { + INSERT, + UPDATE, + DELETE +} diff --git a/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java index f7f538ca..fca1c866 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java @@ -9,6 +9,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -308,4 +309,99 @@ public void testEqualsAndHashCode() { assertEquals(mockUser.friends, anotherUser.friends); assertEquals(mockUser.friends.hashCode(), anotherUser.friends.hashCode()); } + + @Test + public void testAddHandlerUpdate() throws SQLException { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + List friends = createFriends(5); + user.friends.addAll(friends); + waitForDataPropagation(); + assertEquals(5, user.friendAdditions.get()); + + List otherFriends = createFriends(5); + + Connection pgConnection = getConnection(); + int i = 0; + for (MockUser friend : friends) { + try (PreparedStatement preparedStatement = pgConnection.prepareStatement( + "UPDATE \"public\".\"user_friends\" SET \"users_ref_id\" = ? WHERE \"users_id\" = ? AND \"users_ref_id\" = ?")) { + preparedStatement.setObject(1, otherFriends.get(i).id.get()); + preparedStatement.setObject(2, user.id.get()); + preparedStatement.setObject(3, friend.id.get()); + preparedStatement.executeUpdate(); + } + waitForDataPropagation(); + assertEquals(5 + (++i), user.friendAdditions.get()); + } + } + + @Test + public void testRemoveHandlerUpdate() throws SQLException { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + List friends = createFriends(5); + user.friends.addAll(friends); + waitForDataPropagation(); + assertEquals(5, user.friendAdditions.get()); + + Connection pgConnection = getConnection(); + int i = 0; + for (MockUser friend : friends) { + try (PreparedStatement preparedStatement = pgConnection.prepareStatement( + "DELETE FROM \"public\".\"user_friends\" WHERE \"users_id\" = ? AND \"users_ref_id\" = ?")) { + preparedStatement.setObject(1, user.id.get()); + preparedStatement.setObject(2, friend.id.get()); + preparedStatement.executeUpdate(); + } + waitForDataPropagation(); + assertEquals(++i, user.friendRemovals.get()); + } + } + + @Test + public void testAddHandlerInsert() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + assertEquals(0, user.friendAdditions.get()); + + List friends = createFriends(5); + int i = 0; + for (MockUser friend : friends) { + user.friends.add(friend); + waitForUpdateHandlers(); + + assertEquals(++i, user.friendAdditions.get()); + } + } + + @Test + public void testRemoveHandlerDelete() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + List friends = createFriends(5); + user.friends.addAll(friends); + waitForUpdateHandlers(); + + assertEquals(5, user.friends.size()); + assertEquals(0, user.friendRemovals.get()); + + int i = 0; + for (MockUser friend : friends) { + user.friends.remove(friend); + waitForUpdateHandlers(); + + assertEquals(++i, user.friendRemovals.get()); + } + } } \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java index cd5cc0f4..5aa711f1 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -303,5 +303,88 @@ public void testEqualsAndHashCode() { } - //todo: test add/remove handlers + @Test + public void testAddHandlerUpdate() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + assertEquals(0, user.sessionAdditions.get()); + + List sessions = createSessions(5); + int i = 0; + for (MockUserSession session : sessions) { + user.sessions.add(session); + waitForUpdateHandlers(); + + assertEquals(++i, user.sessionAdditions.get()); + } + } + + @Test + public void testRemoveHandlerUpdate() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + List sessions = createSessions(5); + user.sessions.addAll(sessions); + waitForUpdateHandlers(); + + assertEquals(5, user.sessions.size()); + assertEquals(0, user.sessionRemovals.get()); + + int i = 0; + for (MockUserSession session : sessions) { + user.sessions.remove(session); + waitForUpdateHandlers(); + + assertEquals(++i, user.sessionRemovals.get()); + } + } + + @Test + public void testAddHandlerInsert() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + assertEquals(0, user.sessionAdditions.get()); + for (int i = 0; i < 5; i++) { + MockUserSession.builder(dataManager) + .id(UUID.randomUUID()) + .userId(user.id.get()) + .timestamp(Timestamp.from(Instant.now())) + .insert(InsertMode.ASYNC); + waitForUpdateHandlers(); + + assertEquals(i + 1, user.sessionAdditions.get()); + } + } + + @Test + public void testRemoveHandlerDelete() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + List sessions = createSessions(5); + user.sessions.addAll(sessions); + waitForUpdateHandlers(); + + assertEquals(5, user.sessions.size()); + assertEquals(0, user.sessionRemovals.get()); + + int i = 0; + for (MockUserSession session : sessions) { + session.delete(); + waitForUpdateHandlers(); + + assertEquals(++i, user.sessionRemovals.get()); + } + } } \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java index c41d1254..9def9db9 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java @@ -293,6 +293,128 @@ public void testEqualsAndHashCode() { .insert(InsertMode.SYNC); assertTrue(mockUser.favoriteNumbers.equals(mockUser.favoriteNumbers)); assertFalse(mockUser.favoriteNumbers.equals(anotherMockUser.favoriteNumbers)); + } + @Test + public void testAddHandlerUpdate() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + MockUser user2 = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + assertEquals(0, user.favoriteNumberAdditions.get()); + + Connection pgConnection = getConnection(); + List numbers = createNumbers(5); + int i = 0; + for (Integer number : numbers) { + try (PreparedStatement preparedStatement = pgConnection.prepareStatement( + "INSERT INTO \"public\".\"favorite_numbers\" (\"favorite_numbers_id\", \"user_id\", \"number\") VALUES (?, ?, ?)" + )) { + preparedStatement.setObject(1, number); + preparedStatement.setObject(2, user2.id.get()); + preparedStatement.setInt(3, number); + preparedStatement.executeUpdate(); + } catch (Exception e) { + throw new RuntimeException(e); + } + try (PreparedStatement preparedStatement = pgConnection.prepareStatement( + "UPDATE \"public\".\"favorite_numbers\" SET \"user_id\" = ? WHERE \"favorite_numbers_id\" = ?" + )) { + preparedStatement.setObject(1, user.id.get()); + preparedStatement.setInt(2, number); + preparedStatement.executeUpdate(); + } catch (Exception e) { + throw new RuntimeException(e); + } + waitForDataPropagation(); + + assertEquals(++i, user.favoriteNumberAdditions.get()); + } + } + + @Test + public void testRemoveHandlerUpdate() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + MockUser user2 = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + Connection pgConnection = getConnection(); + List numbers = createNumbers(5); + int i = 0; + for (Integer number : numbers) { + try (PreparedStatement preparedStatement = pgConnection.prepareStatement( + "INSERT INTO \"public\".\"favorite_numbers\" (\"favorite_numbers_id\", \"user_id\", \"number\") VALUES (?, ?, ?)" + )) { + preparedStatement.setObject(1, number); + preparedStatement.setObject(2, user.id.get()); + preparedStatement.setInt(3, number); + preparedStatement.executeUpdate(); + } catch (Exception e) { + throw new RuntimeException(e); + } + try (PreparedStatement preparedStatement = pgConnection.prepareStatement( + "UPDATE \"public\".\"favorite_numbers\" SET \"user_id\" = ? WHERE \"favorite_numbers_id\" = ?" + )) { + preparedStatement.setObject(1, user2.id.get()); + preparedStatement.setInt(2, number); + preparedStatement.executeUpdate(); + } catch (Exception e) { + throw new RuntimeException(e); + } + waitForDataPropagation(); + + assertEquals(++i, user.favoriteNumberRemovals.get()); + } + } + + @Test + public void testAddHandlerInsert() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + assertEquals(0, user.favoriteNumberAdditions.get()); + + List numbers = createNumbers(5); + user.favoriteNumbers.addAll(numbers); + waitForDataPropagation(); + waitForUpdateHandlers(); + assertEquals(5, user.favoriteNumberAdditions.get()); + } + + @Test + public void testRemoveHandlerDelete() { + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("handler test user") + .insert(InsertMode.SYNC); + + List numbers = createNumbers(5); + user.favoriteNumbers.addAll(numbers); + waitForDataPropagation(); + waitForUpdateHandlers(); + + assertEquals(5, user.favoriteNumbers.size()); + assertEquals(0, user.favoriteNumberRemovals.get()); + + int i = 0; + for (Integer number : numbers) { + user.favoriteNumbers.remove(number); + waitForUpdateHandlers(); + + assertEquals(++i, user.favoriteNumberRemovals.get()); + } } } \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/SnapshotTest.java b/core/src/test/java/net/staticstudios/data/SnapshotTest.java new file mode 100644 index 00000000..58e9f658 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/SnapshotTest.java @@ -0,0 +1,54 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.mock.user.MockUser; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class SnapshotTest extends DataTest { + + @Test + public void testPersistentValues() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + UUID id = UUID.randomUUID(); + + MockUser user = MockUser.builder(dataManager) + .id(id) + .name("some name") + .age(0) + .insert(InsertMode.ASYNC); + + MockUser snapshot = dataManager.createSnapshot(user); + + assertNotNull(snapshot); + assertEquals(user.id.get(), snapshot.id.get()); + assertEquals(user.name.get(), snapshot.name.get()); + assertEquals(user.age.get(), snapshot.age.get()); + + assertEquals(0, user.nameUpdates.get()); + assertEquals(0, snapshot.nameUpdates.get()); + + user.name.set("new name"); + waitForUpdateHandlers(); + + assertEquals("new name", user.name.get()); + assertEquals("some name", snapshot.name.get()); + assertEquals(1, user.nameUpdates.get()); + assertEquals(0, snapshot.nameUpdates.get()); + + user.delete(); + + assertTrue(user.isDeleted()); + assertFalse(snapshot.isDeleted()); + + assertEquals(id, snapshot.id.get()); + assertEquals("some name", snapshot.name.get()); + } + + //todo: tests for cvs, refs, and all collection types +} \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index 9ecf55a2..51f8879e 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -44,17 +44,44 @@ public class MockUser extends UniqueData { @Column(name = "views", nullable = true) public PersistentValue views; + @Identifier("session_additions") + public CachedValue sessionAdditions = CachedValue.of(this, Integer.class) + .withFallback(0); + @Identifier("session_removals") + public CachedValue sessionRemovals = CachedValue.of(this, Integer.class) + .withFallback(0); @Delete(DeleteStrategy.NO_ACTION) @OneToMany(link = "id=user_id") - public PersistentCollection sessions; + public PersistentCollection sessions = PersistentCollection.of(this, MockUserSession.class) + .onAdd(MockUser.class, (user, added) -> user.sessionAdditions.set(user.sessionAdditions.get() + 1)) + .onRemove(MockUser.class, (user, removed) -> user.sessionRemovals.set(user.sessionRemovals.get() + 1)); + + @Identifier("friend_additions") + public CachedValue friendAdditions = CachedValue.of(this, Integer.class) + .withFallback(0); + @Identifier("friend_removals") + public CachedValue friendRemovals = CachedValue.of(this, Integer.class) + .withFallback(0); @Delete(DeleteStrategy.CASCADE) //todo: impl delete strategy for many to many collections @ManyToMany(link = "id=id", joinTable = "user_friends") - public PersistentCollection friends; + public PersistentCollection friends = PersistentCollection.of(this, MockUser.class) + .onAdd(MockUser.class, (user, added) -> user.friendAdditions.set(user.friendAdditions.get() + 1)) + .onRemove(MockUser.class, (user, removed) -> user.friendRemovals.set(user.friendRemovals.get() + 1)); + + + @Identifier("favorite_number_additions") + public CachedValue favoriteNumberAdditions = CachedValue.of(this, Integer.class) + .withFallback(0); + @Identifier("favorite_number_removals") + public CachedValue favoriteNumberRemovals = CachedValue.of(this, Integer.class) + .withFallback(0); @Delete(DeleteStrategy.CASCADE) @OneToMany(link = "id=user_id", table = "favorite_numbers", column = "number") - public PersistentCollection favoriteNumbers; + public PersistentCollection favoriteNumbers = PersistentCollection.of(this, Integer.class) + .onAdd(MockUser.class, (user, added) -> user.favoriteNumberAdditions.set(user.favoriteNumberAdditions.get() + 1)) + .onRemove(MockUser.class, (user, removed) -> user.favoriteNumberRemovals.set(user.favoriteNumberRemovals.get() + 1)); @Identifier("cooldown_updates") public CachedValue cooldownUpdates = CachedValue.of(this, Integer.class) .withFallback(0); From 98916b172b4ebec4cd4b6c44fe004b857ee88cb7 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 11 Nov 2025 21:09:44 -0500 Subject: [PATCH 48/75] update readme --- README.md | 436 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 289 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 967dbad7..5ec5ece9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ # static-data -`static-data` is an ORM (Object-Relational Mapping) library originally designed for Minecraft servers. It provides a -robust solution for managing database operations in distributed applications while avoiding blocking the main thread. -This is what makes it different from other ORMs, read and write speed is the main focus. +`static-data` is an ORM library originally designed for Minecraft servers. It provides a +robust solution for managing database operations in distributed applications while avoiding blocking operations at +runtime. +Minecraft servers are generally single threaded, so this library was built to avoid blocking the main thread during +database operations. +The main idea is simple: keep an in-memory cache of relevant database tables, and update that cache whenever the source +database changes. +As for the implementation, the in-memory cache is built using an in-memory H2 database, while PostgreSQL is used as the +source database. The source database is limited to PostgreSQL due to its support for the `LISTEN / NOTIFY` commands, +which +allow `static-data` to receive notifications whenever a change is made to the database. ## Key Features @@ -72,6 +80,11 @@ The goal is to make the developer experience as seamless as possible. - `@Column(name = "...", nullable = true/false, index = true/false)`: Define a column in the current table. - `@ForeignColumn(name = "...", table = "...", link = "...")`: Define a column in another table, and create a foreign key accordingly. +- `@Identifier([identifier])`: Specifies the identifier to use for `CachedValue` fields. This is used in conjunction + with the `UniqueData` instance's IDs to create a unique key in Redis. +- `@ExpireAfter([seconds])`: Used on `CachedValue` fields to specify the expiration time in seconds. A value of 0 + means no expiration. When a value has expired, subsequent calls to `get()` will return `null`, or the fallback value + if one is specified. - `@OneToOne(link = "...")`: Defines a one-to-one relationship for `Reference` fields. A foreign key constraint is created accordingly. - `@OneToMany(link = "...")`: Defines a one-to-many relationship for `PersistentCollection` fields. A foreign key @@ -81,11 +94,12 @@ The goal is to make the developer experience as seamless as possible. - `@DefaultValue("...")`: Sets a default value for a column. Note that this is a database-level default, not a Java-level default. Only Strings are supported, and they must be valid SQL literals. For example, for an integer column, you would use `@DefaultValue("0")`. -- `@Insert(InsertStrategy.PREFER_EXISTING/OVERWRITE_EXISTING)`: Controls insert behavior for `Reference` and foreign +- `@Insert([InsertStrategy.PREFER_EXISTING/OVERWRITE_EXISTING])`: Controls insert behavior for `Reference` and + foreign columns. -- `@Delete(DeleteStrategy.CASCADE/NO_ACTION)`: Controls delete behavior. This has different behavior depending on the +- `@Delete([DeleteStrategy.CASCADE/NO_ACTION])`: Controls delete behavior. This has different behavior depending on the relationship type, refer to the javadoc on each `DeleteStrategy` enum value for more information. -- `@UpdateInterval(milliseconds)`: Used on `PersistentValue` fields to control how often changes are flushed to the +- `@UpdateInterval([milliseconds])`: Used on `PersistentValue` fields to control how often changes are flushed to the source database. The default is 0 milliseconds. Since a FIFO queue (one connection to the source database) is used to dispatch updates, frequent updates may clog up the queue. When the update interval is set to a non-zero value, only the latest change within the interval is queued for writing to the source database. @@ -94,44 +108,43 @@ The goal is to make the developer experience as seamless as possible. - `PersistentValue`: References a column in a table. Requires one of the annotations: `@IdColumn`, `@Column`, or `@ForeignColumn`. +- `CachedValue`: References a value in redis, "linked" to a specific UniqueData instance. Requires `@Identifier` + annotation. - `Reference`: References another data object (one-to-one relationship). Requires the `@OneToOne` annotation. -- `PersistentCollection`: Represents a collection relationship (one-to-many or many-to-many). Requires either the - `@OneToMany` or - `@ManyToMany` annotation. +- `PersistentCollection`: Represents a collection relationship (one-to-many or many-to-many). One to many + collections support value types that do not extend `UniqueData`, such as `PersistentCollection`. Requires + either the`@OneToMany` or`@ManyToMany` annotation. ### Compile-time Generated Classes For each data class, `static-data` generates two helper classes at compile time, for typesafe operations: -1. **Factory**: Provides a builder pattern for creating and inserting instances +1. **Builder**: Provides a builder pattern for creating and inserting instances ``` // Example: Creating and inserting a new user - User user = UserFactory.builder(dataManager) + User user = User.builder() .id(UUID.randomUUID()) .name("John Doe") .age(30) - .insert(InsertMode.SYNC); + .insert(InsertMode.ASYNC); ``` -Note: The factory can use the global singleton DataManager instance if not explicitly provided. -In the above example, `UserFactory.builder(dataManager)` can be replaced with `UserFactory.builder()` if the global -instance is set. - 2. **Query Builder**: Provides a fluent API for querying instances ``` // Example: Finding users by criteria - List users = UserQuery.where(dataManager) - .nameIsLike("John%") - .and() - .ageIsGreaterThan(25) - .orderByName(Order.ASC) + List users = User.query() + .where(w -> w + .nameIsLike("John%") + .and() + .ageIsGreaterThan(25) + ) + .orderByName(Order.ASCENDING) .limit(10) - .list(); + .findAll(); ``` -Note: The query builder can use the global singleton DataManager instance if not explicitly provided. -In the above example, `UserQuery.where(dataManager)` can be replaced with `UserQuery.where()` if the global instance -is set. +Note: Currently only support for Intellij IDEA is available for IDE integration. You should install the appropriate +plugin for your IDE. ## Data Types @@ -154,128 +167,251 @@ This flexibility allows all primitive types to be nullable when needed, while st [//]: # (TODO: validate the create method calls and ensure theyre correct, i did thie from memory) -```java - -@Data(schema = "my_app", table = "users") -public class User extends UniqueData { - @IdColumn(name = "id") - private PersistentValue id; - - @OneToOne(link = "id=user_id") - private Reference metadata; - - @Column(name = "name", index = true, nullable = false) - private PersistentValue name; - - @ManyToMany(link = "id=id", joinTable = "user_friends") - private PersistentCollection friends; - - /** - * Create a new user and their metadata in one transaction. - * @param id the user ID - * @return the created user - */ - public static User create(int id) { - InsertContext ctx = DataManager.getInstance().createInsertContext(); - UserMetadataFactory.builder() - .id(UUID.randomUUID()) - .userId(id) - .createdAt(new Timestamp(System.currentTimeMillis())) - .insert(InsertMode.SYNC, ctx); - UserFactory factory = UserFactory.builder() - .id(id) - .name("User #" + id) - .insert(InsertMode.SYNC, ctx); - - ctx.insert(); - return factory.get(User.class); - } - - public int getId() { - return id.get(); - } - - public void setId(int id) { - this.id.set(id); - } - - public String getName() { - return name.get(); - } - - public void setName(String name) { - this.name.set(name); - } - - public UserMetadata getMetadata() { - return metadata.get(); - } - - public void setMetadata(UserMetadata metadata) { - this.metadata.set(metadata); - } - - public Collection getFriends() { - return friends.get(); - } - - public void addFriend(User friend) { - this.friends.add(friend); - } - - public void removeFriend(User friend) { - this.friends.remove(friend); - } -} - -@Data(schema = "my_app", table = "user_metadata") -public class UserMetadata extends UniqueData { - @IdColumn(name = "id") - private PersistentValue id; - - @OneToOne(link = "id=id") - private Reference user; - - @Column(name = "user_id", index = true, nullable = false) - private PersistentValue userId; - - @Column(name = "created_at", nullable = false) - private PersistentValue createdAt; - - public UUID getId() { - return id.get(); - } - - public void setId(UUID id) { - this.id.set(id); - } - - public User getUser() { - return user.get(); - } - - public void setUser(User user) { - this.user.set(user); - } - - public int getUserId() { - return userId.get(); - } - - public void setUserId(int userId) { - this.userId.set(userId); - } - - public Timestamp getCreatedAt() { - return createdAt.get(); - } - - public void setCreatedAt(Timestamp createdAt) { - this.createdAt.set(createdAt); - } -} +[//]: # () -``` +[//]: # (```java) + +[//]: # () + +[//]: # (@Data(schema = "my_app", table = "users")) + +[//]: # (public class User extends UniqueData {) + +[//]: # ( @IdColumn(name = "id")) + +[//]: # ( private PersistentValue id;) + +[//]: # () + +[//]: # ( @OneToOne(link = "id=user_id")) + +[//]: # ( private Reference metadata;) + +[//]: # () + +[//]: # ( @Column(name = "name", index = true, nullable = false)) + +[//]: # ( private PersistentValue name;) + +[//]: # () + +[//]: # ( @ManyToMany(link = "id=id", joinTable = "user_friends")) + +[//]: # ( private PersistentCollection friends;) + +[//]: # () + +[//]: # ( /**) + +[//]: # ( * Create a new user and their metadata in one transaction.) + +[//]: # ( * @param id the user ID) + +[//]: # ( * @return the created user) + +[//]: # ( */) + +[//]: # ( public static User create(int id) {) + +[//]: # ( InsertContext ctx = DataManager.getInstance().createInsertContext();) + +[//]: # ( UserMetadataFactory.builder()) + +[//]: # ( .id(UUID.randomUUID())) + +[//]: # ( .userId(id)) + +[//]: # ( .createdAt(new Timestamp(System.currentTimeMillis()))) + +[//]: # ( .insert(InsertMode.SYNC, ctx);) + +[//]: # ( UserFactory factory = UserFactory.builder()) + +[//]: # ( .id(id)) + +[//]: # ( .name("User #" + id)) + +[//]: # ( .insert(InsertMode.SYNC, ctx);) + +[//]: # () + +[//]: # ( ctx.insert();) + +[//]: # ( return factory.get(User.class);) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public int getId() {) + +[//]: # ( return id.get();) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public void setId(int id) {) + +[//]: # ( this.id.set(id);) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public String getName() {) + +[//]: # ( return name.get();) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public void setName(String name) {) + +[//]: # ( this.name.set(name);) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public UserMetadata getMetadata() {) + +[//]: # ( return metadata.get();) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public void setMetadata(UserMetadata metadata) {) + +[//]: # ( this.metadata.set(metadata);) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public Collection getFriends() {) + +[//]: # ( return friends.get();) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public void addFriend(User friend) {) + +[//]: # ( this.friends.add(friend);) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public void removeFriend(User friend) {) + +[//]: # ( this.friends.remove(friend);) + +[//]: # ( }) + +[//]: # (}) + +[//]: # () + +[//]: # (@Data(schema = "my_app", table = "user_metadata")) + +[//]: # (public class UserMetadata extends UniqueData {) + +[//]: # ( @IdColumn(name = "id")) + +[//]: # ( private PersistentValue id;) + +[//]: # () + +[//]: # ( @OneToOne(link = "id=id")) + +[//]: # ( private Reference user;) + +[//]: # () + +[//]: # ( @Column(name = "user_id", index = true, nullable = false)) + +[//]: # ( private PersistentValue userId;) + +[//]: # () + +[//]: # ( @Column(name = "created_at", nullable = false)) + +[//]: # ( private PersistentValue createdAt;) + +[//]: # () + +[//]: # ( public UUID getId() {) + +[//]: # ( return id.get();) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public void setId(UUID id) {) + +[//]: # ( this.id.set(id);) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public User getUser() {) + +[//]: # ( return user.get();) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public void setUser(User user) {) + +[//]: # ( this.user.set(user);) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public int getUserId() {) + +[//]: # ( return userId.get();) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public void setUserId(int userId) {) + +[//]: # ( this.userId.set(userId);) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public Timestamp getCreatedAt() {) + +[//]: # ( return createdAt.get();) + +[//]: # ( }) + +[//]: # () + +[//]: # ( public void setCreatedAt(Timestamp createdAt) {) + +[//]: # ( this.createdAt.set(createdAt);) + +[//]: # ( }) + +[//]: # (}) + +[//]: # () + +[//]: # (```) ## Current Limitations @@ -291,7 +427,13 @@ public class UserMetadata extends UniqueData { - **Disk-based cache**: Plans are in place to add support for a disk-based cache option (using H2 on disk instead of in memory) to reduce memory consumption while still maintaining the benefits of the caching architecture. -## Getting Started +## Miscellaneous + +- Anywhere a schema, table, or column name can be specified, environment variables can be used via the syntax + `${ENV_VAR_NAME}`. + This allows for dynamic configuration based on the deployment environment. + +[//]: # (## Getting Started) [//]: # (TODO: this section is incomplete since the impl isnt finished. update this later) From 85adab56e37ee074329c32ce2cea82e73a30e8ad Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 12 Nov 2025 11:16:20 -0500 Subject: [PATCH 49/75] setup StaticData as a wrapper around the DataManager --- .../net/staticstudios/data/DataManager.java | 22 +++-- .../net/staticstudios/data/StaticData.java | 60 +++++++++++- .../staticstudios/data/StaticDataConfig.java | 93 +++++++++++++++++++ ...ersistentOneToManyValueCollectionTest.java | 10 +- .../net/staticstudios/data/misc/DataTest.java | 27 +++--- .../data/misc/MockEnvironment.java | 4 +- 6 files changed, 186 insertions(+), 30 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/StaticDataConfig.java diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index e26c62b0..6d9c1e59 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -13,7 +13,6 @@ import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; import net.staticstudios.data.utils.Link; -import net.staticstudios.utils.ThreadUtils; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -28,6 +27,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; public class DataManager { private static Boolean useGlobal = null; @@ -49,13 +49,21 @@ public class DataManager { private final Set registeredChangeHandlersForCollection = ConcurrentHashMap.newKeySet(); private final List> valueSerializers = new CopyOnWriteArrayList<>(); + private final Consumer updateHandlerExecurtor; //todo: custom types are serialized and deserialized all the time currently, we should have a cache for these. caffeine with time based eviction sounds good. - public DataManager(DataSourceConfig dataSourceConfig) { - this(dataSourceConfig, true); - } + public DataManager(StaticDataConfig config, boolean setGlobal) { + DataSourceConfig dataSourceConfig = new DataSourceConfig( + config.postgresHost(), + config.postgresPort(), + config.postgresDatabase(), + config.postgresUsername(), + config.postgresPassword(), + config.redisHost(), + config.redisPort() + ); + this.updateHandlerExecurtor = config.updateHandlerExecutor(); - public DataManager(DataSourceConfig dataSourceConfig, boolean setGlobal) { if (setGlobal) { if (Boolean.FALSE.equals(DataManager.useGlobal)) { throw new IllegalStateException("DataManager global instance has been disabled"); @@ -95,7 +103,7 @@ public InsertContext createInsertContext() { return new InsertContext(this); } - public void addUpdateHandler(String schema, String table, String column, ValueUpdateHandlerWrapper handler) {//todo: allow us to specify what data type to convert the data to. this is useful when this method is called externally + public void addUpdateHandler(String schema, String table, String column, ValueUpdateHandlerWrapper handler) { String key = schema + "." + table + "." + column; persistentValueUpdateHandlers.computeIfAbsent(key, k -> new ConcurrentHashMap<>()) .computeIfAbsent(handler.getHolderClass(), k -> new CopyOnWriteArrayList<>()) @@ -514,7 +522,7 @@ private UniqueData getInstanceForCollectionChangeHandler(Class + * 1. It extracts the class's metadata to prepare for subsequent operations. This process is done recursively, so any referenced classes will also be loaded automatically. They do not need to be specified here.
+ * 2. It pulls all existing records of the specified classes from the database into memory, making them readily accessible for future operations. + * + * @param classes the UniqueData classes to load + */ + public static void load(Class... classes) { + assertInit(); + DataManager.getInstance().load(classes); + } + + /** + * Create an InsertContext for batching multiple insert operations together. + * + * @return a new InsertContext instance + */ + public static InsertContext createInsertContext() { + assertInit(); + return DataManager.getInstance().createInsertContext(); + } + + private static void assertInit() { + Preconditions.checkState(initialized, "StaticData has not been initialized! Please call StaticData.init(...) before using any other methods."); + } - public static void init() { - //TODO: this should be the entry point for static-data. consumers should be able to pass in some sort of datasource config to this method. - // a global dm will then be initialized. also loading and any other public methods would be cool to have here. basically a consumer will never touch the datamanager directly, - // they will only need to use static methods here + /** + * Creates a snapshot of the given UniqueData instance. + * The snapshot instance will have the same ID columns as the original instance, + * but it's values will be read-only and represent the state of the data at the time of snapshot creation. + * + * @param instance the UniqueData instance to create a snapshot of + * @param the type of UniqueData + * @return a snapshot UniqueData instance + */ + public T createSnapshot(T instance) { + assertInit(); + return DataManager.getInstance().createSnapshot(instance); } } diff --git a/core/src/main/java/net/staticstudios/data/StaticDataConfig.java b/core/src/main/java/net/staticstudios/data/StaticDataConfig.java new file mode 100644 index 00000000..a8a8f45e --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/StaticDataConfig.java @@ -0,0 +1,93 @@ +package net.staticstudios.data; + +import com.google.common.base.Preconditions; +import net.staticstudios.utils.ThreadUtils; + +import java.util.function.Consumer; + +public record StaticDataConfig(String postgresHost, + int postgresPort, + String postgresDatabase, + String postgresUsername, + String postgresPassword, + String redisHost, + int redisPort, + Consumer updateHandlerExecutor +) { + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String postgresHost; + private int postgresPort = 5432; + private String postgresDatabase; + private String postgresUsername; + private String postgresPassword; + private String redisHost; + private int redisPort = 6379; + private Consumer updateHandlerExecutor = ThreadUtils::submit; + + + public Builder postgresHost(String postgresHost) { + this.postgresHost = postgresHost; + return this; + } + + public Builder postgresPort(int postgresPort) { + this.postgresPort = postgresPort; + return this; + } + + public Builder postgresDatabase(String postgresDatabase) { + this.postgresDatabase = postgresDatabase; + return this; + } + + public Builder postgresUsername(String postgresUsername) { + this.postgresUsername = postgresUsername; + return this; + } + + public Builder postgresPassword(String postgresPassword) { + this.postgresPassword = postgresPassword; + return this; + } + + public Builder redisHost(String redisHost) { + this.redisHost = redisHost; + return this; + } + + public Builder redisPort(int redisPort) { + this.redisPort = redisPort; + return this; + } + + public Builder updateHandlerExecutor(Consumer updateHandlerExecutor) { + this.updateHandlerExecutor = updateHandlerExecutor; + return this; + } + + public StaticDataConfig build() { + Preconditions.checkNotNull(postgresHost, "Postgres host must be set"); + Preconditions.checkNotNull(postgresDatabase, "Postgres database must be set"); + Preconditions.checkNotNull(postgresUsername, "Postgres username must be set"); + Preconditions.checkNotNull(postgresPassword, "Postgres password must be set"); + Preconditions.checkNotNull(redisHost, "Redis host must be set"); + Preconditions.checkNotNull(updateHandlerExecutor, "Update handler executor must be set"); + + return new StaticDataConfig( + postgresHost, + postgresPort, + postgresDatabase, + postgresUsername, + postgresPassword, + redisHost, + redisPort, + updateHandlerExecutor + ); + } + } +} diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java index 9def9db9..c1083628 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java @@ -388,10 +388,12 @@ public void testAddHandlerInsert() { assertEquals(0, user.favoriteNumberAdditions.get()); List numbers = createNumbers(5); - user.favoriteNumbers.addAll(numbers); - waitForDataPropagation(); - waitForUpdateHandlers(); - assertEquals(5, user.favoriteNumberAdditions.get()); + int i = 0; + for (Integer number : numbers) { + user.favoriteNumbers.add(number); + waitForUpdateHandlers(); + assertEquals(++i, user.favoriteNumberAdditions.get()); + } } @Test diff --git a/core/src/test/java/net/staticstudios/data/misc/DataTest.java b/core/src/test/java/net/staticstudios/data/misc/DataTest.java index 1e56e362..f36e756d 100644 --- a/core/src/test/java/net/staticstudios/data/misc/DataTest.java +++ b/core/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -2,8 +2,8 @@ import com.redis.testcontainers.RedisContainer; import net.staticstudios.data.DataManager; +import net.staticstudios.data.StaticDataConfig; import net.staticstudios.data.impl.h2.H2DataAccessor; -import net.staticstudios.data.util.DataSourceConfig; import net.staticstudios.utils.ThreadUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -31,7 +31,7 @@ public class DataTest { .withPassword("password") .withUsername("postgres") .withDatabaseName("postgres"); - public static DataSourceConfig dataSourceConfig; + public static StaticDataConfig config; private static Connection connection; private static Jedis jedis; private List mockEnvironments; @@ -44,15 +44,16 @@ static void initPostgres() throws IOException, SQLException, InterruptedExceptio redis.execInContainer("redis-cli", "config", "set", "notify-keyspace-events", "KEA"); - dataSourceConfig = new DataSourceConfig( - postgres.getHost(), - postgres.getFirstMappedPort(), - postgres.getDatabaseName(), - postgres.getUsername(), - postgres.getPassword(), - redis.getHost(), - redis.getRedisPort() - ); + config = StaticDataConfig.builder() + .postgresHost(postgres.getHost()) + .postgresPort(postgres.getFirstMappedPort()) + .postgresDatabase(postgres.getDatabaseName()) + .postgresUsername(postgres.getUsername()) + .postgresPassword(postgres.getPassword()) + .redisHost(redis.getHost()) + .redisPort(redis.getFirstMappedPort()) + .updateHandlerExecutor(ThreadUtils::submit) + .build(); connection = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); jedis = new Jedis(redis.getHost(), redis.getRedisPort()); @@ -82,9 +83,9 @@ public void setupMockEnvironments() { } protected MockEnvironment createMockEnvironment() { - DataManager dataManager = new DataManager(dataSourceConfig, false); + DataManager dataManager = new DataManager(config, false); - MockEnvironment mockEnvironment = new MockEnvironment(dataSourceConfig, dataManager); + MockEnvironment mockEnvironment = new MockEnvironment(config, dataManager); mockEnvironments.add(mockEnvironment); return mockEnvironment; } diff --git a/core/src/test/java/net/staticstudios/data/misc/MockEnvironment.java b/core/src/test/java/net/staticstudios/data/misc/MockEnvironment.java index a5b37ed1..333e8f28 100644 --- a/core/src/test/java/net/staticstudios/data/misc/MockEnvironment.java +++ b/core/src/test/java/net/staticstudios/data/misc/MockEnvironment.java @@ -1,10 +1,10 @@ package net.staticstudios.data.misc; import net.staticstudios.data.DataManager; -import net.staticstudios.data.util.DataSourceConfig; +import net.staticstudios.data.StaticDataConfig; public record MockEnvironment( - DataSourceConfig dataSourceConfig, + StaticDataConfig config, DataManager dataManager ) { } From d7a4922f149de972922fe144dacaedd82faef4c6 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 12 Nov 2025 21:58:28 -0500 Subject: [PATCH 50/75] add reference update handlers --- .../net/staticstudios/data/DataManager.java | 152 +++++++++++++++++- .../net/staticstudios/data/Reference.java | 19 ++- .../net/staticstudios/data/StaticData.java | 1 + .../data/impl/data/ReadOnlyReference.java | 13 +- .../data/impl/data/ReferenceImpl.java | 17 +- .../h2/trigger/H2UpdateHandlerTrigger.java | 1 + .../data/util/ReferenceMetadata.java | 37 +---- .../data/util/ReferenceUpdateHandler.java | 6 + .../util/ReferenceUpdateHandlerWrapper.java | 47 ++++++ .../net/staticstudios/data/ReferenceTest.java | 46 ++++++ .../data/mock/user/MockUser.java | 15 +- 11 files changed, 292 insertions(+), 62 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandler.java create mode 100644 core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandlerWrapper.java diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 6d9c1e59..e90c39fb 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -42,11 +42,13 @@ public class DataManager { private final ConcurrentHashMap, List>>> persistentValueUpdateHandlers = new ConcurrentHashMap<>(); private final ConcurrentHashMap, List>>> cachedValueUpdateHandlers = new ConcurrentHashMap<>(); private final ConcurrentHashMap, List>>> collectionChangeHandlers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap, List>>> referenceUpdateHandlers = new ConcurrentHashMap<>(); private final PostgresListener postgresListener; private final RedisListener redisListener; private final Set registeredUpdateHandlersForColumns = ConcurrentHashMap.newKeySet(); private final Set registeredUpdateHandlersForRedis = ConcurrentHashMap.newKeySet(); private final Set registeredChangeHandlersForCollection = ConcurrentHashMap.newKeySet(); + private final Set registeredUpdateHandlersForReference = ConcurrentHashMap.newKeySet(); private final List> valueSerializers = new CopyOnWriteArrayList<>(); private final Consumer updateHandlerExecurtor; @@ -276,7 +278,7 @@ private void handleOneToManyCollectionChange(CollectionChangeHandlerWrapper new Link(link.columnInReferringTable(), link.columnInReferencedTable())).toList(), //reverse since the method expects the referenced table to be the join table - columnNames, oldSerializedValues, newSerializedValues, handler); + columnNames, oldSerializedValues, newSerializedValues, handler.getType() == CollectionChangeHandlerWrapper.Type.ADD); if (instance == null) { return; } @@ -472,9 +474,11 @@ private void handleManyToManyCollectionChange(CollectionChangeHandlerWrapper holderClass, List links, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues, CollectionChangeHandlerWrapper handler) { + private UniqueData getInstanceForCollectionChangeHandler(Class holderClass, List links, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues, boolean useNewValues) { UniqueData instance = null; UniqueDataMetadata uniqueDataMetadata = getMetadata(holderClass); + SQLTable uniqueDataTable = Objects.requireNonNull(this.sqlBuilder.getSchema(uniqueDataMetadata.schema())).getTable(uniqueDataMetadata.table()); + Preconditions.checkNotNull(uniqueDataTable, "Table %s.%s not found", uniqueDataMetadata.schema(), uniqueDataMetadata.table()); StringBuilder instanceSqlBuilder = new StringBuilder(); instanceSqlBuilder.append("SELECT "); @@ -492,14 +496,14 @@ private UniqueData getInstanceForCollectionChangeHandler(Class valueType = null; - for (ColumnMetadata idColumn : uniqueDataMetadata.idColumns()) { - if (idColumn.name().equals(link.columnInReferringTable())) { - valueType = idColumn.type(); + for (SQLColumn column : uniqueDataTable.getColumns()) { + if (column.getName().equals(link.columnInReferringTable())) { + valueType = column.getType(); break; } } Preconditions.checkNotNull(valueType, "Could not find column %s in holder UniqueData class %s", link.columnInReferringTable(), uniqueDataMetadata.clazz().getName()); - Object deserializedValue = handler.getType() == CollectionChangeHandlerWrapper.Type.ADD + Object deserializedValue = useNewValues ? deserialize(valueType, newSerializedValues[columnNames.indexOf(link.columnInReferencedTable())]) : deserialize(valueType, oldSerializedValues[columnNames.indexOf(link.columnInReferencedTable())]); instanceValues.add(deserializedValue); @@ -521,6 +525,124 @@ private UniqueData getInstanceForCollectionChangeHandler(Class holderClass, List columnNames, Object[] newSerializedValues) { + UniqueDataMetadata uniqueDataMetadata = getMetadata(holderClass); + ColumnValuePair[] idColumns = new ColumnValuePair[uniqueDataMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : uniqueDataMetadata.idColumns()) { + int columnIndex = columnNames.indexOf(idColumn.name()); + Object serializedValue = newSerializedValues[columnIndex]; + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[uniqueDataMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + return getInstance(uniqueDataMetadata.clazz(), idColumns); + } + + @ApiStatus.Internal + public void callReferenceUpdateHandlers(List columnNames, String schema, String table, List changedColumns, Object[] oldSerializedValues, Object[] newSerializedValues) { + logger.trace("Calling reference update handlers for {}.{} on changed columns {} with old values {} and new values {}", schema, table, changedColumns, Arrays.toString(oldSerializedValues), Arrays.toString(newSerializedValues)); + Map, List>> handlersForTable = referenceUpdateHandlers.get(schema + "." + table); + if (handlersForTable == null) { + return; + } + + for (Map.Entry, List>> entry : handlersForTable.entrySet()) { + List> handlers = entry.getValue(); + for (ReferenceUpdateHandlerWrapper wrapper : handlers) { + ReferenceMetadata metadata = wrapper.getReferenceMetadata(); + List links = metadata.links(); + Object[] oldLinkValues = new Object[links.size()]; + Object[] newLinkValues = new Object[links.size()]; + boolean differenceFound = false; + for (int i = 0; i < links.size(); i++) { + Link link = links.get(i); + int columnIndex = columnNames.indexOf(link.columnInReferringTable()); + Preconditions.checkArgument(columnIndex != -1, "Column %s not found in provided name names %s", link.columnInReferringTable(), columnNames); + oldLinkValues[i] = oldSerializedValues[columnIndex]; + newLinkValues[i] = newSerializedValues[columnIndex]; + if (!Objects.equals(oldLinkValues[i], newLinkValues[i])) { + differenceFound = true; + } + } + + if (!differenceFound) { + continue; + } + + UniqueDataMetadata referencedMetadata = getMetadata(metadata.referencedClass()); + SQLTable referencedTable = Objects.requireNonNull(this.sqlBuilder.getSchema(referencedMetadata.schema())).getTable(referencedMetadata.table()); + Preconditions.checkNotNull(referencedTable, "Referenced table %s.%s not found", referencedMetadata.schema(), referencedMetadata.table()); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + sqlBuilder.append("\"").append(idColumn.name()).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" FROM \"").append(referencedMetadata.schema()).append("\".\"").append(referencedMetadata.table()).append("\" WHERE "); + List oldValues = new ArrayList<>(); + List newValues = new ArrayList<>(); + for (Link link : metadata.links()) { + if (!oldValues.isEmpty()) { + sqlBuilder.append(" AND "); + } + sqlBuilder.append("\"").append(link.columnInReferencedTable()).append("\" = ? "); + Class columnType = null; + for (SQLColumn column : referencedTable.getColumns()) { + if (column.getName().equals(link.columnInReferencedTable())) { + columnType = column.getType(); + break; + } + } + Preconditions.checkNotNull(columnType, "Could not find column %s in referenced table %s.%s", link.columnInReferencedTable(), referencedMetadata.schema(), referencedMetadata.table()); + int columnIndex = columnNames.indexOf(link.columnInReferringTable()); + Object oldDeserializedValue = deserialize(columnType, oldSerializedValues[columnIndex]); + oldValues.add(oldDeserializedValue); + Object newDeserializedValue = deserialize(columnType, newSerializedValues[columnIndex]); + newValues.add(newDeserializedValue); + } + + UniqueData instance = getInstanceForReferenceUpdateHandler(metadata.holderClass(), columnNames, newSerializedValues); + if (instance == null) { + continue; + } + + UniqueData oldInstance = null; + UniqueData newInstance = null; + try (ResultSet rs = dataAccessor.executeQuery(sqlBuilder.toString(), oldValues)) { + if (rs.next()) { + ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + Object serializedValue = rs.getObject(idColumn.name()); + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + oldInstance = getInstance(referencedMetadata.clazz(), idColumns); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + try (ResultSet rs = dataAccessor.executeQuery(sqlBuilder.toString(), newValues)) { + if (rs.next()) { + ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + Object serializedValue = rs.getObject(idColumn.name()); + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + newInstance = getInstance(referencedMetadata.clazz(), idColumns); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + UniqueData finalOldInstance = oldInstance; + UniqueData finalNewInstance = newInstance; + submitUpdateHandler(() -> wrapper.unsafeHandle(instance, finalOldInstance, finalNewInstance)); + } + } + } + private void submitUpdateHandler(Runnable runnable) { updateHandlerExecurtor.accept(runnable); } @@ -567,6 +689,20 @@ public void registerCollectionChangeHandlers(PersistentCollectionMetadata metada } } + @ApiStatus.Internal + public void registerReferenceUpdateHandlers(ReferenceMetadata metadata, Collection> handlers) { + if (registeredUpdateHandlersForReference.add(metadata)) { + handlers.forEach(h -> h.setReferenceMetadata(metadata)); + UniqueDataMetadata holderMetadata = getMetadata(metadata.holderClass()); + String key = holderMetadata.schema() + "." + holderMetadata.table(); + for (ReferenceUpdateHandlerWrapper handler : handlers) { + referenceUpdateHandlers.computeIfAbsent(key, k -> new ConcurrentHashMap<>()) + .computeIfAbsent(holderMetadata.clazz(), k -> new CopyOnWriteArrayList<>()) + .add(handler); + } + } + } + public List> getUpdateHandlers(String schema, String table, String column, Class holderClass) { String key = schema + "." + table + "." + column; if (persistentValueUpdateHandlers.containsKey(key) && persistentValueUpdateHandlers.get(key).containsKey(holderClass)) { diff --git a/core/src/main/java/net/staticstudios/data/Reference.java b/core/src/main/java/net/staticstudios/data/Reference.java index e03c9ebb..d426560a 100644 --- a/core/src/main/java/net/staticstudios/data/Reference.java +++ b/core/src/main/java/net/staticstudios/data/Reference.java @@ -1,10 +1,15 @@ 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 org.jetbrains.annotations.Nullable; import java.lang.reflect.AccessFlag; +import java.util.ArrayList; +import java.util.List; public interface Reference extends Relation { @@ -20,11 +25,12 @@ static Reference of(UniqueData holder, Class refere void set(@Nullable T value); - //todo: support update handlers + Reference onUpdate(Class holderClass, ReferenceUpdateHandler updateHandler); class ProxyReference implements Reference { private final UniqueData holder; private final Class referenceType; + private final List> updateHandlers = new ArrayList<>(); private @Nullable Reference delegate; public ProxyReference(UniqueData holder, Class referenceType) { @@ -43,6 +49,14 @@ public Class getReferenceType() { return referenceType; } + @Override + public Reference onUpdate(Class holderClass, ReferenceUpdateHandler updateHandler) { + Preconditions.checkArgument(delegate == null, "Cannot dynamically add an update handler after the holder has been initialized!"); + ReferenceUpdateHandlerWrapper wrapper = new ReferenceUpdateHandlerWrapper<>(updateHandler); + this.updateHandlers.add(wrapper); + return this; + } + @Override public T get() { Preconditions.checkState(delegate != null, "Reference has not been initialized yet"); @@ -55,9 +69,10 @@ public void set(T value) { delegate.set(value); } - public void setDelegate(Reference delegate) { + public void setDelegate(ReferenceMetadata metadata, Reference delegate) { Preconditions.checkState(this.delegate == null, "Delegate has already been set"); this.delegate = delegate; + holder.getDataManager().registerReferenceUpdateHandlers(metadata, updateHandlers); } } } diff --git a/core/src/main/java/net/staticstudios/data/StaticData.java b/core/src/main/java/net/staticstudios/data/StaticData.java index b85e9f7a..d8758738 100644 --- a/core/src/main/java/net/staticstudios/data/StaticData.java +++ b/core/src/main/java/net/staticstudios/data/StaticData.java @@ -27,6 +27,7 @@ public static void init(StaticDataConfig config) { * * @param classes the UniqueData classes to load */ + @SafeVarargs public static void load(Class... classes) { assertInit(); DataManager.getInstance().load(classes); 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 31e20589..4ce0ba1c 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 @@ -20,14 +20,14 @@ private static void createAndDelegate(Reference.ProxyRefe ReadOnlyReference delegate = new ReadOnlyReference<>( proxy.getHolder(), proxy.getReferenceType(), - ReferenceImpl.create(proxy.getHolder(), proxy.getReferenceType(), metadata.getLinks()).getReferencedColumnValuePairs() + ReferenceImpl.create(proxy.getHolder(), proxy.getReferenceType(), metadata.links()).getReferencedColumnValuePairs() ); - proxy.setDelegate(delegate); + proxy.setDelegate(metadata, delegate); } private static Reference create(UniqueData holder, Class referenceType, ReferenceMetadata metadata) { - return new ReadOnlyReference<>(holder, referenceType, ReferenceImpl.create(holder, referenceType, metadata.getLinks()).getReferencedColumnValuePairs()); + return new ReadOnlyReference<>(holder, referenceType, ReferenceImpl.create(holder, referenceType, metadata.links()).getReferencedColumnValuePairs()); } public static void delegate(U instance) { @@ -39,7 +39,7 @@ public static void delegate(U instance) { } else { pair.field().setAccessible(true); try { - pair.field().set(instance, create(instance, refMetadata.getReferencedClass(), refMetadata)); + pair.field().set(instance, create(instance, refMetadata.referencedClass(), refMetadata)); } catch (IllegalAccessException e) { throw new RuntimeException(e); } @@ -57,6 +57,11 @@ public Class getReferenceType() { return referenceType; } + @Override + public Reference onUpdate(Class holderClass, ReferenceUpdateHandler updateHandler) { + throw new UnsupportedOperationException("Read-only value cannot have update handlers"); + } + @Override public T get() { return holder.getDataManager().getInstance(referenceType, referencedColumnValuePairs.getPairs()); 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 bdbb4314..4d8f0324 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 @@ -30,13 +30,13 @@ public ReferenceImpl(UniqueData holder, Class type, List link) { this.link = link; } - public static void createAndDelegate(Reference.ProxyReference proxy, List link) { + public static void createAndDelegate(Reference.ProxyReference proxy, ReferenceMetadata metadata) { ReferenceImpl delegate = new ReferenceImpl<>( proxy.getHolder(), proxy.getReferenceType(), - link + metadata.links() ); - proxy.setDelegate(delegate); + proxy.setDelegate(metadata, delegate); } public static ReferenceImpl create(UniqueData holder, Class type, List link) { @@ -49,11 +49,11 @@ public static void delegate(T instance) { ReferenceMetadata refMetadata = metadata.referenceMetadata().get(pair.field()); if (pair.instance() instanceof Reference.ProxyReference proxyRef) { - createAndDelegate(proxyRef, refMetadata.getLinks()); + createAndDelegate(proxyRef, refMetadata); } else { pair.field().setAccessible(true); try { - pair.field().set(instance, create(instance, refMetadata.getReferencedClass(), refMetadata.getLinks())); + pair.field().set(instance, create(instance, refMetadata.referencedClass(), refMetadata.links())); } catch (IllegalAccessException e) { throw new RuntimeException(e); } @@ -68,7 +68,7 @@ public static Map extractMetada Preconditions.checkNotNull(oneToOneAnnotation, "Field %s in class %s is missing @OneToOne annotation".formatted(field.getName(), clazz.getName())); Class referencedClass = ReflectionUtils.getGenericType(field); Preconditions.checkNotNull(referencedClass, "Field %s in class %s is not parameterized".formatted(field.getName(), clazz.getName())); - metadataMap.put(field, new ReferenceMetadata((Class) referencedClass, SQLBuilder.parseLinks(oneToOneAnnotation.link()))); + metadataMap.put(field, new ReferenceMetadata(clazz, (Class) referencedClass, SQLBuilder.parseLinks(oneToOneAnnotation.link()))); } return metadataMap; @@ -84,6 +84,11 @@ public Class getReferenceType() { return type; } + @Override + public Reference onUpdate(Class holderClass, ReferenceUpdateHandler updateHandler) { + throw new UnsupportedOperationException("Dynamically adding update handlers is not supported"); + } + @Override public @Nullable T get() { ColumnValuePairs referencedColumnValuePairs = getReferencedColumnValuePairs(); 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 ff391124..1b550013 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 @@ -91,6 +91,7 @@ private void handleUpdate(Object[] oldRow, Object[] newRow) { } dataManager.callCollectionChangeHandlers(columnNames, schema, table, changedColumns, oldRow, newRow, TriggerCause.UPDATE); + dataManager.callReferenceUpdateHandlers(columnNames, schema, table, changedColumns, oldRow, newRow); } private void handleDelete(Object[] oldRow) { 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 cef210c7..6906ddb9 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java +++ b/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java @@ -4,40 +4,7 @@ import net.staticstudios.data.utils.Link; import java.util.List; -import java.util.Objects; -public class ReferenceMetadata { - private final Class referencedClass; - private final List links; - - public ReferenceMetadata(Class referencedClass, List links) { - this.referencedClass = referencedClass; - this.links = links; - } - - public Class getReferencedClass() { - return referencedClass; - } - - public List getLinks() { - return links; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - ReferenceMetadata that = (ReferenceMetadata) o; - return Objects.equals(referencedClass, that.referencedClass) && Objects.equals(links, that.links); - } - - @Override - public int hashCode() { - return Objects.hash(referencedClass, links); - } - - @Override - public String toString() { - return "ReferenceMetadata[" + - "links=" + links + ']'; - } +public record ReferenceMetadata(Class holderClass, Class referencedClass, + List links) { } diff --git a/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandler.java b/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandler.java new file mode 100644 index 00000000..ba383864 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandler.java @@ -0,0 +1,6 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +public interface ReferenceUpdateHandler extends ValueUpdateHandler { +} diff --git a/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandlerWrapper.java b/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandlerWrapper.java new file mode 100644 index 00000000..f26ea8b2 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ReferenceUpdateHandlerWrapper.java @@ -0,0 +1,47 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +import java.util.Objects; + +public class ReferenceUpdateHandlerWrapper { + private final ReferenceUpdateHandler handler; + private ReferenceMetadata referenceMetadata; + + public ReferenceUpdateHandlerWrapper(ReferenceUpdateHandler 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; + } + + public void setReferenceMetadata(ReferenceMetadata referenceMetadata) { + this.referenceMetadata = referenceMetadata; + } + + public ReferenceMetadata getReferenceMetadata() { + return referenceMetadata; + } + + public ReferenceUpdateHandler getHandler() { + return handler; + } + + public void unsafeHandle(UniqueData holder, Object oldValue, Object newValue) { + handler.unsafeHandle(holder, oldValue, newValue); + } + + @Override + public int hashCode() { + return Objects.hash(handler, referenceMetadata); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + ReferenceUpdateHandlerWrapper that = (ReferenceUpdateHandlerWrapper) obj; + return referenceMetadata.equals(that.referenceMetadata) && handler.equals(that.handler); + } +} diff --git a/core/src/test/java/net/staticstudios/data/ReferenceTest.java b/core/src/test/java/net/staticstudios/data/ReferenceTest.java index 89644f62..d90b3f77 100644 --- a/core/src/test/java/net/staticstudios/data/ReferenceTest.java +++ b/core/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -166,4 +166,50 @@ public void testDeleteStrategyCascade() throws SQLException { } } + @Test + public void testUpdateHandlerUpdate() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("name") + .insert(InsertMode.SYNC); + + MockUserSettings settings = MockUserSettings.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + assertEquals(0, user.settingsUpdates.get()); + + user.settings.set(settings); + waitForUpdateHandlers(); + + assertEquals(1, user.settingsUpdates.get()); + + user.settings.set(settings); + waitForUpdateHandlers(); + + assertEquals(1, user.settingsUpdates.get()); + + user.settings.set(null); + waitForUpdateHandlers(); + + assertEquals(2, user.settingsUpdates.get()); + + user.settings.set(settings); + waitForUpdateHandlers(); + + assertEquals(3, user.settingsUpdates.get()); + + MockUserSettings settings2 = MockUserSettings.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + user.settings.set(settings2); + waitForUpdateHandlers(); + + assertEquals(4, user.settingsUpdates.get()); + } + } \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index 51f8879e..07848c52 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -23,9 +23,14 @@ public class MockUser extends UniqueData { @ForeignColumn(name = "fav_color", table = "user_preferences", nullable = true, link = "id=user_id") public PersistentValue favoriteColor; + @Identifier("settings_updates") + public CachedValue settingsUpdates = CachedValue.of(this, Integer.class) + .withFallback(0); + @Delete(DeleteStrategy.CASCADE) @OneToOne(link = "settings_id=user_id") - public Reference settings; + public Reference settings = Reference.of(this, MockUserSettings.class) + .onUpdate(MockUser.class, (user, update) -> user.settingsUpdates.set(user.settingsUpdates.get() + 1)); @Insert(InsertStrategy.OVERWRITE_EXISTING) @Delete(DeleteStrategy.NO_ACTION) @@ -36,9 +41,7 @@ public class MockUser extends UniqueData { @DefaultValue("Unknown") @Column(name = "name", index = true) public PersistentValue name = PersistentValue.of(this, String.class) - .onUpdate(MockUser.class, (user, update) -> { - user.nameUpdates.set(user.getNameUpdates() + 1); - }); + .onUpdate(MockUser.class, (user, update) -> user.nameUpdates.set(user.getNameUpdates() + 1)); @UpdateInterval(5000) @Column(name = "views", nullable = true) @@ -88,9 +91,7 @@ public class MockUser extends UniqueData { @Identifier("on_cooldown") @ExpireAfter(5) public CachedValue onCooldown = CachedValue.of(this, Boolean.class) - .onUpdate(MockUser.class, (user, update) -> { - user.cooldownUpdates.set(user.cooldownUpdates.get() + 1); - }) + .onUpdate(MockUser.class, (user, update) -> user.cooldownUpdates.set(user.cooldownUpdates.get() + 1)) .withFallback(false); public int getNameUpdates() { From 4b9a6d010d6d5112bff4c5565501f4b4fcfdaaf8 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 12 Nov 2025 22:05:21 -0500 Subject: [PATCH 51/75] rename javac-plugin -> processor --- benchmark/build.gradle | 4 ++-- core/build.gradle | 8 ++++---- {javac-plugin => processor}/build.gradle | 0 .../net/staticstudios/data/compiler/javac/Parent.java | 0 .../net/staticstudios/data/compiler/javac/Permit.java | 0 .../data/compiler/javac/ProcessorContext.java | 0 .../data/compiler/javac/StaticDataProcessor.java | 0 .../compiler/javac/javac/AbstractBuilderProcessor.java | 0 .../data/compiler/javac/javac/BuilderProcessor.java | 0 .../javac/javac/ParsedForeignPersistentValue.java | 0 .../data/compiler/javac/javac/ParsedPersistentValue.java | 0 .../data/compiler/javac/javac/ParsedReference.java | 0 .../data/compiler/javac/javac/PositionedTreeMaker.java | 0 .../data/compiler/javac/javac/QueryBuilderProcessor.java | 0 .../data/compiler/javac/javac/SuperClass.java | 0 .../data/compiler/javac/util/SimpleField.java | 0 .../staticstudios/data/compiler/javac/util/TypeUtils.java | 0 .../services/javax.annotation.processing.Processor | 0 settings.gradle | 2 +- 19 files changed, 7 insertions(+), 7 deletions(-) rename {javac-plugin => processor}/build.gradle (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/Parent.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/Permit.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java (100%) rename {javac-plugin => processor}/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java (100%) rename {javac-plugin => processor}/src/main/resources/META-INF/services/javax.annotation.processing.Processor (100%) diff --git a/benchmark/build.gradle b/benchmark/build.gradle index dfb371ab..ca884342 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -16,8 +16,8 @@ dependencies { implementation("org.openjdk.jmh:jmh-generator-annprocess:1.37") implementation(project(":core")) implementation(project(":utils")) - compileOnly project(':javac-plugin') - jmhAnnotationProcessor project(':javac-plugin') + compileOnly project(':processor') + jmhAnnotationProcessor project(':processor') implementation 'net.staticstudios:static-utils:1.0.6-SNAPSHOT' implementation("org.testcontainers:postgresql:1.19.8") diff --git a/core/build.gradle b/core/build.gradle index aa4a02ae..a5d0b567 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -33,10 +33,10 @@ dependencies { testImplementation("org.testcontainers:postgresql:1.19.8") testImplementation("com.redis:testcontainers-redis:2.2.2") testImplementation("org.slf4j:slf4j-log4j12:2.0.16") - compileOnly project(':javac-plugin') - annotationProcessor project(':javac-plugin') - testCompileOnly project(':javac-plugin') - testAnnotationProcessor project(':javac-plugin') + compileOnly project(':processor') + annotationProcessor project(':processor') + testCompileOnly project(':processor') + testAnnotationProcessor project(':processor') } diff --git a/javac-plugin/build.gradle b/processor/build.gradle similarity index 100% rename from javac-plugin/build.gradle rename to processor/build.gradle diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Parent.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/Parent.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Parent.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/Parent.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Permit.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/Permit.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/Permit.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/Permit.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java diff --git a/javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java similarity index 100% rename from javac-plugin/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java rename to processor/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java diff --git a/javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor similarity index 100% rename from javac-plugin/src/main/resources/META-INF/services/javax.annotation.processing.Processor rename to processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/settings.gradle b/settings.gradle index 75bf04a8..0bc6bd07 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,7 @@ rootProject.name = 'static-data' include 'benchmark' include 'core' -include 'javac-plugin' +include 'processor' include 'intellij-plugin' include 'utils' include 'annotations' \ No newline at end of file From 5cd85ce23825e31129972d28c7e08d915d06dc94 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Thu, 13 Nov 2025 12:21:55 -0500 Subject: [PATCH 52/75] update build.gradle --- build.gradle | 55 ++++++++++++++++++++++++++++++++++----- core/build.gradle | 34 ++++++++++--------------- processor/build.gradle | 58 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 109 insertions(+), 38 deletions(-) diff --git a/build.gradle b/build.gradle index 45220657..120a0c3b 100644 --- a/build.gradle +++ b/build.gradle @@ -2,12 +2,55 @@ plugins { id 'java' } -group = 'net.staticstudios' -version = '3.0.0-SNAPSHOT' +allprojects { + group = 'net.staticstudios' + version = '3.0.0-alpha.0-SNAPSHOT' + repositories { + mavenCentral() + maven { + name = "StaticStudios" + url = "https://repo.staticstudios.net/snapshots/" + } + } + + java { + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } +} + +subprojects { + plugins.withId('maven-publish') { + publishing { + repositories { + maven { + name = "StaticStudios" + url = "https://repo.staticstudios.net/private/" + credentials(org.gradle.api.credentials.PasswordCredentials) + } + } + } + + rootProject.tasks.named("publish").configure { + dependsOn(tasks.named("publish")) + } -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) + rootProject.tasks.named("publishToMavenLocal").configure { + dependsOn(tasks.named("publishToMavenLocal")) + } } -} \ No newline at end of file +} + +tasks.register("publish") { + group = "publishing" + description = "Publishes all modules to the StaticStudios Maven repository." +} + +tasks.register("publishToMavenLocal") { + group = "publishing" + description = "Publishes all modules to Maven Local." +} diff --git a/core/build.gradle b/core/build.gradle index a5d0b567..78352563 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -4,17 +4,6 @@ plugins { id 'com.gradleup.shadow' version '8.3.3' } -group = 'net.staticstudios' -version = '3.0.0-SNAPSHOT' - -repositories { - mavenCentral() - maven { - name = "StaticStudios" - url = 'https://repo.staticstudios.net/snapshots/' - } -} - dependencies { implementation(project(":utils")) implementation(project(":annotations")) @@ -62,6 +51,14 @@ javadoc { options.tags = ["implSpec", "apiNote", "implNote"] } +shadowJar { + archiveClassifier.set('all') + + dependencies { + include(project(":annotations")) + } +} + publishing { repositories { maven { @@ -72,7 +69,10 @@ publishing { } publications { maven(MavenPublication) { - from components.java + artifactId = 'static-data' + artifact(shadowJar) { + classifier = null + } pom { name = 'Static Data' description = 'Data library used by StaticStudios.' @@ -92,12 +92,4 @@ publishing { } } } -} - -def targetJavaVersion = 21 -java { - def javaVersion = JavaVersion.toVersion(targetJavaVersion) - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) -} +} \ No newline at end of file diff --git a/processor/build.gradle b/processor/build.gradle index e0f1aaa5..0772c593 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -1,9 +1,6 @@ plugins { id 'java' -} - -repositories { - mavenCentral() + id 'maven-publish' } dependencies { @@ -23,14 +20,53 @@ tasks.withType(JavaCompile).configureEach { '--add-exports', 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', '--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' + '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '--add-opens', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED' ] } -java { - targetCompatibility = JavaVersion.VERSION_21 - sourceCompatibility = JavaVersion.VERSION_21 - toolchain { - languageVersion = JavaLanguageVersion.of(21) +tasks.named("publish") { + dependsOn(build) +} + +//java { +// withSourcesJar() +// withJavadocJar() +//} + + +publishing { + repositories { + maven { + credentials(org.gradle.api.credentials.PasswordCredentials.class) + name = "StaticStudios" + setUrl("https://repo.staticstudios.net/private/") + } + } + publications { + maven(MavenPublication) { + from components.java + artifactId = 'static-data-processor' + pom { + name = 'Static Data Processor' + description = 'Compile-time generator for Static Data.' + url = 'https://github.com/StaticStudios/static-data' + developers { + developer { + id = 'staticstudios' + name = 'Static Studios' + email = 'support@staticstudios.net' + } + } + scm { + connection = 'scm:git:git://github.com/StaticStudios/static-data.git' + developerConnection = 'scm:git:ssh://github.com:StaticStudios/static-data.git' + url = 'https://github.com/StaticStudios/static-data' + } + } + } } -} \ No newline at end of file +} + From 8ba658c12056ac8db342eee14b8451f90d543fc8 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 6 Dec 2025 17:31:22 -0500 Subject: [PATCH 53/75] changes: batch insert, more clauses, etc... --- .../net/staticstudios/data/DataManager.java | 45 ++- .../data/PersistentCollection.java | 4 + .../net/staticstudios/data/StaticData.java | 23 +- .../PersistentManyToManyCollectionImpl.java | 152 ++++----- .../data/insert/BatchInsert.java | 44 +++ .../data/insert/InsertContext.java | 23 +- ...toJoinTableManyToManyPostInsertAction.java | 132 ++++++++ .../data/insert/PostInsertAction.java | 52 +++ .../data/insert/SQLPostInsertAction.java | 21 ++ .../data/query/BaseQueryBuilder.java | 28 +- .../data/query/BaseQueryWhere.java | 18 +- .../data/query/QueryBuilder.java | 300 ++++++++++++++++++ .../query/clause/EqualsIngoreCaseClause.java | 23 ++ .../clause/NotEqualsIngoreCaseClause.java | 23 ++ .../staticstudios/data/BatchInsertTest.java | 136 ++++++++ .../net/staticstudios/data/ReferenceTest.java | 19 +- .../ide/intellij/DataPsiAugmentProvider.java | 16 +- .../javac/javac/BuilderProcessor.java | 131 ++++++++ .../javac/javac/PositionedTreeMaker.java | 4 + .../staticstudios/data/utils/Constants.java | 1 + 20 files changed, 1072 insertions(+), 123 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/insert/BatchInsert.java create mode 100644 core/src/main/java/net/staticstudios/data/insert/InsertIntoJoinTableManyToManyPostInsertAction.java create mode 100644 core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java create mode 100644 core/src/main/java/net/staticstudios/data/insert/SQLPostInsertAction.java create mode 100644 core/src/main/java/net/staticstudios/data/query/QueryBuilder.java create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/EqualsIngoreCaseClause.java create mode 100644 core/src/main/java/net/staticstudios/data/query/clause/NotEqualsIngoreCaseClause.java create mode 100644 core/src/test/java/net/staticstudios/data/BatchInsertTest.java diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index e90c39fb..ac98fa67 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -7,7 +7,9 @@ import net.staticstudios.data.impl.h2.H2DataAccessor; import net.staticstudios.data.impl.pg.PostgresListener; import net.staticstudios.data.impl.redis.RedisListener; +import net.staticstudios.data.insert.BatchInsert; import net.staticstudios.data.insert.InsertContext; +import net.staticstudios.data.insert.PostInsertAction; import net.staticstudios.data.parse.*; import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; @@ -1078,7 +1080,41 @@ private T createSnapshot(Class clazz, ColumnValuePairs return instance; } - public void insert(InsertContext insertContext, InsertMode insertMode) { + public BatchInsert createBatchInsert() { + return new BatchInsert(this); + } + + public void insert(InsertContext context, InsertMode insertMode) { + BatchInsert batch = createBatchInsert(); + + batch.add(context); + insert(batch, insertMode); + } + + public void insert(BatchInsert batch, InsertMode insertMode) { + List statements = new ArrayList<>(); + + for (InsertContext context : batch.getInsertContexts()) { + statements.addAll(generateStatements(context)); + } + + for (PostInsertAction action : batch.getPostInsertActions()) { + statements.addAll(action.getStatements()); + } + + try { + dataAccessor.insert(statements, insertMode); + + for (InsertContext context : batch.getInsertContexts()) { + context.markInserted(); + context.runPostInsertActions(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private List generateStatements(InsertContext insertContext) { //todo: when inserting validate all id and required values are present - this will be enforced by h2, but we should do it here for better logging/errors. Set tables = new HashSet<>(); insertContext.getEntries().forEach((simpleColumnMetadata, o) -> { @@ -1264,12 +1300,7 @@ public void insert(InsertContext insertContext, InsertMode insertMode) { sqlStatements.add(new SQlStatement(h2Sql, pgSql, values)); } - try { - insertContext.markInserted(); - dataAccessor.insert(sqlStatements, insertMode); - } catch (SQLException e) { - throw new RuntimeException(e); - } + return sqlStatements; } public T get(String schema, String table, String column, ColumnValuePairs idColumns, List idColumnLinks, Class dataType) { diff --git a/core/src/main/java/net/staticstudios/data/PersistentCollection.java b/core/src/main/java/net/staticstudios/data/PersistentCollection.java index ca04779c..66f489c2 100644 --- a/core/src/main/java/net/staticstudios/data/PersistentCollection.java +++ b/core/src/main/java/net/staticstudios/data/PersistentCollection.java @@ -38,6 +38,10 @@ public ProxyPersistentCollection(UniqueData holder, Class referenceType) { this.referenceType = referenceType; } + public @Nullable PersistentCollection getDelegate() { + return delegate; + } + @Override public PersistentCollection onAdd(Class holderClass, CollectionChangeHandler addHandler) { changeHandlers.add(new CollectionChangeHandlerWrapper<>(addHandler, referenceType, holder.getClass(), CollectionChangeHandlerWrapper.Type.ADD)); diff --git a/core/src/main/java/net/staticstudios/data/StaticData.java b/core/src/main/java/net/staticstudios/data/StaticData.java index d8758738..9a00bfac 100644 --- a/core/src/main/java/net/staticstudios/data/StaticData.java +++ b/core/src/main/java/net/staticstudios/data/StaticData.java @@ -1,7 +1,8 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; -import net.staticstudios.data.insert.InsertContext; +import net.staticstudios.data.insert.BatchInsert; +import net.staticstudios.data.query.QueryBuilder; /** * Entry point for initializing and interacting with the StaticData system. @@ -34,13 +35,13 @@ public static void load(Class... classes) { } /** - * Create an InsertContext for batching multiple insert operations together. + * Create an BatchInsert for batching multiple insert operations together. * - * @return a new InsertContext instance + * @return a new BatchInsert instance */ - public static InsertContext createInsertContext() { + public static BatchInsert createBatchInsert() { assertInit(); - return DataManager.getInstance().createInsertContext(); + return DataManager.getInstance().createBatchInsert(); } private static void assertInit() { @@ -56,8 +57,18 @@ private static void assertInit() { * @param the type of UniqueData * @return a snapshot UniqueData instance */ - public T createSnapshot(T instance) { + public static T createSnapshot(T instance) { assertInit(); return DataManager.getInstance().createSnapshot(instance); } + + public static void registerValueSerializer(ValueSerializer serializer) { + assertInit(); + DataManager.getInstance().registerValueSerializer(serializer); + } + + public static QueryBuilder query(Class type) { + assertInit(); + return new QueryBuilder<>(DataManager.getInstance(), type); + } } 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 8dbbfc6f..4a5144fb 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 @@ -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.ManyToMany; import net.staticstudios.data.PersistentCollection; import net.staticstudios.data.UniqueData; @@ -127,6 +128,79 @@ public static List getJoinTableToReferencedTableLinks(String dataTable, St return joinTableToReferencedTableLinks; } + public static SQLTransaction.Statement buildUpdateStatement(DataManager dataManager, PersistentManyToManyCollectionMetadata metadata) { + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(dataManager); + List joinTableToReferencedTableLinks = metadata.getJoinTableToReferencedTableLinks(dataManager); + + String joinTableSchema = metadata.getJoinTableSchema(dataManager); + String joinTableName = metadata.getJoinTableName(dataManager); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("MERGE INTO \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" AS _target USING (VALUES ("); + sqlBuilder.append("?, ".repeat(Math.max(0, joinTableToDataTableLinks.size() + joinTableToReferencedTableLinks.size()))); + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(")) AS _source ("); + for (Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + for (Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") ON "); + for (Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("_target.\"").append(joinColumn).append("\" = _source.\"").append(joinColumn).append("\" AND "); + } + for (Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("_target.\"").append(joinColumn).append("\" = _source.\"").append(joinColumn).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + sqlBuilder.append(" WHEN NOT MATCHED THEN INSERT ("); + for (Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + for (Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") VALUES ("); + for (Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("_source.\"").append(joinColumn).append("\", "); + } + for (Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("_source.\"").append(joinColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(")"); + @Language("SQL") String h2MergeSql = sqlBuilder.toString(); + + sqlBuilder.setLength(0); + sqlBuilder.append("INSERT INTO \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" ("); + for (Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + for (Link entry : joinTableToReferencedTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") VALUES ("); + sqlBuilder.append("?, ".repeat(Math.max(0, joinTableToDataTableLinks.size() + joinTableToReferencedTableLinks.size()))); + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(") ON CONFLICT DO NOTHING"); + @Language("SQL") String pgInsertSql = sqlBuilder.toString(); + + return SQLTransaction.Statement.of(h2MergeSql, pgInsertSql); + } + @Override public PersistentCollection onAdd(Class holderClass, CollectionChangeHandler addHandler) { throw new UnsupportedOperationException("Dynamically adding change handlers is not supported for PersistentCollections"); @@ -137,6 +211,10 @@ public PersistentCollection onRemove(Class holderCl throw new UnsupportedOperationException("Dynamically adding change handlers is not supported for PersistentCollections"); } + public PersistentManyToManyCollectionMetadata getMetadata() { + return this.metadata; + } + @Override public UniqueData getHolder() { return holder; @@ -365,7 +443,6 @@ public void clear() { } } - public boolean removeIds(List idsToRemove) { if (idsToRemove.isEmpty()) { //this operation isn't cheap, so we should avoid it if we can return false; @@ -494,7 +571,6 @@ public Set getIds() { return ids; } - private SQLTransaction.Statement buildSelectDataIdsStatement() { UniqueDataMetadata holderMetadata = holder.getMetadata(); List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); @@ -535,78 +611,8 @@ private SQLTransaction.Statement buildSelectReferencedIdsStatement() { return SQLTransaction.Statement.of(sql, sql); } - private SQLTransaction.Statement buildUpdateStatement() { - List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); - List joinTableToReferencedTableLinks = metadata.getJoinTableToReferencedTableLinks(holder.getDataManager()); - - String joinTableSchema = metadata.getJoinTableSchema(holder.getDataManager()); - String joinTableName = metadata.getJoinTableName(holder.getDataManager()); - StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append("MERGE INTO \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" AS _target USING (VALUES ("); - sqlBuilder.append("?, ".repeat(Math.max(0, joinTableToDataTableLinks.size() + joinTableToReferencedTableLinks.size()))); - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(")) AS _source ("); - for (Link entry : joinTableToDataTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("\"").append(joinColumn).append("\", "); - } - for (Link entry : joinTableToReferencedTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("\"").append(joinColumn).append("\", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(") ON "); - for (Link entry : joinTableToDataTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("_target.\"").append(joinColumn).append("\" = _source.\"").append(joinColumn).append("\" AND "); - } - for (Link entry : joinTableToReferencedTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("_target.\"").append(joinColumn).append("\" = _source.\"").append(joinColumn).append("\" AND "); - } - sqlBuilder.setLength(sqlBuilder.length() - 5); - sqlBuilder.append(" WHEN NOT MATCHED THEN INSERT ("); - for (Link entry : joinTableToDataTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("\"").append(joinColumn).append("\", "); - } - for (Link entry : joinTableToReferencedTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("\"").append(joinColumn).append("\", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(") VALUES ("); - for (Link entry : joinTableToDataTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("_source.\"").append(joinColumn).append("\", "); - } - for (Link entry : joinTableToReferencedTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("_source.\"").append(joinColumn).append("\", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(")"); - @Language("SQL") String h2MergeSql = sqlBuilder.toString(); - - sqlBuilder.setLength(0); - sqlBuilder.append("INSERT INTO \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" ("); - for (Link entry : joinTableToDataTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("\"").append(joinColumn).append("\", "); - } - for (Link entry : joinTableToReferencedTableLinks) { - String joinColumn = entry.columnInReferringTable(); - sqlBuilder.append("\"").append(joinColumn).append("\", "); - } - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(") VALUES ("); - sqlBuilder.append("?, ".repeat(Math.max(0, joinTableToDataTableLinks.size() + joinTableToReferencedTableLinks.size()))); - sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(") ON CONFLICT DO NOTHING"); - @Language("SQL") String pgInsertSql = sqlBuilder.toString(); - - return SQLTransaction.Statement.of(h2MergeSql, pgInsertSql); + return buildUpdateStatement(holder.getDataManager(), metadata); } private SQLTransaction.Statement buildRemoveStatement() { diff --git a/core/src/main/java/net/staticstudios/data/insert/BatchInsert.java b/core/src/main/java/net/staticstudios/data/insert/BatchInsert.java new file mode 100644 index 00000000..7a49cd95 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/insert/BatchInsert.java @@ -0,0 +1,44 @@ +package net.staticstudios.data.insert; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.InsertMode; + +import java.util.ArrayList; +import java.util.List; + +public class BatchInsert { + private final DataManager dataManager; + private final List insertContexts = new ArrayList<>(); + private final List postInsertActions = new ArrayList<>(); + + public BatchInsert(DataManager dataManager) { + this.dataManager = dataManager; + } + + public List getPostInsertActions() { + return postInsertActions; + } + + public void addPostInsertAction(PostInsertAction action) { + Preconditions.checkNotNull(action, "PostInsertAction cannot be null"); + postInsertActions.add(action); + } + + public void add(InsertContext context) { + Preconditions.checkNotNull(context, "InsertContext cannot be null"); + Preconditions.checkState(!insertContexts.contains(context), "InsertContext already added to BatchInsert"); + insertContexts.add(context); + } + + public void insert(InsertMode insertMode) { + Preconditions.checkNotNull(insertMode, "InsertMode cannot be null"); + Preconditions.checkState(!insertContexts.isEmpty(), "No InsertContexts to insert"); + Preconditions.checkArgument(insertContexts.stream().noneMatch(InsertContext::isInserted), "All InsertContexts must not be inserted before calling insert"); + dataManager.insert(this, insertMode); + } + + public List getInsertContexts() { + return insertContexts; + } +} diff --git a/core/src/main/java/net/staticstudios/data/insert/InsertContext.java b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java index 15e54549..73159cf3 100644 --- a/core/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -11,18 +11,20 @@ import net.staticstudios.data.util.ColumnValuePair; import net.staticstudios.data.util.SimpleColumnMetadata; import net.staticstudios.data.util.UniqueDataMetadata; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +@ApiStatus.Internal public class InsertContext { private final AtomicBoolean inserted = new AtomicBoolean(false); private final DataManager dataManager; private final Map entries = new HashMap<>(); private final Map insertStrategies = new HashMap<>(); + private final List> callbacks = new ArrayList<>(); public InsertContext(DataManager dataManager) { this.dataManager = dataManager; @@ -72,6 +74,10 @@ public void markInserted() { inserted.set(true); } + public boolean isInserted() { + return inserted.get(); + } + public InsertContext insert(InsertMode insertMode) { dataManager.insert(this, insertMode); return this; @@ -104,4 +110,15 @@ public T get(Class holderClass) { } return dataManager.getInstance(holderClass, idColumnValues); } + + public void addPostInsertAction(Consumer callback) { + Preconditions.checkState(!inserted.get(), "Cannot modify InsertContext after it has been inserted"); + callbacks.add(callback); + } + + public void runPostInsertActions() { + for (Consumer callback : callbacks) { + callback.accept(this); + } + } } \ No newline at end of file diff --git a/core/src/main/java/net/staticstudios/data/insert/InsertIntoJoinTableManyToManyPostInsertAction.java b/core/src/main/java/net/staticstudios/data/insert/InsertIntoJoinTableManyToManyPostInsertAction.java new file mode 100644 index 00000000..be4d0981 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/insert/InsertIntoJoinTableManyToManyPostInsertAction.java @@ -0,0 +1,132 @@ +package net.staticstudios.data.insert; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.impl.data.PersistentManyToManyCollectionImpl; +import net.staticstudios.data.util.*; + +import java.util.ArrayList; +import java.util.List; + +public class InsertIntoJoinTableManyToManyPostInsertAction implements PostInsertAction { + private final DataManager dataManager; + private final PersistentManyToManyCollectionMetadata collectionMetadata; + private final List values; + + + public InsertIntoJoinTableManyToManyPostInsertAction(DataManager dataManager, PersistentManyToManyCollectionMetadata collectionMetadata, Class referringClass, Class referencedClass, List referringIds, List referencedIds) { + this.dataManager = dataManager; + this.collectionMetadata = collectionMetadata; + this.values = new ArrayList<>(); + UniqueDataMetadata referringMetadata = dataManager.getMetadata(referringClass); + UniqueDataMetadata referencedMetadata = dataManager.getMetadata(referencedClass); + + for (ColumnMetadata columnMetadata : referringMetadata.idColumns()) { + ColumnValuePair idValue = referringIds.stream() + .filter(idVal -> idVal.column().equals(columnMetadata.name())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Referring IDs must contain value for column: " + columnMetadata.name())); + this.values.add(idValue.value()); + } + + for (ColumnMetadata columnMetadata : referencedMetadata.idColumns()) { + ColumnValuePair idValue = referencedIds.stream() + .filter(idVal -> idVal.column().equals(columnMetadata.name())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Referenced IDs must contain value for column: " + columnMetadata.name())); + this.values.add(idValue.value()); + } + } + + @Override + public List getStatements() { + SQLTransaction.Statement update = PersistentManyToManyCollectionImpl.buildUpdateStatement(this.dataManager, this.collectionMetadata); + return List.of(new SQlStatement( + update.getH2Sql(), + update.getPgSql(), + this.values + )); + } + + public static class Builder { + private final DataManager dataManager; + private final List referringIds = new ArrayList<>(); + private final List referencedIds = new ArrayList<>(); + private Class referringClass; + private Class referencedClass; + private String joinTableSchema; + private String joinTableName; + + public Builder(DataManager dataManager) { + this.dataManager = dataManager; + } + + public Builder referringClass(Class referringClass) { + this.referringClass = referringClass; + return this; + } + + public Builder referencedClass(Class referencedClass) { + this.referencedClass = referencedClass; + return this; + } + + public Builder joinTableSchema(String joinTableSchema) { + this.joinTableSchema = joinTableSchema; + return this; + } + + public Builder joinTableName(String joinTableName) { + this.joinTableName = joinTableName; + return this; + } + + public Builder referringId(String column, Object value) { + this.referringIds.add(new ColumnValuePair(column, value)); + return this; + } + + public Builder referencedId(String column, Object value) { + this.referencedIds.add(new ColumnValuePair(column, value)); + return this; + } + + public PostInsertAction build() { + Preconditions.checkNotNull(dataManager, "DataManager must be provided."); + Preconditions.checkNotNull(joinTableSchema, "Join table schema must be provided."); + Preconditions.checkNotNull(joinTableName, "Join table name must be provided."); + Preconditions.checkNotNull(referringClass, "Referring class must be provided."); + Preconditions.checkNotNull(referencedClass, "Referenced class must be provided."); + UniqueDataMetadata referringMetadata = dataManager.getMetadata(referringClass); + + PersistentManyToManyCollectionMetadata collectionMetadata = (PersistentManyToManyCollectionMetadata) referringMetadata.persistentCollectionMetadata().values().stream().filter(meta -> { + if (!(meta instanceof PersistentManyToManyCollectionMetadata manyToManyMeta)) { + return false; + } + if (!manyToManyMeta.getReferencedType().equals(referencedClass)) { + return false; + } + String parsedJoinTableSchema = manyToManyMeta.getJoinTableSchema(dataManager); + String parsedJoinTableName = manyToManyMeta.getJoinTableName(dataManager); + return parsedJoinTableSchema.equals(joinTableSchema) && parsedJoinTableName.equals(joinTableName); + }).findFirst().orElseThrow(() -> new IllegalArgumentException("No many-to-many collection found for the given parameters.")); + + + Preconditions.checkState(!referringIds.isEmpty(), "At least one referring ID must be provided."); + Preconditions.checkState(!referencedIds.isEmpty(), "At least one referenced ID must be provided."); + + Preconditions.checkState(referringIds.size() == referringMetadata.idColumns().size(), "Number of referring IDs provided does not match number of ID columns in referring class."); + Preconditions.checkState(referencedIds.size() == dataManager.getMetadata(referencedClass).idColumns().size(), "Number of referenced IDs provided does not match number of ID columns in referenced class."); + + return new InsertIntoJoinTableManyToManyPostInsertAction<>( + dataManager, + collectionMetadata, + referringClass, + referencedClass, + referringIds, + referencedIds + ); + } + } +} diff --git a/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java b/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java new file mode 100644 index 00000000..b3afd810 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java @@ -0,0 +1,52 @@ +package net.staticstudios.data.insert; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.PersistentCollection; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.impl.data.PersistentManyToManyCollectionImpl; +import net.staticstudios.data.util.ColumnValuePair; +import net.staticstudios.data.util.SQlStatement; + +import java.util.List; + +public interface PostInsertAction { + + static InsertIntoJoinTableManyToManyPostInsertAction.Builder manyToMany(DataManager dataManager) { + return new InsertIntoJoinTableManyToManyPostInsertAction.Builder(dataManager); + } + + static InsertIntoJoinTableManyToManyPostInsertAction.Builder manyToMany() { + return new InsertIntoJoinTableManyToManyPostInsertAction.Builder(DataManager.getInstance()); + } + + static InsertIntoJoinTableManyToManyPostInsertAction.Builder manyToMany(PersistentCollection collection) { + PersistentManyToManyCollectionImpl impl = null; + if (collection instanceof PersistentManyToManyCollectionImpl) { + impl = (PersistentManyToManyCollectionImpl) collection; + } else if (collection instanceof PersistentCollection.ProxyPersistentCollection proxy) { + if (proxy.getDelegate() instanceof PersistentManyToManyCollectionImpl delegateImpl) { + impl = delegateImpl; + } + } + + if (impl == null) { + throw new IllegalArgumentException("The provided collection is not a PersistentManyToManyCollection!"); + } + + InsertIntoJoinTableManyToManyPostInsertAction.Builder builder = new InsertIntoJoinTableManyToManyPostInsertAction.Builder(impl.getHolder().getDataManager()) + .referringClass(impl.getHolder().getClass()) + .referencedClass(impl.getMetadata().getReferencedType()) + .joinTableSchema(impl.getMetadata().getJoinTableSchema(impl.getHolder().getDataManager())) + .joinTableName(impl.getMetadata().getJoinTableName(impl.getHolder().getDataManager())); + + UniqueData holder = impl.getHolder(); + for (ColumnValuePair idColumnValuePair : holder.getIdColumns()) { + builder.referringId(idColumnValuePair.column(), idColumnValuePair.value()); + } + + return builder; + } + + List getStatements(); + +} diff --git a/core/src/main/java/net/staticstudios/data/insert/SQLPostInsertAction.java b/core/src/main/java/net/staticstudios/data/insert/SQLPostInsertAction.java new file mode 100644 index 00000000..965677ce --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/insert/SQLPostInsertAction.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.insert; + +import net.staticstudios.data.util.SQlStatement; + +import java.util.ArrayList; +import java.util.List; + +public class SQLPostInsertAction implements PostInsertAction { + + private final List statements = new ArrayList<>(); + + + public void addStatement(SQlStatement statement) { + statements.add(statement); + } + + @Override + public List getStatements() { + return statements; + } +} 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 fb90b478..64b8bbd2 100644 --- a/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java @@ -1,6 +1,5 @@ package net.staticstudios.data.query; -import com.google.common.base.Preconditions; import net.staticstudios.data.DataManager; import net.staticstudios.data.Order; import net.staticstudios.data.UniqueData; @@ -59,23 +58,24 @@ protected void setOffset(int offset) { private ComputedClause compute() { - Preconditions.checkState(!where.isEmpty(), "No clause defined"); StringBuilder sb = new StringBuilder(); - for (InnerJoin join : where.getInnerJoins()) { - sb.append("INNER JOIN \"").append(join.referencedSchema()).append("\".\"").append(join.referencedTable()).append("\" ON "); - for (int i = 0; i < join.columnsInReferringTable().length; i++) { - sb.append("\"").append(join.referringSchema()).append("\".\"").append(join.referringTable()).append("\".\"").append(join.columnsInReferringTable()[i]).append("\" = \"") - .append(join.referencedSchema()).append("\".\"").append(join.referencedTable()).append("\".\"").append(join.columnsInReferencedTable()[i]).append("\""); - if (i < join.columnsInReferringTable().length - 1) { - sb.append(" AND "); + List parameters = new ArrayList<>(); + if (!where.isEmpty()) { + for (InnerJoin join : where.getInnerJoins()) { + sb.append("INNER JOIN \"").append(join.referencedSchema()).append("\".\"").append(join.referencedTable()).append("\" ON "); + for (int i = 0; i < join.columnsInReferringTable().length; i++) { + sb.append("\"").append(join.referringSchema()).append("\".\"").append(join.referringTable()).append("\".\"").append(join.columnsInReferringTable()[i]).append("\" = \"") + .append(join.referencedSchema()).append("\".\"").append(join.referencedTable()).append("\".\"").append(join.columnsInReferencedTable()[i]).append("\""); + if (i < join.columnsInReferringTable().length - 1) { + sb.append(" AND "); + } } + sb.append(" "); } - sb.append(" "); - } - sb.append("WHERE "); + sb.append("WHERE "); - List parameters = new ArrayList<>(); - where.buildWhereClause(sb, parameters); + where.buildWhereClause(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 cba28420..a14a0592 100644 --- a/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java @@ -2,6 +2,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.query.clause.*; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.HashSet; @@ -95,6 +96,15 @@ protected void notEqualsClause(String schema, String table, String column, @Null setValueClause(new NotEqualsClause(schema, table, column, o)); } + //todo: add IJ and processor support for equalsIgnoreCaseClause and notEqualsIgnoreCaseClause + protected void equalsIgnoreCaseClause(String schema, String table, String column, @NotNull String eq) { + setValueClause(new EqualsIngoreCaseClause(schema, table, column, eq)); + } + + protected void notEqualsIgnoreCaseClause(String schema, String table, String column, @NotNull String neq) { + setValueClause(new NotEqualsIngoreCaseClause(schema, table, column, neq)); + } + protected void nullClause(String schema, String table, String column) { setValueClause(new NullClause(schema, table, column)); } @@ -107,6 +117,10 @@ protected void likeClause(String schema, String table, String column, String for setValueClause(new LikeClause(schema, table, column, format)); } + protected void notLikeClause(String schema, String table, String column, String format) { + setValueClause(new NotLikeClause(schema, table, column, format)); + } + protected void inClause(String referringSchema, String referringTable, String column, Object[] in) { setValueClause(new InClause(referringSchema, referringTable, column, in)); } @@ -115,10 +129,6 @@ protected void notInClause(String referringSchema, String referringTable, String setValueClause(new NotInClause(referringSchema, referringTable, column, in)); } - protected void notLikeClause(String schema, String table, String column, String format) { - setValueClause(new NotLikeClause(schema, table, column, format)); - } - protected void betweenClause(String schema, String table, String column, Object min, Object max) { setValueClause(new BetweenClause(schema, table, column, min, max)); } diff --git a/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java b/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java new file mode 100644 index 00000000..d2772ceb --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/QueryBuilder.java @@ -0,0 +1,300 @@ +package net.staticstudios.data.query; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.Order; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.parse.ForeignKey; +import net.staticstudios.data.parse.SQLSchema; +import net.staticstudios.data.parse.SQLTable; +import net.staticstudios.data.util.UniqueDataMetadata; +import net.staticstudios.data.utils.Link; + +import java.util.List; +import java.util.function.Function; + +public class QueryBuilder extends BaseQueryBuilder { + + public QueryBuilder(DataManager dataManager, Class type) { + super(dataManager, type, new QueryWhere(dataManager, dataManager.getMetadata(type))); + } + + public QueryBuilder where(Function function) { + function.apply(super.where); + return this; + } + + public final QueryBuilder limit(int limit) { + super.setLimit(limit); + return this; + } + + public final QueryBuilder offset(int offset) { + super.setOffset(offset); + return this; + } + + public final QueryBuilder orderBy(String schema, String table, String column, Order order) { + super.setOrderBy(schema, table, column, order); + return this; + } + + public final QueryBuilder orderBy(String column, Order order) { + super.setOrderBy(super.where.metadata.schema(), super.where.metadata.table(), column, order); + return this; + } + + + public static class QueryWhere extends BaseQueryWhere { + private final DataManager dataManager; + private final UniqueDataMetadata metadata; + + public QueryWhere(DataManager dataManager, UniqueDataMetadata metadata) { + this.dataManager = dataManager; + this.metadata = metadata; + } + + public final QueryWhere group(Function function) { + super.pushGroup(); + function.apply(this); + super.popGroup(); + return this; + } + + public final QueryWhere and() { + super.andClause(); + return this; + } + + public final QueryWhere or() { + super.orClause(); + return this; + } + + public final QueryWhere is(String schema, String table, String column, Object value) { + maybeAddInnerJoin(schema, table, column); + super.equalsClause(schema, table, column, value); + return this; + } + + public final QueryWhere is(String column, Object value) { + super.equalsClause(metadata.schema(), metadata.table(), column, value); + return this; + } + + public final QueryWhere isNot(String schema, String table, String column, Object value) { + maybeAddInnerJoin(schema, table, column); + super.notEqualsClause(schema, table, column, value); + return this; + } + + public final QueryWhere isNot(String column, Object value) { + super.notEqualsClause(metadata.schema(), metadata.table(), column, value); + return this; + } + + public final QueryWhere isIgnoreCase(String schema, String table, String column, String value) { + maybeAddInnerJoin(schema, table, column); + super.equalsIgnoreCaseClause(schema, table, column, value); + return this; + } + + public final QueryWhere isIgnoreCase(String column, String value) { + super.equalsIgnoreCaseClause(metadata.schema(), metadata.table(), column, value); + return this; + } + + public final QueryWhere isNotIgnoreCase(String schema, String table, String column, String value) { + maybeAddInnerJoin(schema, table, column); + super.notEqualsIgnoreCaseClause(schema, table, column, value); + return this; + } + + public final QueryWhere isNotIgnoreCase(String column, String value) { + super.notEqualsIgnoreCaseClause(metadata.schema(), metadata.table(), column, value); + return this; + } + + public final QueryWhere isNull(String schema, String table, String column) { + super.nullClause(schema, table, column); + return this; + } + + public final QueryWhere isNull(String column) { + super.nullClause(metadata.schema(), metadata.table(), column); + return this; + } + + public final QueryWhere isNotNull(String schema, String table, String column) { + super.notNullClause(schema, table, column); + return this; + } + + public final QueryWhere isNotNull(String column) { + super.notNullClause(metadata.schema(), metadata.table(), column); + return this; + } + + public final QueryWhere like(String schema, String table, String column, String format) { + maybeAddInnerJoin(schema, table, column); + super.likeClause(schema, table, column, format); + return this; + } + + public final QueryWhere like(String column, String format) { + super.likeClause(metadata.schema(), metadata.table(), column, format); + return this; + } + + public final QueryWhere notLike(String schema, String table, String column, String format) { + maybeAddInnerJoin(schema, table, column); + super.notLikeClause(schema, table, column, format); + return this; + } + + public final QueryWhere notLike(String column, String format) { + super.notLikeClause(metadata.schema(), metadata.table(), column, format); + return this; + } + + public final QueryWhere in(String schema, String table, String column, Object[] in) { + maybeAddInnerJoin(schema, table, column); + super.inClause(schema, table, column, in); + return this; + } + + public final QueryWhere in(String column, Object[] in) { + super.inClause(metadata.schema(), metadata.table(), column, in); + return this; + } + + public final QueryWhere notIn(String schema, String table, String column, Object[] in) { + maybeAddInnerJoin(schema, table, column); + super.notInClause(schema, table, column, in); + return this; + } + + public final QueryWhere notIn(String column, Object[] in) { + super.notInClause(metadata.schema(), metadata.table(), column, in); + return this; + } + + public final QueryWhere in(String schema, String table, String column, List in) { + maybeAddInnerJoin(schema, table, column); + super.inClause(schema, table, column, in.toArray()); + return this; + } + + public final QueryWhere in(String column, List in) { + super.inClause(metadata.schema(), metadata.table(), column, in.toArray()); + return this; + } + + public final QueryWhere notIn(String schema, String table, String column, List in) { + maybeAddInnerJoin(schema, table, column); + super.notInClause(schema, table, column, in.toArray()); + return this; + } + + public final QueryWhere notIn(String column, List in) { + super.notInClause(metadata.schema(), metadata.table(), column, in.toArray()); + return this; + } + + public final QueryWhere between(String schema, String table, String column, Object min, Object max) { + maybeAddInnerJoin(schema, table, column); + super.betweenClause(schema, table, column, min, max); + return this; + } + + public final QueryWhere between(String column, Object min, Object max) { + super.betweenClause(metadata.schema(), metadata.table(), column, min, max); + return this; + } + + public final QueryWhere notBetween(String schema, String table, String column, Object min, Object max) { + maybeAddInnerJoin(schema, table, column); + super.notBetweenClause(schema, table, column, min, max); + return this; + } + + public final QueryWhere notBetween(String column, Object min, Object max) { + super.notBetweenClause(metadata.schema(), metadata.table(), column, min, max); + return this; + } + + public final QueryWhere greaterThan(String schema, String table, String column, Object value) { + maybeAddInnerJoin(schema, table, column); + super.greaterThanClause(schema, table, column, value); + return this; + } + + public final QueryWhere greaterThan(String column, Object value) { + super.greaterThanClause(metadata.schema(), metadata.table(), column, value); + return this; + } + + public final QueryWhere greaterThanOrEqualTo(String schema, String table, String column, Object value) { + maybeAddInnerJoin(schema, table, column); + super.greaterThanOrEqualToClause(schema, table, column, value); + return this; + } + + public final QueryWhere greaterThanOrEqualTo(String column, Object value) { + super.greaterThanOrEqualToClause(metadata.schema(), metadata.table(), column, value); + return this; + } + + public final QueryWhere lessThan(String schema, String table, String column, Object value) { + maybeAddInnerJoin(schema, table, column); + super.lessThanClause(schema, table, column, value); + return this; + } + + public final QueryWhere lessThan(String column, Object value) { + super.lessThanClause(metadata.schema(), metadata.table(), column, value); + return this; + } + + public final QueryWhere lessThanOrEqualTo(String schema, String table, String column, Object value) { + maybeAddInnerJoin(schema, table, column); + super.lessThanOrEqualToClause(schema, table, column, value); + return this; + } + + private void maybeAddInnerJoin(String schema, String table, String column) { + if (schema.equals(metadata.schema()) && table.equals(metadata.table())) { + return; + } + + ForeignKey fkey = null; + SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(metadata.schema()); + Preconditions.checkNotNull(sqlSchema, "Schema not found: " + metadata.schema()); + SQLTable sqlTable = sqlSchema.getTable(metadata.table()); + Preconditions.checkNotNull(sqlTable, "Table not found: " + metadata.table()); + + for (ForeignKey foreignKey : sqlTable.getForeignKeys()) { + if (!foreignKey.getReferencedSchema().equals(schema)) { + continue; + } + if (!foreignKey.getReferencedTable().equals(table)) { + continue; + } + if (foreignKey.getLinkingColumns().stream().anyMatch(l -> l.columnInReferencedTable().equals(column))) { + fkey = foreignKey; + break; + } + } + + Preconditions.checkNotNull(fkey, "No foreign key found from " + metadata.schema() + "." + metadata.table() + " to " + schema + "." + table + " for column " + column); + super.addInnerJoin( + metadata.schema(), + metadata.table(), + fkey.getLinkingColumns().stream().map(Link::columnInReferringTable).toArray(String[]::new), + schema, + table, + fkey.getLinkingColumns().stream().map(Link::columnInReferencedTable).toArray(String[]::new) + ); + } + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/EqualsIngoreCaseClause.java b/core/src/main/java/net/staticstudios/data/query/clause/EqualsIngoreCaseClause.java new file mode 100644 index 00000000..1ac1bdfb --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/EqualsIngoreCaseClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class EqualsIngoreCaseClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final String value; + + public EqualsIngoreCaseClause(String schema, String table, String column, String value) { + this.schema = schema; + this.table = table; + this.column = column; + this.value = value; + } + + @Override + public List append(StringBuilder sb) { + sb.append("UPPER(\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\") = UPPER(?)"); + return List.of(value); + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/NotEqualsIngoreCaseClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotEqualsIngoreCaseClause.java new file mode 100644 index 00000000..ca248907 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/NotEqualsIngoreCaseClause.java @@ -0,0 +1,23 @@ +package net.staticstudios.data.query.clause; + +import java.util.List; + +public class NotEqualsIngoreCaseClause implements ValueClause { + private final String schema; + private final String table; + private final String column; + private final String value; + + public NotEqualsIngoreCaseClause(String schema, String table, String column, String value) { + this.schema = schema; + this.table = table; + this.column = column; + this.value = value; + } + + @Override + public List append(StringBuilder sb) { + sb.append("UPPER(\"").append(schema).append("\".\"").append(table).append("\".\"").append(column).append("\") <> UPPER(?)"); + return List.of(value); + } +} diff --git a/core/src/test/java/net/staticstudios/data/BatchInsertTest.java b/core/src/test/java/net/staticstudios/data/BatchInsertTest.java new file mode 100644 index 00000000..ff19973b --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/BatchInsertTest.java @@ -0,0 +1,136 @@ +package net.staticstudios.data; + +import net.staticstudios.data.insert.BatchInsert; +import net.staticstudios.data.insert.PostInsertAction; +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.misc.TestUtils; +import net.staticstudios.data.mock.user.MockUser; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.*; + +public class BatchInsertTest extends DataTest { + + @Test + public void testCompletableFuture() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + BatchInsert batch = dataManager.createBatchInsert(); + CompletableFuture cf = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("zakari") + .insert(batch); + + assertNotNull(cf); + + batch.insert(InsertMode.SYNC); + + MockUser user = cf.join(); + assertNotNull(user); + } + + @Test + public void testManyToManyPostInsertAction() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + List users = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + users.add(MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("user" + i) + .insert(InsertMode.SYNC)); + } + + UUID userId = UUID.randomUUID(); + + BatchInsert batch = dataManager.createBatchInsert(); + CompletableFuture cf = MockUser.builder(dataManager) + .id(userId) + .name("zakari") + .insert(batch); + + assertNotNull(cf); + + for (MockUser user : users) { + batch.addPostInsertAction(PostInsertAction.manyToMany(dataManager) + .referringClass(MockUser.class) + .referencedClass(MockUser.class) + .joinTableSchema("public") + .joinTableName("user_friends") + .referringId("id", userId) + .referencedId("id", user.id.get()) + .build()); + } + + batch.insert(InsertMode.SYNC); + + Connection pgConnection = getConnection(); + + try (PreparedStatement statement = pgConnection.prepareStatement("SELECT * FROM \"public\".\"user_friends\"")) { + ResultSet rs = statement.executeQuery(); + + assertEquals(users.size(), TestUtils.getResultCount(rs)); + } catch (Exception e) { + throw new RuntimeException(e); + } + + MockUser u = cf.join(); + + assertTrue(u.friends.containsAll(users)); + } + + @Test + public void testManyToManyPostInsertAction2() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("zakari") + .insert(InsertMode.SYNC); + + List friendIds = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + friendIds.add(UUID.randomUUID()); + } + + List> friendFutures = new ArrayList<>(); + BatchInsert batch = dataManager.createBatchInsert(); + for (int i = 0; i < 5; i++) { + friendFutures.add(MockUser.builder(dataManager) + .id(friendIds.get(i)) + .name("friend" + i) + .insert(batch)); + } + + for (UUID friendId : friendIds) { + batch.addPostInsertAction(PostInsertAction.manyToMany(dataManager) + .referringClass(MockUser.class) + .referencedClass(MockUser.class) + .joinTableSchema("public") + .joinTableName("user_friends") + .referringId("id", user.id.get()) + .referencedId("id", friendId) + .build()); + } + + batch.insert(InsertMode.SYNC); + + assertEquals(friendIds.size(), user.friends.size()); + + for (CompletableFuture friendFuture : friendFutures) { + MockUser friend = friendFuture.join(); + assertTrue(user.friends.contains(friend)); + } + } +} \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/ReferenceTest.java b/core/src/test/java/net/staticstudios/data/ReferenceTest.java index d90b3f77..0f461c92 100644 --- a/core/src/test/java/net/staticstudios/data/ReferenceTest.java +++ b/core/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -1,6 +1,6 @@ package net.staticstudios.data; -import net.staticstudios.data.insert.InsertContext; +import net.staticstudios.data.insert.BatchInsert; import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.mock.user.MockUser; import net.staticstudios.data.mock.user.MockUserSettings; @@ -11,6 +11,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import static org.junit.jupiter.api.Assertions.*; @@ -54,20 +55,20 @@ public void testCreateUserAndReferenceInSingleInsert() { dataManager.load(MockUser.class); UUID settingsId = UUID.randomUUID(); - InsertContext ctx = dataManager.createInsertContext(); - MockUserSettings.builder(dataManager) + BatchInsert batch = dataManager.createBatchInsert(); + CompletableFuture settingsCf = MockUserSettings.builder(dataManager) .id(settingsId) - .insert(ctx); + .insert(batch); - MockUser.builder(dataManager) + CompletableFuture userCf = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("test user") .settingsId(settingsId) - .insert(ctx); + .insert(batch); - ctx.insert(InsertMode.SYNC); - MockUserSettings settings = ctx.get(MockUserSettings.class); - MockUser user = ctx.get(MockUser.class); + batch.insert(InsertMode.SYNC); + MockUserSettings settings = settingsCf.join(); + MockUser user = userCf.join(); assertNotNull(settings); assertNotNull(user); 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 0f8c12d5..cac747e3 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 @@ -197,13 +197,15 @@ private SyntheticBuilderClass createBuilderBuilderClass(PsiClass parentClass) { insertModeMethod.addModifier(PsiModifier.FINAL); builderClass.addMethod(insertModeMethod); - SyntheticMethod insertContextMethod = new SyntheticMethod(parentClass, builderClass, "insert", null); - PsiType insertContextType = JavaPsiFacade.getElementFactory(parentClass.getProject()) - .createTypeFromText(Constants.INSERT_CONTEXT_FQN, parentClass); - insertContextMethod.addParameter("ctx", insertContextType); - insertContextMethod.addModifier(PsiModifier.PUBLIC); - insertContextMethod.addModifier(PsiModifier.FINAL); - builderClass.addMethod(insertContextMethod); + PsiType completableFutureType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createTypeFromText("java.util.concurrent.CompletableFuture<" + parentClass.getName() + ">", parentClass); + SyntheticMethod insertBatchMethod = new SyntheticMethod(parentClass, builderClass, "insert", completableFutureType); + PsiType batchInsertType = JavaPsiFacade.getElementFactory(parentClass.getProject()) + .createTypeFromText(Constants.BATCH_INSERT_FQN, parentClass); + insertBatchMethod.addParameter("batch", batchInsertType); + insertBatchMethod.addModifier(PsiModifier.PUBLIC); + insertBatchMethod.addModifier(PsiModifier.FINAL); + builderClass.addMethod(insertBatchMethod); return builderClass; } diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java index 8702a6f4..fabbe898 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java @@ -38,6 +38,7 @@ protected void process() { makeInsertContextMethod(persistentValues, references); makeInsertModeMethod(); + makeInsertBatchMethod(); } @@ -244,6 +245,136 @@ private void processReference(ParsedReference ref) { ), builderClassDecl); } + private void makeInsertBatchMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("insert"), + TypeApply( + chainDots("java", "util", "concurrent", "CompletableFuture"), + List.of( + Ident(dataClassDecl.name) + ) + ), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("batch"), + chainDots("net", "staticstudios", "data", "insert", "BatchInsert"), + null + ) + ), + List.nil(), + Block(0, List.of( + VarDef( + Modifiers(0), + names.fromString("___$cf"), + TypeApply( + chainDots("java", "util", "concurrent", "CompletableFuture"), + List.of( + Ident(dataClassDecl.name) + ) + ), + NewClass( + null, + List.nil(), + chainDots("java", "util", "concurrent", "CompletableFuture"), + List.nil(), + null + ) + ), + VarDef( + Modifiers(0), + names.fromString("___$ctx"), + chainDots("net", "staticstudios", "data", "insert", "InsertContext"), + Apply( + List.nil(), + Select( + Ident(names.fromString("dataManager")), + names.fromString("createInsertContext") + ), + List.nil() + ) + ), Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("this")), + names.fromString("insert") + ), + List.of(Ident(names.fromString("___$ctx"))) + ) + ), + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("batch")), + names.fromString("add") + ), + List.of(Ident(names.fromString("___$ctx"))) + ) + ), + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("___$ctx")), + names.fromString("addPostInsertAction") + ), + List.of( + Lambda( + List.of( + VarDef( + Modifiers(0), + names.fromString("___$inserted"), + chainDots("net", "staticstudios", "data", "insert", "InsertContext"), + null + ) + ), + Block(0, List.of( + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("___$cf")), + names.fromString("complete") + ), + List.of( + Apply( + List.nil(), + Select( + Ident(names.fromString("___$inserted")), + names.fromString("get") + ), + List.of( + Select( + Ident(dataClassDecl.name), + names.fromString("class") + ) + ) + ) + ) + ) + ) + )) + ) + ) + ) + ), + Return( + Ident(names.fromString("___$cf")) + ) + )), + null + ), builderClassDecl); + + //declare CF, + // create context + // call insert(ctx) + // add post action to complete the cf + } + private void makeInsertContextMethod(Collection parsedPersistentValues, Collection parsedReferences) { java.util.List bodyStatements = new ArrayList<>(); diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java index 1a1ce9d9..3f09284d 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java @@ -178,4 +178,8 @@ public JCTree.JCTypeUnion TypeUnion(List alternatives) { return treeMaker.at(pos).TypeUnion(alternatives); } + public JCTree.JCLambda Lambda(List params, JCTree body) { + return treeMaker.at(pos).Lambda(params, body); + } + } 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 117c989f..3338afc2 100644 --- a/utils/src/main/java/net/staticstudios/data/utils/Constants.java +++ b/utils/src/main/java/net/staticstudios/data/utils/Constants.java @@ -17,5 +17,6 @@ public class Constants { public static final String INSERT_STRATEGY_FQN = "net.staticstudios.data.InsertStrategy"; public static final String INSERT_MODE_FQN = "net.staticstudios.data.InsertMode"; public static final String INSERT_CONTEXT_FQN = "net.staticstudios.data.insert.InsertContext"; + public static final String BATCH_INSERT_FQN = "net.staticstudios.data.insert.BatchInsert"; } From 210f46e62445033fe403ea01426d97078d33516b Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Fri, 12 Dec 2025 07:13:41 -0500 Subject: [PATCH 54/75] parse values in InsertIntoJoinTableManyToManyPostInsertAction.Builder --- .../InsertIntoJoinTableManyToManyPostInsertAction.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/insert/InsertIntoJoinTableManyToManyPostInsertAction.java b/core/src/main/java/net/staticstudios/data/insert/InsertIntoJoinTableManyToManyPostInsertAction.java index be4d0981..116de83c 100644 --- a/core/src/main/java/net/staticstudios/data/insert/InsertIntoJoinTableManyToManyPostInsertAction.java +++ b/core/src/main/java/net/staticstudios/data/insert/InsertIntoJoinTableManyToManyPostInsertAction.java @@ -73,22 +73,22 @@ public Builder referencedClass(Class referencedClass) { } public Builder joinTableSchema(String joinTableSchema) { - this.joinTableSchema = joinTableSchema; + this.joinTableSchema = ValueUtils.parseValue(joinTableSchema); return this; } public Builder joinTableName(String joinTableName) { - this.joinTableName = joinTableName; + this.joinTableName = ValueUtils.parseValue(joinTableName); return this; } public Builder referringId(String column, Object value) { - this.referringIds.add(new ColumnValuePair(column, value)); + this.referringIds.add(new ColumnValuePair(ValueUtils.parseValue(column), value)); return this; } public Builder referencedId(String column, Object value) { - this.referencedIds.add(new ColumnValuePair(column, value)); + this.referencedIds.add(new ColumnValuePair(ValueUtils.parseValue(column), value)); return this; } From 0fb4990973631e2eb227f3d7446ed791005e2f74 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Fri, 12 Dec 2025 07:17:02 -0500 Subject: [PATCH 55/75] add ValueUtils.setValue --- .../data/util/EnvironmentVariableAccessor.java | 13 +++++++++++++ .../net/staticstudios/data/util/ValueUtils.java | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/core/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java b/core/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java index d36f3336..680fc178 100644 --- a/core/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java +++ b/core/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java @@ -1,7 +1,20 @@ package net.staticstudios.data.util; +import java.util.HashMap; +import java.util.Map; + public class EnvironmentVariableAccessor { + private final Map override = new HashMap<>(); + + public void set(String key, String value) { + override.put(key, value); + } + public String getEnv(String name) { + String value = override.get(name); + if (value != null) { + return value; + } return System.getenv(name); } } diff --git a/core/src/main/java/net/staticstudios/data/util/ValueUtils.java b/core/src/main/java/net/staticstudios/data/util/ValueUtils.java index c9751234..277b988a 100644 --- a/core/src/main/java/net/staticstudios/data/util/ValueUtils.java +++ b/core/src/main/java/net/staticstudios/data/util/ValueUtils.java @@ -11,6 +11,10 @@ public class ValueUtils { @VisibleForTesting public static EnvironmentVariableAccessor ENVIRONMENT_VARIABLE_ACCESSOR = new EnvironmentVariableAccessor(); + public static void setValue(String key, String value) { + ENVIRONMENT_VARIABLE_ACCESSOR.set(key, value); + } + public static String parseValue(String encoded) { Preconditions.checkNotNull(encoded, "Encoded value cannot be null"); Matcher matcher = ENVIRONMENT_VARIABLE_PATTERN.matcher(encoded); From e461111f7771a2d7f57a4bc2435948efa9800a7b Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Thu, 18 Dec 2025 18:03:02 -0500 Subject: [PATCH 56/75] changes --- .../data/insert/PostInsertAction.java | 27 +++++++++++++++++-- .../staticstudios/data/util/ValueUtils.java | 2 +- .../ide/intellij/IntelliJPluginUtils.java | 5 ++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java b/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java index b3afd810..fff12139 100644 --- a/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java +++ b/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java @@ -1,11 +1,11 @@ package net.staticstudios.data.insert; +import com.google.common.base.Preconditions; import net.staticstudios.data.DataManager; import net.staticstudios.data.PersistentCollection; import net.staticstudios.data.UniqueData; import net.staticstudios.data.impl.data.PersistentManyToManyCollectionImpl; -import net.staticstudios.data.util.ColumnValuePair; -import net.staticstudios.data.util.SQlStatement; +import net.staticstudios.data.util.*; import java.util.List; @@ -47,6 +47,29 @@ static InsertIntoJoinTableManyToManyPostInsertAction.Builder manyToMany(Persiste return builder; } + static InsertIntoJoinTableManyToManyPostInsertAction.Builder manyToMany(Class holderClass, String collectionJoinTableSchema, String collectionJoinTableName) { + DataManager dataManager = DataManager.getInstance(); + UniqueDataMetadata metadata = dataManager.getMetadata(holderClass); + PersistentManyToManyCollectionMetadata collectionMetadata = null; + for (PersistentCollectionMetadata persistentCollectionMetadata : metadata.persistentCollectionMetadata().values()) { + if (persistentCollectionMetadata instanceof PersistentManyToManyCollectionMetadata manyToManyMetadata) { + String joinTableSchema = manyToManyMetadata.getJoinTableSchema(dataManager); + String joinTableName = manyToManyMetadata.getJoinTableName(dataManager); + if (joinTableSchema.equals(collectionJoinTableSchema) && joinTableName.equals(collectionJoinTableName)) { + collectionMetadata = manyToManyMetadata; + break; + } + } + } + + Preconditions.checkNotNull(collectionMetadata, "Could not find PersistentManyToManyCollectionMetadata for the provided class and join table!"); + return manyToMany(dataManager) + .referringClass(holderClass) + .referencedClass(collectionMetadata.getReferencedType()) + .joinTableSchema(collectionMetadata.getJoinTableSchema(dataManager)) + .joinTableName(collectionMetadata.getJoinTableName(dataManager)); + } + List getStatements(); } diff --git a/core/src/main/java/net/staticstudios/data/util/ValueUtils.java b/core/src/main/java/net/staticstudios/data/util/ValueUtils.java index 277b988a..42d95165 100644 --- a/core/src/main/java/net/staticstudios/data/util/ValueUtils.java +++ b/core/src/main/java/net/staticstudios/data/util/ValueUtils.java @@ -15,7 +15,7 @@ public static void setValue(String key, String value) { ENVIRONMENT_VARIABLE_ACCESSOR.set(key, value); } - public static String parseValue(String encoded) { + public static String parseValue(String encoded) { //TODO: we should cache parsed values Preconditions.checkNotNull(encoded, "Encoded value cannot be null"); Matcher matcher = ENVIRONMENT_VARIABLE_PATTERN.matcher(encoded); StringBuilder sb = new StringBuilder(); 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 dcd25582..a773bef7 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 @@ -37,6 +37,11 @@ public static boolean extendsClass(PsiClass psiClass, String classFqn) { extendsClass = true; break; } + PsiClass superClass = superType.resolve(); + if (superClass != null && extendsClass(superClass, classFqn)) { + extendsClass = true; + break; + } } return extendsClass; From 05eaec5adf6855b6e7eba711b13a5af1b817879e Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 21 Dec 2025 17:44:02 -0500 Subject: [PATCH 57/75] add support for equalsIgnoreCaseClause and notEqualsIgnoreCaseClause --- .../data/query/BaseQueryWhere.java | 1 - .../ide/intellij/query/QueryBuilderUtils.java | 3 + .../query/clause/IsIgnoreCaseClause.java | 26 ++++++ .../query/clause/IsNotIgnoreCaseClause.java | 26 ++++++ .../javac/javac/QueryBuilderProcessor.java | 86 ++++++++++++++++++- 5 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsIgnoreCaseClause.java create mode 100644 intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotIgnoreCaseClause.java 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 a14a0592..467c1030 100644 --- a/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java @@ -96,7 +96,6 @@ protected void notEqualsClause(String schema, String table, String column, @Null setValueClause(new NotEqualsClause(schema, table, column, o)); } - //todo: add IJ and processor support for equalsIgnoreCaseClause and notEqualsIgnoreCaseClause protected void equalsIgnoreCaseClause(String schema, String table, String column, @NotNull String eq) { setValueClause(new EqualsIngoreCaseClause(schema, table, column, eq)); } 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 105c3add..9f2d3a0b 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 @@ -30,6 +30,9 @@ public class QueryBuilderUtils { pvClauses.add(new IsLikeClause()); pvClauses.add(new IsNotLikeClause()); + pvClauses.add(new IsIgnoreCaseClause()); + pvClauses.add(new IsNotIgnoreCaseClause()); + pvClauses.add(new IsGreaterThanClause()); pvClauses.add(new IsLessThanClause()); pvClauses.add(new IsGreaterThanOrEqualToClause()); diff --git a/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsIgnoreCaseClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsIgnoreCaseClause.java new file mode 100644 index 00000000..ac25b431 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsIgnoreCaseClause.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; +import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class IsIgnoreCaseClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return IntelliJPluginUtils.genericTypeIs(psiField.getType(), String.class.getName()); + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsIgnoreCase"; + } + + @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/IsNotIgnoreCaseClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotIgnoreCaseClause.java new file mode 100644 index 00000000..c2d2e5da --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotIgnoreCaseClause.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.*; +import com.intellij.psi.impl.light.LightParameter; +import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +import java.util.List; + +public class IsNotIgnoreCaseClause implements QueryClause { + + @Override + public boolean matches(PsiField psiField, boolean nullable) { + return IntelliJPluginUtils.genericTypeIs(psiField.getType(), String.class.getName()); + } + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNotIgnoreCase"; + } + + @Override + public List getMethodParamTypes(PsiManager manager, PsiType fieldType, PsiElement scope) { + return List.of(new LightParameter("value", fieldType, scope)); + } +} 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 f30d8a0a..2479ea59 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 @@ -273,9 +273,7 @@ private void processValue(ParsedPersistentValue pv) { String tableFieldName = storeTable(pv.getFieldName(), pv.getTable()); String columnFieldName = storeColumn(pv.getFieldName(), pv.getColumn()); - ParsedForeignPersistentValue fpv = null; - if (pv instanceof ParsedForeignPersistentValue _fpv) { - fpv = _fpv; + if (pv instanceof ParsedForeignPersistentValue fpv) { storeLinks(fpv.getFieldName(), fpv.getLinks()); } @@ -295,6 +293,9 @@ private void processValue(ParsedPersistentValue pv) { if (typeUtils.isType(pv.getType(), String.class)) { addIsLikeMethod(pv, schemaFieldName, tableFieldName, columnFieldName); addIsNotLikeMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + + addIsIgnoreCaseMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsNotIgnoreCaseMethod(pv, schemaFieldName, tableFieldName, columnFieldName); } if (typeUtils.isNumericType(pv.getType()) || typeUtils.isType(pv.getType(), Timestamp.class)) { @@ -992,6 +993,85 @@ private void addIsNotBetweenMethod(ParsedPersistentValue pv, String schemaFieldN ), builderClassDecl); } + private void addIsIgnoreCaseMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsIgnoreCase"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("value"), + Ident(names.fromString("String")), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("equalsIgnoreCaseClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString("value")) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + + + private void addIsNotIgnoreCaseMethod(ParsedPersistentValue pv, String schemaFieldName, String tableFieldName, String columnFieldName) { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName() + "IsNotIgnoreCase"), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("value"), + Ident(names.fromString("String")), + null + ) + ), + List.nil(), + Block(0, clause(pv, + Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("super")), + names.fromString("notEqualsIgnoreCaseClause") + ), + List.of( + Ident(names.fromString(schemaFieldName)), + Ident(names.fromString(tableFieldName)), + Ident(names.fromString(columnFieldName)), + Ident(names.fromString("value")) + ) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + } + private void addGroupMethod() { createMethod(MethodDef( Modifiers(Flags.PUBLIC | Flags.FINAL), From 762d3ebc9ca004732cf5bc57527a7fd730adf485 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sat, 27 Dec 2025 19:19:23 -0500 Subject: [PATCH 58/75] changes & fixes --- core/build.gradle | 9 -- .../net/staticstudios/data/DataManager.java | 104 ++++++++++++------ .../net/staticstudios/data/StaticData.java | 10 ++ .../PersistentManyToManyCollectionImpl.java | 2 +- .../data/impl/h2/H2DataAccessor.java | 7 ++ .../staticstudios/data/parse/SQLBuilder.java | 29 +++-- .../staticstudios/data/parse/SQLColumn.java | 6 +- .../staticstudios/data/parse/SQLTable.java | 6 +- .../data/util/ReflectionUtils.java | 4 +- .../data/mock/user/MockUser.java | 3 + processor/build.gradle | 10 +- 11 files changed, 131 insertions(+), 59 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index 78352563..37d86955 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -31,7 +31,6 @@ dependencies { tasks.named('build') { dependsOn(shadowJar) - } tasks.named("publish") { @@ -51,14 +50,6 @@ javadoc { options.tags = ["implSpec", "apiNote", "implNote"] } -shadowJar { - archiveClassifier.set('all') - - dependencies { - include(project(":annotations")) - } -} - publishing { repositories { maven { diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index ac98fa67..434d42c4 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -17,6 +17,7 @@ import net.staticstudios.data.utils.Link; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -24,6 +25,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; @@ -748,48 +750,61 @@ public final void load(Class... classes) { } public UniqueDataMetadata extractMetadata(Class clazz) { + Data dataAnnotation = clazz.getAnnotation(Data.class); + return extractMetadata(clazz, dataAnnotation); + } + + public UniqueDataMetadata extractMetadata(Class clazz, Data fallbackDataAnnotation) { + logger.debug("Extracting metadata for UniqueData class {}", clazz.getName()); Preconditions.checkArgument(!uniqueDataMetadataMap.containsKey(clazz), "UniqueData class %s has already been parsed", clazz.getName()); Data dataAnnotation = clazz.getAnnotation(Data.class); + if (dataAnnotation == null) { + dataAnnotation = fallbackDataAnnotation; + } Preconditions.checkNotNull(dataAnnotation, "UniqueData class %s is missing @Data annotation", clazz.getName()); - List idColumns = new ArrayList<>(); - for (Field field : ReflectionUtils.getFields(clazz, PersistentValue.class)) { - IdColumn idColumnAnnotation = field.getAnnotation(IdColumn.class); - if (idColumnAnnotation == null) { - continue; - } - idColumns.add(new ColumnMetadata( - ValueUtils.parseValue(dataAnnotation.schema()), - ValueUtils.parseValue(dataAnnotation.table()), - ValueUtils.parseValue(idColumnAnnotation.name()), - ReflectionUtils.getGenericType(field), - false, - false, - "" - )); - } - Preconditions.checkArgument(!idColumns.isEmpty(), "UniqueData class %s must have at least one @IdColumn annotated PersistentValue field", clazz.getName()); - String schema = ValueUtils.parseValue(dataAnnotation.schema()); - String table = ValueUtils.parseValue(dataAnnotation.table()); - Map persistentCollectionMetadataMap = new HashMap<>(); - persistentCollectionMetadataMap.putAll(PersistentOneToManyCollectionImpl.extractMetadata(this, clazz)); - persistentCollectionMetadataMap.putAll(PersistentManyToManyCollectionImpl.extractMetadata(clazz)); - persistentCollectionMetadataMap.putAll(PersistentOneToManyValueCollectionImpl.extractMetadata(clazz, schema)); - UniqueDataMetadata metadata = new UniqueDataMetadata( - clazz, - schema, - table, - idColumns, - CachedValueImpl.extractMetadata(schema, table, clazz), - PersistentValueImpl.extractMetadata(schema, table, clazz), - ReferenceImpl.extractMetadata(clazz), - persistentCollectionMetadataMap - ); - uniqueDataMetadataMap.put(clazz, metadata); + UniqueDataMetadata metadata = null; + + if (!Modifier.isAbstract(clazz.getModifiers())) { + List idColumns = new ArrayList<>(); + for (Field field : ReflectionUtils.getFields(clazz, PersistentValue.class)) { + IdColumn idColumnAnnotation = field.getAnnotation(IdColumn.class); + if (idColumnAnnotation == null) { + continue; + } + idColumns.add(new ColumnMetadata( + ValueUtils.parseValue(dataAnnotation.schema()), + ValueUtils.parseValue(dataAnnotation.table()), + ValueUtils.parseValue(idColumnAnnotation.name()), + ReflectionUtils.getGenericType(field), + false, + false, + "" + )); + } + Preconditions.checkArgument(!idColumns.isEmpty(), "UniqueData class %s must have at least one @IdColumn annotated PersistentValue field", clazz.getName()); + String schema = ValueUtils.parseValue(dataAnnotation.schema()); + String table = ValueUtils.parseValue(dataAnnotation.table()); + Map persistentCollectionMetadataMap = new HashMap<>(); + persistentCollectionMetadataMap.putAll(PersistentOneToManyCollectionImpl.extractMetadata(this, clazz)); + persistentCollectionMetadataMap.putAll(PersistentManyToManyCollectionImpl.extractMetadata(clazz)); + persistentCollectionMetadataMap.putAll(PersistentOneToManyValueCollectionImpl.extractMetadata(clazz, schema)); + metadata = new UniqueDataMetadata( + clazz, + schema, + table, + idColumns, + CachedValueImpl.extractMetadata(schema, table, clazz), + PersistentValueImpl.extractMetadata(schema, table, clazz), + ReferenceImpl.extractMetadata(clazz), + persistentCollectionMetadataMap + ); + uniqueDataMetadataMap.put(clazz, metadata); + } for (Field field : ReflectionUtils.getFields(clazz, Relation.class)) { Class genericType = ReflectionUtils.getGenericType(field); - if (genericType != null && UniqueData.class.isAssignableFrom(genericType)) { + if (genericType != null && !Modifier.isAbstract(genericType.getModifiers()) && UniqueData.class.isAssignableFrom(genericType)) { Class dependencyClass = genericType.asSubclass(UniqueData.class); if (!uniqueDataMetadataMap.containsKey(dependencyClass)) { extractMetadata(dependencyClass); @@ -797,6 +812,14 @@ public UniqueDataMetadata extractMetadata(Class clazz) { } } + Class superClass = clazz.getSuperclass(); + if (superClass != null && UniqueData.class.isAssignableFrom(superClass) && superClass != UniqueData.class) { + Class superUniqueDataClass = superClass.asSubclass(UniqueData.class); + if (!uniqueDataMetadataMap.containsKey(superUniqueDataClass)) { + extractMetadata(superUniqueDataClass, dataAnnotation); + } + } + return metadata; } @@ -1552,4 +1575,15 @@ public T copy(T value, Class dataType) { // instance.markDeleted(); // return instance; // } + + /** + * Block the calling thread until all previously enqueued tasks have been completed + */ + @Blocking + public void flushTaskQueue() { + //This will add a task to the queue and block until it's done + taskQueue.submitTask(connection -> { + //Ignore + }).join(); + } } diff --git a/core/src/main/java/net/staticstudios/data/StaticData.java b/core/src/main/java/net/staticstudios/data/StaticData.java index 9a00bfac..149cf950 100644 --- a/core/src/main/java/net/staticstudios/data/StaticData.java +++ b/core/src/main/java/net/staticstudios/data/StaticData.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import net.staticstudios.data.insert.BatchInsert; import net.staticstudios.data.query.QueryBuilder; +import org.jetbrains.annotations.Blocking; /** * Entry point for initializing and interacting with the StaticData system. @@ -71,4 +72,13 @@ public static QueryBuilder query(Class type) { assertInit(); return new QueryBuilder<>(DataManager.getInstance(), type); } + + /** + * Block the calling thread until all previously enqueued tasks have been completed + */ + @Blocking + public static void flushTaskQueue() { + assertInit(); + DataManager.getInstance().flushTaskQueue(); + } } 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 4a5144fb..af972ed9 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 @@ -101,7 +101,7 @@ public static String getReferencedTableColumnPrefix(String dataTable, String ref if (dataTable.equals(referencedTable)) { return referencedTable + "_ref"; } - return ""; + return referencedTable; } public static List getJoinTableToDataTableLinks(String dataTable, String links) { 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 f66449b9..edb0762c 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 @@ -64,6 +64,13 @@ public class H2DataAccessor implements DataAccessor { private final Set knownRedisPartialKeys = ConcurrentHashMap.newKeySet(); public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener, RedisListener redisListener, TaskQueue taskQueue) { + + try { + Class.forName("org.h2.Driver"); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to load H2 Driver", e); + } + this.taskQueue = taskQueue; this.postgresListener = postgresListener; this.redisListener = redisListener; 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 6ecfcd87..246a5be2 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.*; /** @@ -59,6 +60,7 @@ public static List parseLinks(String links) { public List parse(Class clazz) { Preconditions.checkNotNull(clazz, "Class cannot be null"); + logger.trace("Starting SQL parsing for class {}", clazz.getName()); Set> visited = walk(clazz); Map schemas = new HashMap<>(); @@ -80,6 +82,7 @@ public List parse(Class clazz) { for (SQLTable newTable : newSchema.getTables()) { SQLTable existingTable = existingSchema.getTable(newTable.getName()); if (existingTable == null) { + newTable.setSchema(existingSchema); existingSchema.addTable(newTable); continue; } @@ -89,6 +92,7 @@ public List parse(Class clazz) { Preconditions.checkState(existingColumn.equals(newColumn), "Column " + newColumn.getName() + " in referringTable " + newTable.getName() + " has conflicting definitions! Existing: " + existingColumn + ", New: " + newColumn); continue; } + newColumn.setTable(existingTable); existingTable.addColumn(newColumn); } } @@ -257,7 +261,7 @@ private void walk(Class clazz, Set genericType = ReflectionUtils.getGenericType(field); - if (genericType == null || !UniqueData.class.isAssignableFrom(genericType)) { + if (genericType == null || !UniqueData.class.isAssignableFrom(genericType) || Modifier.isAbstract(genericType.getModifiers())) { continue; } Class related = genericType.asSubclass(UniqueData.class); @@ -291,6 +295,10 @@ private void parseIndividualRelations(Class clazz, Map genericType = ReflectionUtils.getGenericType(field); + if (genericType != null && Modifier.isAbstract(genericType.getModifiers())) { + continue; + } parseReference(clazz, schemas, dataAnnotation, metadata, field); parsePersistentCollection(clazz, schemas, dataAnnotation, metadata, field); } @@ -477,7 +485,10 @@ private void parseReference(Class clazz, Map clazz, Map columnType = null; SQLColumn columnInReferringTable = table.getColumn(link.columnInReferringTable()); @@ -553,7 +562,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, columnType, link.columnInReferencedTable(), false, false, false, null); + SQLColumn linkingColumn = new SQLColumn(referencedTable, dataManager.getSerializedType(columnType), link.columnInReferencedTable(), false, false, false, null); referencedTable.addColumn(linkingColumn); } @@ -643,7 +652,7 @@ private void parseManyToManyPersistentCollection(ManyToMany manyToMany, Class type; private final String name; private final boolean nullable; private final boolean indexed; private final boolean unique; private final @Nullable String defaultValue; + private SQLTable table; public SQLColumn(SQLTable table, Class type, String name, boolean nullable, boolean indexed, boolean unique, @Nullable String defaultValue) { this.table = table; @@ -23,6 +23,10 @@ public SQLColumn(SQLTable table, Class type, String name, boolean nullable, b this.defaultValue = defaultValue; } + public void setTable(SQLTable table) { + this.table = table; + } + public SQLTable getTable() { return table; } diff --git a/core/src/main/java/net/staticstudios/data/parse/SQLTable.java b/core/src/main/java/net/staticstudios/data/parse/SQLTable.java index 25bb807b..077a8791 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLTable.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -7,12 +7,12 @@ import java.util.*; public class SQLTable { - private final SQLSchema schema; private final String name; private final List idColumns; private final Map columns; private final Set foreignKeys; private final Set triggers; + private SQLSchema schema; public SQLTable(SQLSchema schema, String name, List idColumns) { this.schema = schema; @@ -23,6 +23,10 @@ public SQLTable(SQLSchema schema, String name, List idColumns) { this.triggers = new HashSet<>(); } + public void setSchema(SQLSchema schema) { + this.schema = schema; + } + public SQLSchema getSchema() { return schema; } 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 a240c978..d58d96a8 100644 --- a/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java +++ b/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java @@ -66,7 +66,9 @@ public static List getFieldInstances(Object instance, Class fieldType) if (field.getGenericType() instanceof Class) { return (Class) field.getGenericType(); } else if (field.getGenericType() instanceof java.lang.reflect.ParameterizedType parameterizedType) { - return (Class) parameterizedType.getActualTypeArguments()[0]; + if (parameterizedType.getActualTypeArguments()[0] instanceof Class) { + return (Class) parameterizedType.getActualTypeArguments()[0]; + } } return null; } diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index 07848c52..2882b9b1 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -9,6 +9,8 @@ @Data(schema = "public", table = "users") public class MockUser extends UniqueData { //todo: test inheritance properly. test the ij plugin and AP too. + //todo: + // test composite id cols, using non primitive types. also test collection and reference relations using non primitive types @IdColumn(name = "id") public PersistentValue id = PersistentValue.of(this, UUID.class); @@ -68,6 +70,7 @@ public class MockUser extends UniqueData { .withFallback(0); @Delete(DeleteStrategy.CASCADE) //todo: impl delete strategy for many to many collections @ManyToMany(link = "id=id", joinTable = "user_friends") + //todo: there should be a way to specify the column names for the join tables within the ManyToMany annotation. this only matters if the referring and referenced tables are the same. public PersistentCollection friends = PersistentCollection.of(this, MockUser.class) .onAdd(MockUser.class, (user, added) -> user.friendAdditions.set(user.friendAdditions.get() + 1)) .onRemove(MockUser.class, (user, removed) -> user.friendRemovals.set(user.friendRemovals.get() + 1)); diff --git a/processor/build.gradle b/processor/build.gradle index 0772c593..912902bf 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -1,6 +1,7 @@ plugins { id 'java' id 'maven-publish' + id 'com.gradleup.shadow' version '8.3.3' } dependencies { @@ -27,10 +28,15 @@ tasks.withType(JavaCompile).configureEach { ] } +build { + dependsOn(shadowJar) +} + tasks.named("publish") { dependsOn(build) } + //java { // withSourcesJar() // withJavadocJar() @@ -47,8 +53,10 @@ publishing { } publications { maven(MavenPublication) { - from components.java artifactId = 'static-data-processor' + artifact(tasks.shadowJar) { + classifier = null + } pom { name = 'Static Data Processor' description = 'Compile-time generator for Static Data.' From 1f5d865ae81809cce8f436919b4eaaa87a101cfa Mon Sep 17 00:00:00 2001 From: Noah <59799222+Leguan16@users.noreply.github.com> Date: Sun, 28 Dec 2025 02:11:02 +0100 Subject: [PATCH 59/75] Add ID to Run Tests step in build workflow --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a9348ca..781aa989 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,6 +32,7 @@ jobs: run: ./gradlew build -x test -PStaticStudiosUsername=github -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} - name: Run Tests + id: test run: ./gradlew test --info --stacktrace -PStaticStudiosUsername=github -PStaticStudiosPassword=${{ secrets.REPOSITORY_SECRET }} | tee gradle-test.log continue-on-error: true @@ -47,4 +48,4 @@ jobs: - name: Fail if tests failed if: steps.test.outcome != 'success' - run: exit 1 \ No newline at end of file + run: exit 1 From ac7ad1860e24c45cdbf87acaa2abff207cc16edc Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 28 Dec 2025 14:36:46 -0500 Subject: [PATCH 60/75] alter how insert statements are generated. - previously each insert context only knew about itself. this lead to statements being generated to satisfy dependencies. this is fine, however in the context of a batch, the statement may already be present. this change makes every statement aware of every other statement within the batch --- core/build.gradle | 19 +- .../net/staticstudios/data/DataManager.java | 220 +++---------- .../data/impl/h2/H2DataAccessor.java | 8 +- .../data/insert/InsertContext.java | 15 +- .../data/util/InsertStatement.java | 308 ++++++++++++++++++ .../data/mock/user/MockUser.java | 1 - .../javac/javac/BuilderProcessor.java | 156 ++++++++- .../javac/ParsedForeignPersistentValue.java | 2 +- .../javac/javac/ParsedPersistentValue.java | 14 +- .../compiler/javac/javac/ParsedReference.java | 15 +- .../compiler/javac/javac/ParsedRelation.java | 9 + .../compiler/javac/javac/ParsedValue.java | 21 ++ 12 files changed, 560 insertions(+), 228 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/util/InsertStatement.java create mode 100644 processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedRelation.java create mode 100644 processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedValue.java diff --git a/core/build.gradle b/core/build.gradle index 37d86955..1235e878 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -28,6 +28,9 @@ dependencies { testAnnotationProcessor project(':processor') } +shadowJar { + archiveClassifier.set('') +} tasks.named('build') { dependsOn(shadowJar) @@ -51,19 +54,15 @@ javadoc { } publishing { - repositories { - maven { - credentials(org.gradle.api.credentials.PasswordCredentials.class) - name = "StaticStudios" - setUrl("https://repo.staticstudios.net/private/") - } - } publications { maven(MavenPublication) { artifactId = 'static-data' - artifact(shadowJar) { - classifier = null - } + + from components.shadow + + artifact sourcesJar + artifact javadocJar + pom { name = 'Static Data' description = 'Data library used by StaticStudios.' diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 434d42c4..d9ec303e 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -10,7 +10,10 @@ import net.staticstudios.data.insert.BatchInsert; import net.staticstudios.data.insert.InsertContext; import net.staticstudios.data.insert.PostInsertAction; -import net.staticstudios.data.parse.*; +import net.staticstudios.data.parse.DDLStatement; +import net.staticstudios.data.parse.SQLBuilder; +import net.staticstudios.data.parse.SQLColumn; +import net.staticstudios.data.parse.SQLTable; import net.staticstudios.data.primative.Primitives; import net.staticstudios.data.util.*; import net.staticstudios.data.util.TaskQueue; @@ -1115,10 +1118,24 @@ public void insert(InsertContext context, InsertMode insertMode) { } public void insert(BatchInsert batch, InsertMode insertMode) { - List statements = new ArrayList<>(); + List insertStatements = new LinkedList<>(); for (InsertContext context : batch.getInsertContexts()) { - statements.addAll(generateStatements(context)); + insertStatements.addAll(generateInsertStatements(context)); + } + + for (InsertStatement insertStatement : List.copyOf(insertStatements)) { + insertStatement.calculateRequiredDependencies(); + insertStatements.addAll(insertStatement.createUnmetDependencyStatements(insertStatements)); + } + + InsertStatement.checkForCycles(insertStatements); + + insertStatements = InsertStatement.sort(insertStatements); + + List statements = new ArrayList<>(); + for (InsertStatement insertStatement : insertStatements) { + statements.add(insertStatement.asStatement()); } for (PostInsertAction action : batch.getPostInsertActions()) { @@ -1137,193 +1154,40 @@ public void insert(BatchInsert batch, InsertMode insertMode) { } } - private List generateStatements(InsertContext insertContext) { - //todo: when inserting validate all id and required values are present - this will be enforced by h2, but we should do it here for better logging/errors. - Set tables = new HashSet<>(); - insertContext.getEntries().forEach((simpleColumnMetadata, o) -> { - SQLTable table = Objects.requireNonNull(sqlBuilder.getSchema(simpleColumnMetadata.schema())).getTable(simpleColumnMetadata.table()); - tables.add(table); - }); - - // handle foreign keys. for each foreign key, if we have a value for the local column, we need to set the value for the foreign column. this consists of linking ids mostly. - for (SQLTable table : tables) { - for (ForeignKey fKey : table.getForeignKeys()) { - SQLSchema referencedSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getReferencedSchema())); - SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(fKey.getReferencedTable())); - for (Link link : fKey.getLinkingColumns()) { - String myColumnName = link.columnInReferringTable(); - String otherColumnName = link.columnInReferencedTable(); - SQLColumn otherColumn = Objects.requireNonNull(referencedTable.getColumn(otherColumnName)); - - // if its nullable and we don't have a value, skip it. - if (otherColumn.isNullable()) { - continue; - } - - insertContext.set(fKey.getReferencedSchema(), fKey.getReferencedTable(), otherColumn.getName(), insertContext.getEntries().get(new SimpleColumnMetadata(fKey.getReferringSchema(), fKey.getReferringTable(), myColumnName, otherColumn.getType())), InsertStrategy.PREFER_EXISTING); - } - } - } + private List generateInsertStatements(InsertContext insertContext) { + List insertStatements = new LinkedList<>(); - tables.clear(); // rebuild the referringTable set in case we added any new tables from foreign keys + Map> tableColumnsMap = new HashMap<>(); insertContext.getEntries().forEach((simpleColumnMetadata, o) -> { SQLTable table = Objects.requireNonNull(sqlBuilder.getSchema(simpleColumnMetadata.schema())).getTable(simpleColumnMetadata.table()); - tables.add(table); + tableColumnsMap.computeIfAbsent(table, k -> new LinkedList<>()) + .add(simpleColumnMetadata); }); - // Build dependency graph: referringTable -> set of tables it depends on - Map> dependencyGraph = new HashMap<>(); - for (SQLTable table : tables) { - Set dependsOn = new HashSet<>(); - for (ForeignKey fKey : table.getForeignKeys()) { - SQLSchema referencedSchema = Objects.requireNonNull(sqlBuilder.getSchema(fKey.getReferencedSchema())); - SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(fKey.getReferencedTable())); - - boolean addDependency = true; - // if one of the linking columnsInReferringTable is not present in the insert context, we can't add the dependency - for (Link link : fKey.getLinkingColumns()) { - Object value = insertContext.getEntries().entrySet().stream() - .filter(entry -> { - SimpleColumnMetadata key = entry.getKey(); - return key.schema().equals(fKey.getReferencedSchema()) && - key.table().equals(fKey.getReferencedTable()) && - key.name().equals(link.columnInReferencedTable()); - }) - .findFirst() - .orElse(null); - - if (value == null) { - addDependency = false; - break; - } - } + for (SQLTable table : tableColumnsMap.keySet()) { + List idColumns = new ArrayList<>(); + Map otherColumnValues = new HashMap<>(); + List columns = tableColumnsMap.get(table); + for (SimpleColumnMetadata column : columns) { + Object value = insertContext.getEntries().get(column); - if (addDependency) { - dependsOn.add(referencedTable); - } - } - if (!dependsOn.isEmpty()) { - dependencyGraph.put(table.getName(), dependsOn); - } - } - - // DFS to detect cycles - Set visited = new HashSet<>(); - Set stack = new HashSet<>(); - for (SQLTable table : tables) { - if (hasCycle(table, dependencyGraph, visited, stack)) { - throw new IllegalStateException(String.format("Cycle detected in foreign key dependencies involving referringTable %s.%s", table.getSchema().getName(), table.getName())); - } - } - - // Topological sort for insert order - List orderedTables = new ArrayList<>(); - visited.clear(); - for (SQLTable table : tables) { - topoSort(table, dependencyGraph, visited, orderedTables); - } - - List sqlStatements = new ArrayList<>(); - - Map> columnsToInsert = new HashMap<>(); - for (Map.Entry entry : insertContext.getEntries().entrySet()) { - SimpleColumnMetadata column = entry.getKey(); - columnsToInsert.computeIfAbsent(column.table(), k -> new ArrayList<>()) - .add(column); - } - - for (SQLTable table : orderedTables) { - String schemaName = table.getSchema().getName(); - String tableName = table.getName(); - List columnsInTable = columnsToInsert.get(tableName); - - - StringBuilder h2SqlBuilder = new StringBuilder("MERGE INTO \""); - h2SqlBuilder.append(schemaName).append("\".\"").append(tableName).append("\" AS target USING (VALUES ("); - Map conflicts = new HashMap<>(); - h2SqlBuilder.append("?, ".repeat(columnsInTable.size())); - h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); - h2SqlBuilder.append(")) AS source ("); - for (SimpleColumnMetadata column : columnsInTable) { - h2SqlBuilder.append("\"").append(column.name()).append("\", "); - InsertStrategy strategy = insertContext.getInsertStrategies().get(column); - if (strategy != null) { - conflicts.put(column, strategy); - } - } - h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); - h2SqlBuilder.append(") ON "); - for (ColumnMetadata idColumn : table.getIdColumns()) { - h2SqlBuilder.append("target.\"").append(idColumn.name()).append("\" = source.\"").append(idColumn.name()).append("\" AND "); - } - h2SqlBuilder.setLength(h2SqlBuilder.length() - 5); - h2SqlBuilder.append(" WHEN NOT MATCHED THEN INSERT ("); - for (SimpleColumnMetadata column : columnsInTable) { - h2SqlBuilder.append("\"").append(column.name()).append("\", "); - } - h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); - h2SqlBuilder.append(") VALUES ("); - for (SimpleColumnMetadata column : columnsInTable) { - h2SqlBuilder.append("source.\"").append(column.name()).append("\", "); - } - h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); - h2SqlBuilder.append(")"); - - List overwriteExisting = new ArrayList<>(); - for (Map.Entry entry : conflicts.entrySet()) { - if (entry.getValue() == InsertStrategy.OVERWRITE_EXISTING) { - overwriteExisting.add(entry.getKey()); - } - } - if (!overwriteExisting.isEmpty()) { - h2SqlBuilder.append(" WHEN MATCHED THEN UPDATE SET "); - for (SimpleColumnMetadata column : overwriteExisting) { - h2SqlBuilder.append("\"").append(column.name()).append("\" = source.\"").append(column.name()).append("\", "); - } - h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); - } - - StringBuilder pgSqlBuilder = new StringBuilder("INSERT INTO \""); - pgSqlBuilder.append(schemaName).append("\".\"").append(tableName).append("\" ("); - for (SimpleColumnMetadata column : columnsInTable) { - pgSqlBuilder.append("\"").append(column.name()).append("\", "); - } - pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); - pgSqlBuilder.append(") VALUES ("); - pgSqlBuilder.append("?, ".repeat(columnsInTable.size())); - pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); - pgSqlBuilder.append(")"); - - if (!conflicts.isEmpty()) { - pgSqlBuilder.append(" ON CONFLICT ("); - for (ColumnMetadata idColumn : table.getIdColumns()) { - pgSqlBuilder.append("\"").append(idColumn.name()).append("\", "); - } - pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); - pgSqlBuilder.append(") DO "); - if (!overwriteExisting.isEmpty()) { - pgSqlBuilder.append("UPDATE SET "); - for (SimpleColumnMetadata column : overwriteExisting) { - pgSqlBuilder.append("\"").append(column.name()).append("\" = EXCLUDED.\"").append(column.name()).append("\", "); - } - pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); + if (table.getIdColumns().stream().anyMatch(c -> c.name().equals(column.name()))) { + idColumns.add(new ColumnValuePair(column.name(), value)); } else { - pgSqlBuilder.append("NOTHING"); + otherColumnValues.put(column, value); } } + InsertStatement statement = new InsertStatement(this, table, new ColumnValuePairs(idColumns.toArray(new ColumnValuePair[0]))); - String h2Sql = h2SqlBuilder.toString(); - String pgSql = pgSqlBuilder.toString(); - List values = new ArrayList<>(); - for (SimpleColumnMetadata column : columnsInTable) { - Object deserializedValue = insertContext.getEntries().get(column); - Object serializedValue = serialize(deserializedValue); - values.add(serializedValue); - } - sqlStatements.add(new SQlStatement(h2Sql, pgSql, values)); + otherColumnValues.forEach((column, value) -> { + InsertStrategy strategy = insertContext.getInsertStrategy(column); + statement.set(column.name(), strategy, value); + }); + + insertStatements.add(statement); } - return sqlStatements; + return insertStatements; } public T get(String schema, String table, String column, ColumnValuePairs idColumns, List idColumnLinks, Class dataType) { 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 edb0762c..04a2b943 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 @@ -373,7 +373,7 @@ public void insert(List sqlStatements, InsertMode insertMode) thro for (Object value : sqlStatement.getValues()) { preparedStatement.setObject(i++, value); } - logger.debug("[H2] {}", sqlStatement.getH2Sql()); + logger.trace("[H2] {}", sqlStatement.getH2Sql()); preparedStatement.executeUpdate(); } } @@ -425,7 +425,7 @@ public ResultSet executeQuery(@Language("SQL") String sql, List values) for (int i = 0; i < values.size(); i++) { cachePreparedStatement.setObject(i + 1, values.get(i)); } - logger.debug("[H2] {}", sql); + logger.trace("[H2] {}", sql); return cachePreparedStatement.executeQuery(); } @@ -444,7 +444,7 @@ public void executeTransaction(SQLTransaction transaction, int delay) throws SQL for (Object value : values) { cachePreparedStatement.setObject(++i, value); } - logger.debug("[H2] {}", h2Sql); + logger.trace("[H2] {}", h2Sql); Consumer resultHandler = operation.getResultHandler(); if (resultHandler == null) { cachePreparedStatement.executeUpdate(); @@ -557,7 +557,7 @@ private synchronized void updateKnownTables() throws SQLException { currentTables.add(schema + "." + table); if (!knownTables.contains(schema + "." + table)) { - logger.trace("Discovered new referringTable {}.{}", schema, table); + 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'"; diff --git a/core/src/main/java/net/staticstudios/data/insert/InsertContext.java b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java index 73159cf3..a67c66dc 100644 --- a/core/src/main/java/net/staticstudios/data/insert/InsertContext.java +++ b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -62,12 +62,23 @@ public InsertContext set(String schema, String table, String column, @Nullable O return this; } + @SuppressWarnings("unused") // Used in generated code + public Object getValue(String schema, String table, String column) { + return entries.entrySet().stream() + .filter(entry -> entry.getKey().schema().equals(schema) + && entry.getKey().table().equals(table) + && entry.getKey().name().equals(column)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + } + public Map getEntries() { return entries; } - public Map getInsertStrategies() { - return insertStrategies; + public InsertStrategy getInsertStrategy(SimpleColumnMetadata columnMetadata) { + return insertStrategies.get(columnMetadata); } public void markInserted() { diff --git a/core/src/main/java/net/staticstudios/data/util/InsertStatement.java b/core/src/main/java/net/staticstudios/data/util/InsertStatement.java new file mode 100644 index 00000000..04c5b8ac --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/InsertStatement.java @@ -0,0 +1,308 @@ +package net.staticstudios.data.util; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.DataManager; +import net.staticstudios.data.InsertStrategy; +import net.staticstudios.data.parse.ForeignKey; +import net.staticstudios.data.parse.SQLColumn; +import net.staticstudios.data.parse.SQLSchema; +import net.staticstudios.data.parse.SQLTable; +import net.staticstudios.data.utils.Link; + +import java.util.*; + +public class InsertStatement { + private final DataManager dataManager; + private final SQLTable table; + private final ColumnValuePairs idColumns; + private final Map columnValues = new HashMap<>(); + private final List dependantOn = new ArrayList<>(); + private List dependencyRequirements; + + public InsertStatement(DataManager dataManager, SQLTable table, ColumnValuePairs idColumns) { + this.dataManager = dataManager; + this.table = table; + this.idColumns = idColumns; + for (ColumnValuePair pair : idColumns.getPairs()) { + columnValues.put(pair.column(), new Value(InsertStrategy.PREFER_EXISTING, pair.value())); + } + } + + public static boolean checkForCycles(List statements) { + Set visited = new HashSet<>(); + Set recursionStack = new HashSet<>(); + + for (InsertStatement statement : statements) { + if (detectCycleDFS(statement, visited, recursionStack)) { + return true; + } + } + return false; + } + + private static boolean detectCycleDFS(InsertStatement current, Set visited, Set stack) { + if (stack.contains(current)) { + throw new IllegalStateException("Dependency cycle detected involving table: " + current.getTable().getName()); + } + if (visited.contains(current)) { + return false; + } + + visited.add(current); + stack.add(current); + + for (InsertStatement dependency : current.dependantOn) { + if (detectCycleDFS(dependency, visited, stack)) { + return true; + } + } + + stack.remove(current); + return false; + } + + public static List sort(List statements) { + List sortedList = new ArrayList<>(); + Set visited = new HashSet<>(); + + for (InsertStatement statement : statements) { + performTopoSortDFS(statement, visited, sortedList); + } + + return sortedList; + } + + private static void performTopoSortDFS(InsertStatement current, Set visited, List sortedList) { + if (visited.contains(current)) { + return; + } + visited.add(current); + + for (InsertStatement dependency : current.dependantOn) { + performTopoSortDFS(dependency, visited, sortedList); + } + + sortedList.add(current); + } + + public SQLTable getTable() { + return table; + } + + public ColumnValuePairs getIdColumns() { + return idColumns; + } + + public void set(String column, InsertStrategy insertStrategy, Object value) { + columnValues.put(column, new Value(insertStrategy, value)); + } + + public List createUnmetDependencyStatements(List existingStatements) { + Preconditions.checkState(dependencyRequirements != null, "Must call calculateRequiredDependencies() before checking for unmet dependencies."); + List unmetStatements = new ArrayList<>(); + for (DependencyRequirement requirement : dependencyRequirements) { + boolean satisfied = false; + for (InsertStatement existingStatement : existingStatements) { + if (requirement.isSatisfiedBy(existingStatement)) { + satisfied = true; + dependantOn.add(existingStatement); + break; + } + } + if (!satisfied) { + InsertStatement satisfyingStatement = requirement.createSatisfyingStatement(dataManager); + unmetStatements.add(satisfyingStatement); + dependantOn.add(satisfyingStatement); + } + } + return unmetStatements; + } + + public void calculateRequiredDependencies() { + Preconditions.checkState(dependantOn.isEmpty(), "Dependencies have already been satisfied."); + dependencyRequirements = new ArrayList<>(); + + + // handle foreign keys. for each foreign key, if we have a value for the local column, we need to set the value for the foreign column. this consists of linking ids mostly. + for (ForeignKey fKey : table.getForeignKeys()) { + SQLSchema referencedSchema = Objects.requireNonNull(dataManager.getSQLBuilder().getSchema(fKey.getReferencedSchema())); + SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(fKey.getReferencedTable())); + + boolean addRequirement = true; + + for (Link link : fKey.getLinkingColumns()) { + String myColumnName = link.columnInReferringTable(); + + if (!columnValues.containsKey(myColumnName)) { + addRequirement = false; + break; + } + + String otherColumnName = link.columnInReferencedTable(); + SQLColumn otherColumn = Objects.requireNonNull(referencedTable.getColumn(otherColumnName)); + + // if its nullable and we don't have a value, skip it. + if (otherColumn.isNullable()) { + addRequirement = false; + break; + } + + } + + // to satisfy this, we need a statement that contains the foreign column with this value. + if (addRequirement) { + List requiredColumnValues = new ArrayList<>(); + for (Link link : fKey.getLinkingColumns()) { + String myColumnName = link.columnInReferringTable(); + String otherColumnName = link.columnInReferencedTable(); + Value myValue = columnValues.get(myColumnName); + requiredColumnValues.add(new ColumnValuePair(otherColumnName, myValue.value())); + } + dependencyRequirements.add(new DependencyRequirement( + fKey.getReferencedSchema(), + fKey.getReferencedTable(), + requiredColumnValues + )); + } + } + } + + public SQlStatement asStatement() { + String schemaName = table.getSchema().getName(); + String tableName = table.getName(); + + StringBuilder h2SqlBuilder = new StringBuilder("MERGE INTO \""); + h2SqlBuilder.append(schemaName).append("\".\"").append(tableName).append("\" AS target USING (VALUES ("); + h2SqlBuilder.append("?, ".repeat(columnValues.size())); + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); + h2SqlBuilder.append(")) AS source ("); + columnValues.forEach((column, value) -> { + h2SqlBuilder.append("\"").append(column).append("\", "); + }); + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); + h2SqlBuilder.append(") ON "); + for (ColumnMetadata idColumn : table.getIdColumns()) { + h2SqlBuilder.append("target.\"").append(idColumn.name()).append("\" = source.\"").append(idColumn.name()).append("\" AND "); + } + h2SqlBuilder.setLength(h2SqlBuilder.length() - 5); + h2SqlBuilder.append(" WHEN NOT MATCHED THEN INSERT ("); + + columnValues.forEach((column, value) -> { + h2SqlBuilder.append("\"").append(column).append("\", "); + }); + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); + h2SqlBuilder.append(") VALUES ("); + columnValues.forEach((column, value) -> { + h2SqlBuilder.append("source.\"").append(column).append("\", "); + }); + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); + h2SqlBuilder.append(")"); + + List overwriteExisting = new ArrayList<>(); + columnValues.forEach((column, value) -> { + if (value.insertStrategy() == InsertStrategy.OVERWRITE_EXISTING) { + overwriteExisting.add(column); + } + }); + if (!overwriteExisting.isEmpty()) { + h2SqlBuilder.append(" WHEN MATCHED THEN UPDATE SET "); + for (String column : overwriteExisting) { + h2SqlBuilder.append("\"").append(column).append("\" = source.\"").append(column).append("\", "); + } + h2SqlBuilder.setLength(h2SqlBuilder.length() - 2); + } + + StringBuilder pgSqlBuilder = new StringBuilder("INSERT INTO \""); + pgSqlBuilder.append(schemaName).append("\".\"").append(tableName).append("\" ("); + columnValues.forEach((column, value) -> { + pgSqlBuilder.append("\"").append(column).append("\", "); + }); + pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); + pgSqlBuilder.append(") VALUES ("); + pgSqlBuilder.append("?, ".repeat(columnValues.size())); + pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); + pgSqlBuilder.append(")"); + + pgSqlBuilder.append(" ON CONFLICT ("); + for (ColumnMetadata idColumn : table.getIdColumns()) { + pgSqlBuilder.append("\"").append(idColumn.name()).append("\", "); + } + pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); + pgSqlBuilder.append(") DO "); + if (!overwriteExisting.isEmpty()) { + pgSqlBuilder.append("UPDATE SET "); + for (String column : overwriteExisting) { + pgSqlBuilder.append("\"").append(column).append("\" = EXCLUDED.\"").append(column).append("\", "); + } + pgSqlBuilder.setLength(pgSqlBuilder.length() - 2); + } else { + pgSqlBuilder.append("NOTHING"); + } + + String h2Sql = h2SqlBuilder.toString(); + String pgSql = pgSqlBuilder.toString(); + List values = new ArrayList<>(); + columnValues.forEach((column, value) -> { + Object serializedValue = dataManager.serialize(value.value()); + values.add(serializedValue); + }); + return new SQlStatement(h2Sql, pgSql, values); + } + + public record Value(InsertStrategy insertStrategy, Object value) { + + } + + private static class DependencyRequirement { + private final String schema; + private final String table; + private final List requiredColumnValues; + + public DependencyRequirement(String schema, String table, List requiredColumnValues) { + this.schema = schema; + this.table = table; + this.requiredColumnValues = requiredColumnValues; + } + + public boolean isSatisfiedBy(InsertStatement statement) { + if (!statement.getTable().getSchema().getName().equals(schema)) { + return false; + } + if (!statement.getTable().getName().equals(table)) { + return false; + } + + List lookingFor = new ArrayList<>(requiredColumnValues); + + statement.columnValues.forEach((columnName, value) -> { + lookingFor.removeIf(pair -> pair.column().equals(columnName) && Objects.equals(pair.value(), value.value())); + }); + return lookingFor.isEmpty(); + } + + public InsertStatement createSatisfyingStatement(DataManager dataManager) { + SQLSchema sqlSchema = Objects.requireNonNull(dataManager.getSQLBuilder().getSchema(schema)); + SQLTable sqlTable = Objects.requireNonNull(sqlSchema.getTable(table)); + ColumnValuePair[] idColumns = new ColumnValuePair[sqlTable.getIdColumns().size()]; + + for (ColumnMetadata idColumn : sqlTable.getIdColumns()) { + Optional matchingPair = requiredColumnValues.stream() + .filter(pair -> pair.column().equals(idColumn.name())) + .findFirst(); + Preconditions.checkState(matchingPair.isPresent(), "Cannot create satisfying statement for dependency requirement: missing id column value for " + idColumn.name()); + idColumns[sqlTable.getIdColumns().indexOf(idColumn)] = matchingPair.get(); + } + + InsertStatement statement = new InsertStatement(dataManager, sqlTable, new ColumnValuePairs(idColumns)); + for (ColumnValuePair pair : requiredColumnValues) { + if (sqlTable.getIdColumns().stream().noneMatch(col -> col.name().equals(pair.column()))) { + statement.set(pair.column(), InsertStrategy.PREFER_EXISTING, pair.value()); + } + } + + return statement; + } + + } + +} diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index 2882b9b1..3355308c 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -61,7 +61,6 @@ public class MockUser extends UniqueData { .onAdd(MockUser.class, (user, added) -> user.sessionAdditions.set(user.sessionAdditions.get() + 1)) .onRemove(MockUser.class, (user, removed) -> user.sessionRemovals.set(user.sessionRemovals.get() + 1)); - @Identifier("friend_additions") public CachedValue friendAdditions = CachedValue.of(this, Integer.class) .withFallback(0); diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java index fabbe898..96e703ee 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java @@ -6,6 +6,7 @@ import com.sun.tools.javac.util.List; import net.staticstudios.data.InsertStrategy; import net.staticstudios.data.compiler.javac.ProcessorContext; +import net.staticstudios.data.utils.Link; import java.util.ArrayList; import java.util.Collection; @@ -84,6 +85,11 @@ private void processValue(ParsedPersistentValue pv) { )), null ), builderClassDecl); + + + if (pv instanceof ParsedForeignPersistentValue fpv) { + processFpv(fpv); + } } private void processReference(ParsedReference ref) { @@ -245,6 +251,84 @@ private void processReference(ParsedReference ref) { ), builderClassDecl); } + private void processFpv(ParsedForeignPersistentValue fpv) { + + + String linkingColumnReferredBySchemaFieldName = fpv.getFieldName() + "$_referredBySchema"; + String linkingColumnReferredByTableFieldName = fpv.getFieldName() + "$_referredByTable"; + + createField(VarDef( + Modifiers(Flags.PRIVATE), + names.fromString(linkingColumnReferredBySchemaFieldName), + Ident(names.fromString("String")), + Apply( + List.nil(), + Select( + chainDots("net", "staticstudios", "data", "util", "ValueUtils"), + names.fromString("parseValue") + ), + List.of( + Literal(dataAnnotation.schema()) + ) + ) + ), builderClassDecl); + + createField(VarDef( + Modifiers(Flags.PRIVATE), + names.fromString(linkingColumnReferredByTableFieldName), + Ident(names.fromString("String")), + Apply( + List.nil(), + Select( + chainDots("net", "staticstudios", "data", "util", "ValueUtils"), + names.fromString("parseValue") + ), + List.of( + Literal(dataAnnotation.table()) + ) + ) + ), builderClassDecl); + + for (Link link : fpv.getLinks()) { + String linkingColumnReferencesFieldName = fpv.getFieldName() + "$" + link.columnInReferringTable() + "_references"; + String linkingColumnReferredByColumnFieldName = fpv.getFieldName() + "$" + link.columnInReferringTable() + "_referredByColumn"; + + + createField(VarDef( + Modifiers(Flags.PRIVATE), + names.fromString(linkingColumnReferencesFieldName), + Ident(names.fromString("String")), + Apply( + List.nil(), + Select( + chainDots("net", "staticstudios", "data", "util", "ValueUtils"), + names.fromString("parseValue") + ), + List.of( + Literal(link.columnInReferencedTable()) + ) + ) + ), builderClassDecl); + + + createField(VarDef( + Modifiers(Flags.PRIVATE), + names.fromString(linkingColumnReferredByColumnFieldName), + Ident(names.fromString("String")), + Apply( + List.nil(), + Select( + chainDots("net", "staticstudios", "data", "util", "ValueUtils"), + names.fromString("parseValue") + ), + List.of( + Literal(link.columnInReferringTable()) + ) + ) + ), builderClassDecl); + } + } + private void makeInsertBatchMethod() { createMethod(MethodDef( Modifiers(Flags.PUBLIC | Flags.FINAL), @@ -404,19 +488,77 @@ private void makeInsertContextMethod(Collection parsedPer tableFieldAccess, columnFieldAccess, fieldAccess, - insertStrategy != null ? - Select( - chainDots("net", "staticstudios", "data", "InsertStrategy"), - names.fromString(insertStrategy.name()) - ) - : - Literal(TypeTag.BOT, null) + Select( + chainDots("net", "staticstudios", "data", "InsertStrategy"), + names.fromString(insertStrategy != null ? insertStrategy.name() : InsertStrategy.PREFER_EXISTING.name()) + ) ) ); bodyStatements.add(Exec(insertStatement)); } + for (ParsedPersistentValue pv : parsedPersistentValues) { + if (!(pv instanceof ParsedForeignPersistentValue fpv)) { + continue; + } + + + bodyStatements.add( + If( + Binary( + JCTree.Tag.NE, + Ident(names.fromString(pv.getFieldName())), + Literal(TypeTag.BOT, null) + ), + Block(0, + List.from( + fpv.getLinks().stream().map(link -> { + String linkingColumnReferencesFieldName = fpv.getFieldName() + "$" + link.columnInReferringTable() + "_references"; + String linkingColumnReferredBySchemaFieldName = fpv.getFieldName() + "$_referredBySchema"; + String linkingColumnReferredByTableFieldName = fpv.getFieldName() + "$_referredByTable"; + String linkingColumnReferredByColumnFieldName = fpv.getFieldName() + "$" + link.columnInReferringTable() + "_referredByColumn"; + + + return Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("ctx")), + names.fromString("set") + ), + List.of( + Ident(names.fromString(pv.getFieldName() + "$schema")), + Ident(names.fromString(pv.getFieldName() + "$table")), + Ident(names.fromString(linkingColumnReferencesFieldName)), + Apply( + List.nil(), + Select( + Ident(names.fromString("ctx")), + names.fromString("getValue") + ), + List.of( + Ident(names.fromString(linkingColumnReferredBySchemaFieldName)), + Ident(names.fromString(linkingColumnReferredByTableFieldName)), + Ident(names.fromString(linkingColumnReferredByColumnFieldName)) + + ) + ), + Select( + chainDots("net", "staticstudios", "data", "InsertStrategy"), + names.fromString("PREFER_EXISTING") + ) + ) + ) + ); + }).toList() + ) + ), + null + ) + ); + } + for (ParsedReference ref : parsedReferences) { String idColumnValuePairsFieldName = ref.getFieldName() + "_reference$idColumnValuePairs"; String schemaFieldName = ref.getFieldName() + "_reference$schema"; diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java index 04a234fe..856246c8 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java @@ -6,7 +6,7 @@ import javax.lang.model.element.TypeElement; import java.util.List; -class ParsedForeignPersistentValue extends ParsedPersistentValue { +class ParsedForeignPersistentValue extends ParsedPersistentValue implements ParsedRelation { private final InsertStrategy insertStrategy; private final List links; diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java index 216cef41..4f01405b 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java @@ -14,21 +14,18 @@ import java.util.ArrayList; import java.util.Collection; -public class ParsedPersistentValue { - private final String fieldName; +public class ParsedPersistentValue extends ParsedValue { private final String schema; private final String table; private final String column; private final boolean nullable; - private final TypeElement type; public ParsedPersistentValue(String fieldName, String schema, String table, String column, boolean nullable, TypeElement type) { - this.fieldName = fieldName; + super(fieldName, type); this.schema = schema; this.table = table; this.column = column; this.nullable = nullable; - this.type = type; } public static Collection extractPersistentValues(@NotNull TypeElement dataClass, @@ -111,10 +108,6 @@ public static Collection extractPersistentValues(@NotNull return persistentValues; } - public String getFieldName() { - return fieldName; - } - public String getSchema() { return schema; } @@ -131,9 +124,6 @@ public boolean isNullable() { return nullable; } - public TypeElement getType() { - return type; - } public String[] getTypeFQNParts() { return type.getQualifiedName().toString().split("\\."); diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java index bbc939fc..0cdca70b 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java @@ -16,15 +16,12 @@ import java.util.Collection; import java.util.List; -public class ParsedReference { - private final String fieldName; +public class ParsedReference extends ParsedValue implements ParsedRelation { private final List links; - private final TypeElement type; public ParsedReference(String fieldName, List links, TypeElement type) { - this.fieldName = fieldName; + super(fieldName, type); this.links = links; - this.type = type; } public static Collection extractReferences(@NotNull TypeElement dataClass, @@ -54,18 +51,10 @@ public static Collection extractReferences(@NotNull TypeElement return references; } - public String getFieldName() { - return fieldName; - } - public List getLinks() { return links; } - public TypeElement getType() { - return type; - } - public String[] getTypeFQNParts() { return type.getQualifiedName().toString().split("\\."); } diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedRelation.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedRelation.java new file mode 100644 index 00000000..71a0aad4 --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedRelation.java @@ -0,0 +1,9 @@ +package net.staticstudios.data.compiler.javac.javac; + +import net.staticstudios.data.utils.Link; + +import java.util.List; + +public interface ParsedRelation { + List getLinks(); +} diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedValue.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedValue.java new file mode 100644 index 00000000..f9a463d7 --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedValue.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.compiler.javac.javac; + +import javax.lang.model.element.TypeElement; + +public class ParsedValue { + protected final String fieldName; + protected final TypeElement type; + + public ParsedValue(String fieldName, TypeElement type) { + this.fieldName = fieldName; + this.type = type; + } + + public String getFieldName() { + return fieldName; + } + + public TypeElement getType() { + return type; + } +} From 66b25b6f3d2ae412ac570ec0743ce6abee4d9761 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 28 Dec 2025 15:14:38 -0500 Subject: [PATCH 61/75] add option to skip fkey generation for References --- .../java/net/staticstudios/data/OneToOne.java | 7 +++++ .../data/impl/data/ReferenceImpl.java | 2 +- .../staticstudios/data/parse/SQLBuilder.java | 4 +++ .../data/util/ReferenceMetadata.java | 2 +- .../net/staticstudios/data/ReferenceTest.java | 29 +++++++++++++++++++ .../data/mock/user/MockUser.java | 6 ++++ 6 files changed, 48 insertions(+), 2 deletions(-) diff --git a/annotations/src/main/java/net/staticstudios/data/OneToOne.java b/annotations/src/main/java/net/staticstudios/data/OneToOne.java index 4a2cfe45..0f7e58c5 100644 --- a/annotations/src/main/java/net/staticstudios/data/OneToOne.java +++ b/annotations/src/main/java/net/staticstudios/data/OneToOne.java @@ -19,4 +19,11 @@ //todo: option to force not null? + /** + * Should a foreign key constraint be created for this relation? + * + * @return Whether to create a foreign key constraint + */ + boolean fkey() default true; + } 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 4d8f0324..7c561807 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 @@ -68,7 +68,7 @@ public static Map extractMetada Preconditions.checkNotNull(oneToOneAnnotation, "Field %s in class %s is missing @OneToOne annotation".formatted(field.getName(), clazz.getName())); Class referencedClass = ReflectionUtils.getGenericType(field); Preconditions.checkNotNull(referencedClass, "Field %s in class %s is not parameterized".formatted(field.getName(), clazz.getName())); - metadataMap.put(field, new ReferenceMetadata(clazz, (Class) referencedClass, SQLBuilder.parseLinks(oneToOneAnnotation.link()))); + metadataMap.put(field, new ReferenceMetadata(clazz, (Class) referencedClass, SQLBuilder.parseLinks(oneToOneAnnotation.link()), oneToOneAnnotation.fkey())); } return metadataMap; 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 246a5be2..90b59f2e 100644 --- a/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java +++ b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -482,6 +482,10 @@ private void parseReference(Class clazz, Map holderClass, Class referencedClass, - List links) { + List links, boolean generateFkey) { } diff --git a/core/src/test/java/net/staticstudios/data/ReferenceTest.java b/core/src/test/java/net/staticstudios/data/ReferenceTest.java index 0f461c92..9ea66531 100644 --- a/core/src/test/java/net/staticstudios/data/ReferenceTest.java +++ b/core/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -213,4 +213,33 @@ public void testUpdateHandlerUpdate() { assertEquals(4, user.settingsUpdates.get()); } + @Test + public void testReferenceNoFkey() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + + UUID bestBuddyId = UUID.randomUUID(); + + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .bestBuddyId(bestBuddyId) + .insert(InsertMode.SYNC); + + assertNotNull(user); + assertNull(user.bestBuddy.get()); + assertEquals(bestBuddyId, user.bestBuddyId.get()); + + MockUser bestBuddy = MockUser.builder(dataManager) + .id(bestBuddyId) + .name("best buddy") + .insert(InsertMode.SYNC); + + assertSame(bestBuddy, user.bestBuddy.get()); + bestBuddy.delete(); + + assertNull(user.bestBuddy.get()); + assertEquals(bestBuddyId, user.bestBuddyId.get()); + } + } \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index 3355308c..559c9380 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -34,6 +34,12 @@ public class MockUser extends UniqueData { public Reference settings = Reference.of(this, MockUserSettings.class) .onUpdate(MockUser.class, (user, update) -> user.settingsUpdates.set(user.settingsUpdates.get() + 1)); + @Column(name = "best_buddy_id", nullable = true, unique = true) + public PersistentValue bestBuddyId; + + @OneToOne(link = "best_buddy_id=id", fkey = false) + public Reference bestBuddy; + @Insert(InsertStrategy.OVERWRITE_EXISTING) @Delete(DeleteStrategy.NO_ACTION) @DefaultValue("0") From fa29cd285bcbdd8bd359d9a6b16b7815cf57be40 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Sun, 28 Dec 2025 16:37:20 -0500 Subject: [PATCH 62/75] remove sql ambiguity in one to many collections --- .../data/PersistentOneToManyCollectionImpl.java | 13 +++++++------ .../PersistentOneToManyValueCollectionImpl.java | 10 +++++----- 2 files changed, 12 insertions(+), 11 deletions(-) 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 2474f507..436223c5 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 @@ -411,28 +411,29 @@ public Set getIds() { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT "); for (ColumnMetadata columnMetadata : typeMetadata.idColumns()) { - sqlBuilder.append("\"").append(columnMetadata.name()).append("\", "); + sqlBuilder.append("\"").append(columnMetadata.schema()).append("\".\"").append(columnMetadata.table()).append("\".\"").append(columnMetadata.name()).append("\", "); } for (ColumnMetadata columnMetadata : holderMetadata.idColumns()) { - sqlBuilder.append("_source.\"").append(columnMetadata.name()).append("\", "); + sqlBuilder.append("\"").append(columnMetadata.schema()).append("\".\"").append(columnMetadata.table()).append("\".\"").append(columnMetadata.name()).append("\", "); } sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(" FROM \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" "); - sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" AS _source ON "); + sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" ON "); for (Link entry : link) { String myColumn = entry.columnInReferringTable(); String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\".\"").append(theirColumn).append("\" = _source.\"").append(myColumn).append("\" AND "); + sqlBuilder.append("\"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\".\"").append(theirColumn).append("\" = \"") + .append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(myColumn).append("\" AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); sqlBuilder.append(" WHERE "); for (Link entry : link) { String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(theirColumn).append("\" = _source.\"").append(entry.columnInReferringTable()).append("\" AND "); + 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("_source.\"").append(columnValuePair.column()).append("\" = ? AND "); + sqlBuilder.append("\"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(columnValuePair.column()).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); 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 5df38f2b..ebae240e 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 @@ -426,25 +426,25 @@ private Set getValues() { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("SELECT ").append("\"").append(dataColumn).append("\", "); for (ColumnMetadata columnMetadata : holderMetadata.idColumns()) { - sqlBuilder.append("_source.\"").append(columnMetadata.name()).append("\", "); + sqlBuilder.append("\"").append(columnMetadata.schema()).append("\".\"").append(columnMetadata.table()).append("\".\"").append(columnMetadata.name()).append("\", "); } sqlBuilder.setLength(sqlBuilder.length() - 2); sqlBuilder.append(" FROM \"").append(dataSchema).append("\".\"").append(dataTable).append("\" "); - sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" AS _source ON "); + sqlBuilder.append("INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" ON "); for (Link entry : link) { String myColumn = entry.columnInReferringTable(); String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(dataSchema).append("\".\"").append(dataTable).append("\".\"").append(theirColumn).append("\" = _source.\"").append(myColumn).append("\" AND "); + sqlBuilder.append("\"").append(dataSchema).append("\".\"").append(dataTable).append("\".\"").append(theirColumn).append("\" = \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(myColumn).append("\" AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); sqlBuilder.append(" WHERE "); for (Link entry : link) { String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(theirColumn).append("\" = _source.\"").append(entry.columnInReferringTable()).append("\" AND "); + sqlBuilder.append("\"").append(theirColumn).append("\" = \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(entry.columnInReferringTable()).append("\" AND "); } for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - sqlBuilder.append("_source.\"").append(columnValuePair.column()).append("\" = ? AND "); + sqlBuilder.append("\"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(columnValuePair.column()).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); From 2c8c5cd40cc7d4eed8843b147fb792eb97818360 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 29 Dec 2025 01:03:36 -0500 Subject: [PATCH 63/75] change the setup process. all data classes must be loaded, then #finishLoading() should be called. this is what syncs the data from the source rather than doing multiple syncs --- .../net/staticstudios/data/DataManager.java | 18 +++++-- .../net/staticstudios/data/StaticData.java | 9 ++++ .../staticstudios/data/impl/DataAccessor.java | 2 + .../data/impl/h2/H2DataAccessor.java | 53 ++++++++++--------- .../data/insert/PostInsertAction.java | 2 + .../staticstudios/data/BatchInsertTest.java | 3 ++ .../staticstudios/data/CachedValueTest.java | 5 ++ .../staticstudios/data/CustomTypeTest.java | 2 + .../PersistentManyToManyCollectionTest.java | 1 + .../PersistentOneToManyCollectionTest.java | 1 + ...ersistentOneToManyValueCollectionTest.java | 1 + .../data/PersistentValueTest.java | 15 ++++++ .../net/staticstudios/data/QueryTest.java | 3 ++ .../net/staticstudios/data/ReferenceTest.java | 7 +++ .../net/staticstudios/data/SnapshotTest.java | 1 + 15 files changed, 95 insertions(+), 28 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index d9ec303e..e7eba754 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -58,7 +58,9 @@ public class DataManager { private final Set registeredUpdateHandlersForReference = ConcurrentHashMap.newKeySet(); private final List> valueSerializers = new CopyOnWriteArrayList<>(); - private final Consumer updateHandlerExecurtor; + private final Consumer updateHandlerExecutor; + + private boolean finishedLoading = false; //todo: custom types are serialized and deserialized all the time currently, we should have a cache for these. caffeine with time based eviction sounds good. public DataManager(StaticDataConfig config, boolean setGlobal) { @@ -71,7 +73,7 @@ public DataManager(StaticDataConfig config, boolean setGlobal) { config.redisHost(), config.redisPort() ); - this.updateHandlerExecurtor = config.updateHandlerExecutor(); + this.updateHandlerExecutor = config.updateHandlerExecutor(); if (setGlobal) { if (Boolean.FALSE.equals(DataManager.useGlobal)) { @@ -651,7 +653,7 @@ public void callReferenceUpdateHandlers(List columnNames, String schema, } private void submitUpdateHandler(Runnable runnable) { - updateHandlerExecurtor.accept(runnable); + updateHandlerExecutor.accept(runnable); } @ApiStatus.Internal @@ -718,8 +720,18 @@ public void registerReferenceUpdateHandlers(ReferenceMetadata metadata, Collecti return Collections.emptyList(); } + public final void finishLoading() { + Preconditions.checkState(!finishedLoading, "finishLoading() has already been called"); + + finishedLoading = true; + dataAccessor.resync(); + //todo: can't call load() after this, and only after this is called do we sync data. + } + @SafeVarargs public final void load(Class... classes) { + Preconditions.checkState(!finishedLoading, "Cannot call load(...) after finishLoading() has been called"); + List extracted = new ArrayList<>(); for (Class clazz : classes) { extracted.add(extractMetadata(clazz)); diff --git a/core/src/main/java/net/staticstudios/data/StaticData.java b/core/src/main/java/net/staticstudios/data/StaticData.java index 149cf950..d5ce8f02 100644 --- a/core/src/main/java/net/staticstudios/data/StaticData.java +++ b/core/src/main/java/net/staticstudios/data/StaticData.java @@ -35,6 +35,15 @@ public static void load(Class... classes) { DataManager.getInstance().load(classes); } + /** + * Completes the loading process by ensuring all data is fully loaded and ready for use. + * This method should be called after invoking the {@link #load(Class[])} method. + */ + public static void finishLoading() { + assertInit(); + DataManager.getInstance().finishLoading(); + } + /** * Create an BatchInsert for batching multiple insert operations together. * 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 b292047b..93a4c6af 100644 --- a/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java +++ b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java @@ -32,4 +32,6 @@ default void executeUpdate(SQLTransaction.Statement statement, List valu void setRedisValue(String key, String value, int expirationSeconds); void discoverRedisKeys(List partialRedisKeys); + + void resync(); } 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 04a2b943..40f15996 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 @@ -50,7 +50,7 @@ public class H2DataAccessor implements DataAccessor { private final String jdbcUrl; private final ThreadLocal threadConnection = new ThreadLocal<>(); private final ThreadLocal> threadPreparedStatementCache = new ThreadLocal<>(); - private final Set knownTables = new HashSet<>(); + private final Set knownTables = new HashSet<>(); private final DataManager dataManager; private final PostgresListener postgresListener; private final Map>, Runnable> delayedTasks = new ConcurrentHashMap<>(); @@ -80,9 +80,13 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener postgresListener.addHandler(notification -> { try { SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(notification.getSchema()); - Preconditions.checkNotNull(sqlSchema, "Schema %s not found".formatted(notification.getSchema())); + if (sqlSchema == null) { + return; // we don't care about this schema + } SQLTable sqlTable = sqlSchema.getTable(notification.getTable()); - Preconditions.checkNotNull(sqlTable, "Table %s.%s not found".formatted(notification.getSchema(), notification.getTable())); + if (sqlTable == null) { + return; // we don't care about this table + } switch (notification.getOperation()) { case UPDATE -> { List values = new ArrayList<>(); @@ -104,7 +108,9 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener String column = changed.first(); String encoded = changed.second(); SQLColumn sqlColumn = sqlTable.getColumn(column); - Preconditions.checkNotNull(sqlColumn, "Column %s.%s.%s not found".formatted(notification.getSchema(), notification.getTable(), column)); + if (sqlColumn == null) { + return; // we don't care about this column + } Object decoded = encoded == null ? null : Primitives.decodePrimitive(sqlColumn.getType(), encoded); values.add(decoded); sb.append("\"").append(column).append("\" = ?, "); @@ -144,7 +150,9 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener String column = entry.getKey(); String encoded = entry.getValue(); SQLColumn sqlColumn = sqlTable.getColumn(column); - Preconditions.checkNotNull(sqlColumn, "Column %s.%s.%s not found".formatted(notification.getSchema(), notification.getTable(), column)); + if (sqlColumn == null) { + return; // we don't care about this column + } Object decoded = encoded == null ? null : Primitives.decodePrimitive(sqlColumn.getType(), encoded); values.add(decoded); sb.append("\"").append(column).append("\", "); @@ -225,13 +233,6 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener }); } - public synchronized void resync() { - //todo: i would ideally like to support periodic resyncing of data. even if this means we pause everything until then. not exactly sure how this would look tho. - - //todo: when we resync we should clear the task queue and steal the connection - //todo: if possible, id like to pause everything else until we are done syncing - } - public synchronized void sync(List schemaTables, List redisPartialKeys) throws SQLException { taskQueue.submitTask((realDbConnection, jedis) -> { if (!schemaTables.isEmpty()) { @@ -293,10 +294,6 @@ public synchronized void sync(List schemaTables, List redis } if (!redisPartialKeys.isEmpty()) { for (String partialKey : redisPartialKeys) { - if (knownRedisPartialKeys.contains(partialKey)) { - continue; - } - String cursor = ScanParams.SCAN_POINTER_START; ScanParams scanParams = new ScanParams().match(partialKey).count(1000); @@ -536,27 +533,35 @@ public void setRedisValue(String key, String value, int expirationSeconds) { @Override public void discoverRedisKeys(List partialRedisKeys) { + knownRedisPartialKeys.addAll(partialRedisKeys); + } + + @Override + public synchronized void resync() { + //todo: i would ideally like to support periodic resyncing of data. even if this means we pause everything until then. not exactly sure how this would look tho. + + //todo: when we resync we should clear the task queue and steal the connection + //todo: if possible, id like to pause everything else until we are done syncing try { - sync(Collections.emptyList(), partialRedisKeys); - knownRedisPartialKeys.addAll(partialRedisKeys); + sync(new ArrayList<>(knownTables), new ArrayList<>(knownRedisPartialKeys)); } catch (SQLException e) { - logger.error("Error discovering redis keys", e); + throw new RuntimeException(e); } } private synchronized void updateKnownTables() throws SQLException { - Set currentTables = new HashSet<>(); + Set currentTables = new HashSet<>(); Connection connection = getConnection(); try (Statement statement = connection.createStatement(); ResultSet rs = statement.executeQuery( "SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA NOT IN ('INFORMATION_SCHEMA', 'SYSTEM_LOBS') AND TABLE_TYPE='BASE TABLE'")) { - List toSync = new ArrayList<>(); while (rs.next()) { String schema = rs.getString("TABLE_SCHEMA"); String table = rs.getString("TABLE_NAME"); - currentTables.add(schema + "." + table); + SchemaTable schemaTable = new SchemaTable(schema, table); + currentTables.add(schemaTable); - if (!knownTables.contains(schema + "." + table)) { + 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'"; @@ -578,10 +583,8 @@ private synchronized void updateKnownTables() throws SQLException { } taskQueue.submitTask(realDbConnection -> postgresListener.ensureTableHasTrigger(realDbConnection, schema, table)).join(); - toSync.add(new SchemaTable(schema, table)); } } - sync(toSync, Collections.emptyList()); } knownTables.clear(); knownTables.addAll(currentTables); diff --git a/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java b/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java index fff12139..bd214db0 100644 --- a/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java +++ b/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java @@ -48,6 +48,8 @@ static InsertIntoJoinTableManyToManyPostInsertAction.Builder manyToMany(Persiste } static InsertIntoJoinTableManyToManyPostInsertAction.Builder manyToMany(Class holderClass, String collectionJoinTableSchema, String collectionJoinTableName) { + collectionJoinTableSchema = ValueUtils.parseValue(collectionJoinTableSchema); + collectionJoinTableName = ValueUtils.parseValue(collectionJoinTableName); DataManager dataManager = DataManager.getInstance(); UniqueDataMetadata metadata = dataManager.getMetadata(holderClass); PersistentManyToManyCollectionMetadata collectionMetadata = null; diff --git a/core/src/test/java/net/staticstudios/data/BatchInsertTest.java b/core/src/test/java/net/staticstudios/data/BatchInsertTest.java index ff19973b..f25897d2 100644 --- a/core/src/test/java/net/staticstudios/data/BatchInsertTest.java +++ b/core/src/test/java/net/staticstudios/data/BatchInsertTest.java @@ -23,6 +23,7 @@ public class BatchInsertTest extends DataTest { public void testCompletableFuture() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); BatchInsert batch = dataManager.createBatchInsert(); CompletableFuture cf = MockUser.builder(dataManager) @@ -42,6 +43,7 @@ public void testCompletableFuture() { public void testManyToManyPostInsertAction() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); List users = new ArrayList<>(); for (int i = 0; i < 3; i++) { @@ -93,6 +95,7 @@ public void testManyToManyPostInsertAction() { public void testManyToManyPostInsertAction2() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUser user = MockUser.builder(dataManager) .id(UUID.randomUUID()) diff --git a/core/src/test/java/net/staticstudios/data/CachedValueTest.java b/core/src/test/java/net/staticstudios/data/CachedValueTest.java index ed5b145b..20dc455b 100644 --- a/core/src/test/java/net/staticstudios/data/CachedValueTest.java +++ b/core/src/test/java/net/staticstudios/data/CachedValueTest.java @@ -19,6 +19,7 @@ public class CachedValueTest extends DataTest { public void testBasic() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUser user = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("john doe") @@ -38,6 +39,7 @@ public void testBasic() { public void testFallback() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUser user = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("john doe") @@ -61,6 +63,7 @@ public void testFallback() { public void testUpdateHandler() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUser user = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("john doe") @@ -103,6 +106,7 @@ public void testUpdateHandler() { public void testUpdateRedis() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUser user = MockUser.builder(dataManager) .id(UUID.randomUUID()) .name("john doe") @@ -150,6 +154,7 @@ public void testLoadCachedValues() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUser user = MockUser.builder(dataManager) .id(userId) .name("john doe") diff --git a/core/src/test/java/net/staticstudios/data/CustomTypeTest.java b/core/src/test/java/net/staticstudios/data/CustomTypeTest.java index aca00455..4fd9bfc2 100644 --- a/core/src/test/java/net/staticstudios/data/CustomTypeTest.java +++ b/core/src/test/java/net/staticstudios/data/CustomTypeTest.java @@ -45,6 +45,7 @@ public void testCustomTypesSetGet() { dataManager.registerValueSerializer(new AccountDetailsValueSerializer()); dataManager.registerValueSerializer(new AccountSettingsValueSerializer()); dataManager.load(MockAccount.class); + dataManager.finishLoading(); MockAccount account = MockAccount.builder(dataManager) .id(1) @@ -85,6 +86,7 @@ public void testCustomTypesLoad() throws SQLException { dataManager.registerValueSerializer(detailsSerializer); dataManager.registerValueSerializer(settingsSerializer); dataManager.load(MockAccount.class); + dataManager.finishLoading(); MockAccount account = MockAccount.query(dataManager).where(w -> w.idIs(1)).findOne(); assertNotNull(account); diff --git a/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java index fca1c866..63a2dfa6 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java @@ -27,6 +27,7 @@ public class PersistentManyToManyCollectionTest extends DataTest { public void setUp() { dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); mockUser = MockUser.builder(dataManager) .id(id) diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java index 5aa711f1..eb9630e1 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -30,6 +30,7 @@ public class PersistentOneToManyCollectionTest extends DataTest { public void setUp() { dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); mockUser = MockUser.builder(dataManager) .id(id) diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java index c1083628..771c4ec3 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java @@ -25,6 +25,7 @@ public class PersistentOneToManyValueCollectionTest extends DataTest { public void setUp() { dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); mockUser = MockUser.builder(dataManager) .id(id) diff --git a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java index f44833af..a97b3138 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -29,6 +29,7 @@ public void testReadData() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); for (UUID id : userIds) { MockUser.builder(dataManager) .id(id) @@ -56,6 +57,7 @@ public void testReadData() throws SQLException { public void testUniqueDataCache() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); MockUser mockUser = MockUser.builder(dataManager) .id(id) @@ -83,6 +85,7 @@ public void testUniqueDataCache() throws SQLException { public void testUpdate() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); MockUser mockUser = MockUser.builder(dataManager) .id(id) @@ -133,6 +136,7 @@ public void testUpdate() throws SQLException { public void testUpdateForeignColumn() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); MockUser mockUser = MockUser.builder(dataManager) .id(id) @@ -183,6 +187,7 @@ public void testUpdateForeignColumn() throws SQLException { public void testUpdateHandlerRegistration() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); assertEquals(0, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); MockUser mockUser = MockUser.builder(dataManager) @@ -220,6 +225,7 @@ public void testUpdateHandlerRegistration() { public void testReceiveUpdateFromPostgres() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); MockUser mockUser = MockUser.builder(dataManager) .id(id) @@ -249,6 +255,7 @@ public void testReceiveUpdateFromPostgres() { public void testReceiveInsertFromPostgres() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); try (PreparedStatement preferencesStatement = getConnection().prepareStatement("INSERT INTO user_preferences (user_id, fav_color) VALUES (?, ?)"); @@ -279,6 +286,7 @@ public void testReceiveInsertFromPostgres() { public void testReceiveDeleteFromPostgres() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); MockUser mockUser = MockUser.builder(dataManager) .id(id) @@ -311,6 +319,7 @@ public void testReceiveDeleteFromPostgres() { public void testChangeIdColumn() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); MockUser mockUser = MockUser.builder(dataManager) .id(id) @@ -342,6 +351,7 @@ public void testChangeIdColumnInPostgres() { //todo: this and the other id column test are failing because fkeys have been changed to be on the user referringTable. use a trigger to update the fkeys on id change, similar to the cascade delete trigger DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); MockUser mockUser = MockUser.builder(dataManager) .id(id) @@ -384,6 +394,7 @@ public void testChangeIdColumnInPostgres() { public void testUpdateInterval() throws Exception { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); MockUser mockUser = MockUser.builder(dataManager) .id(id) @@ -421,6 +432,7 @@ public void testUpdateInterval() throws Exception { public void testInsertStrategyPreferExisting() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); Connection connection = getConnection(); UUID id = UUID.randomUUID(); try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO public.user_preferences (user_id, fav_color) VALUES (?, ?)")) { @@ -456,6 +468,7 @@ public void testInsertStrategyPreferExisting() throws SQLException { public void testInsertStrategyOverwriteExisting() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); Connection connection = getConnection(); UUID id = UUID.randomUUID(); try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO public.user_metadata (user_id, name_updates) VALUES (?, ?)")) { @@ -490,6 +503,7 @@ public void testInsertStrategyOverwriteExisting() throws SQLException { public void testDeleteStrategyCascade() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); Connection h2Connection = getH2Connection(dataManager); Connection pgConnection = getConnection(); UUID id = UUID.randomUUID(); @@ -538,6 +552,7 @@ public void testDeleteStrategyCascade() throws SQLException { public void testDeleteStrategyNoAction() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); Connection h2Connection = getH2Connection(dataManager); Connection pgConnection = getConnection(); UUID id = UUID.randomUUID(); diff --git a/core/src/test/java/net/staticstudios/data/QueryTest.java b/core/src/test/java/net/staticstudios/data/QueryTest.java index 6dc26719..d9db18cf 100644 --- a/core/src/test/java/net/staticstudios/data/QueryTest.java +++ b/core/src/test/java/net/staticstudios/data/QueryTest.java @@ -14,6 +14,7 @@ public class QueryTest extends DataTest { public void testFindOneEquals() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); MockUser original = MockUser.builder(dataManager) .id(id) @@ -29,6 +30,7 @@ public void testFindOneEquals() { public void testFindAllLike() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUser original1 = MockUser.builder(dataManager) .id(UUID.randomUUID()) @@ -68,6 +70,7 @@ public void testFindAllLike() { public void testQueryOnForeignColumn() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUser likesRed = MockUser.builder(dataManager) .id(UUID.randomUUID()) diff --git a/core/src/test/java/net/staticstudios/data/ReferenceTest.java b/core/src/test/java/net/staticstudios/data/ReferenceTest.java index 9ea66531..45036a9c 100644 --- a/core/src/test/java/net/staticstudios/data/ReferenceTest.java +++ b/core/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -20,6 +20,7 @@ public class ReferenceTest extends DataTest { public void testCreateSettingsWithoutReference() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUserSettings settings = MockUserSettings.builder(dataManager) .id(UUID.randomUUID()) @@ -32,6 +33,7 @@ public void testCreateSettingsWithoutReference() { public void testCreateSettingsThenReference() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUserSettings settings = MockUserSettings.builder(dataManager) .id(UUID.randomUUID()) @@ -53,6 +55,7 @@ public void testCreateSettingsThenReference() { public void testCreateUserAndReferenceInSingleInsert() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID settingsId = UUID.randomUUID(); BatchInsert batch = dataManager.createBatchInsert(); @@ -79,6 +82,7 @@ public void testCreateUserAndReferenceInSingleInsert() { public void testChangeReference() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUserSettings settings = MockUserSettings.builder(dataManager) .id(UUID.randomUUID()) @@ -120,6 +124,7 @@ public void testChangeReference() { public void testDeleteStrategyCascade() throws SQLException { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); Connection h2Connection = getH2Connection(dataManager); Connection pgConnection = getConnection(); UUID id = UUID.randomUUID(); @@ -171,6 +176,7 @@ public void testDeleteStrategyCascade() throws SQLException { public void testUpdateHandlerUpdate() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); MockUser user = MockUser.builder(dataManager) .id(UUID.randomUUID()) @@ -217,6 +223,7 @@ public void testUpdateHandlerUpdate() { public void testReferenceNoFkey() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID bestBuddyId = UUID.randomUUID(); diff --git a/core/src/test/java/net/staticstudios/data/SnapshotTest.java b/core/src/test/java/net/staticstudios/data/SnapshotTest.java index 58e9f658..dfb4b7f5 100644 --- a/core/src/test/java/net/staticstudios/data/SnapshotTest.java +++ b/core/src/test/java/net/staticstudios/data/SnapshotTest.java @@ -14,6 +14,7 @@ public class SnapshotTest extends DataTest { public void testPersistentValues() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); dataManager.load(MockUser.class); + dataManager.finishLoading(); UUID id = UUID.randomUUID(); From 9857c358185004875205a3fe8f008256ccf13f63 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 29 Dec 2025 01:53:44 -0500 Subject: [PATCH 64/75] add update post insert action --- .../data/insert/PostInsertAction.java | 32 ++++++++++++ .../insert/UpdateColumnPostInsertAction.java | 49 +++++++++++++++++++ .../data/util/InsertStatement.java | 42 +++++++++++++++- 3 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/insert/UpdateColumnPostInsertAction.java diff --git a/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java b/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java index bd214db0..3b472566 100644 --- a/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java +++ b/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java @@ -8,6 +8,7 @@ import net.staticstudios.data.util.*; import java.util.List; +import java.util.Map; public interface PostInsertAction { @@ -72,6 +73,37 @@ static InsertIntoJoinTableManyToManyPostInsertAction.Builder manyToMany(Class UpdateColumnPostInsertAction set(T holder, String column, Object value) { + UniqueDataMetadata metadata = holder.getMetadata(); + + return new UpdateColumnPostInsertAction( + metadata.schema(), + metadata.table(), + holder.getIdColumns(), + Map.of(column, value) + ); + } + + static UpdateColumnPostInsertAction set(T holder, Map updateValues) { + UniqueDataMetadata metadata = holder.getMetadata(); + + return new UpdateColumnPostInsertAction( + metadata.schema(), + metadata.table(), + holder.getIdColumns(), + updateValues + ); + } + + static UpdateColumnPostInsertAction set(String schema, String table, List idColumns, Map updateValues) { + return new UpdateColumnPostInsertAction( + schema, + table, + new ColumnValuePairs(idColumns.toArray(new ColumnValuePair[0])), + updateValues + ); + } + List getStatements(); } diff --git a/core/src/main/java/net/staticstudios/data/insert/UpdateColumnPostInsertAction.java b/core/src/main/java/net/staticstudios/data/insert/UpdateColumnPostInsertAction.java new file mode 100644 index 00000000..b2661b2b --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/insert/UpdateColumnPostInsertAction.java @@ -0,0 +1,49 @@ +package net.staticstudios.data.insert; + +import net.staticstudios.data.util.ColumnValuePair; +import net.staticstudios.data.util.ColumnValuePairs; +import net.staticstudios.data.util.SQlStatement; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class UpdateColumnPostInsertAction implements PostInsertAction { + private final String schema; + private final String table; + private final ColumnValuePairs idColumns; + private final Map updateValues; + + + public UpdateColumnPostInsertAction(String schema, String table, ColumnValuePairs idColumns, Map updateValues) { + this.schema = schema; + this.table = table; + this.idColumns = idColumns; + this.updateValues = updateValues; + } + + @Override + public List getStatements() { + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("UPDATE ") + .append("\"").append(schema).append("\".\"").append(table).append("\" SET "); + List values = new ArrayList<>(); + for (Map.Entry entry : updateValues.entrySet()) { + sqlBuilder.append("\"").append(entry.getKey()).append("\" = ?, "); + values.add(entry.getValue()); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.append(" WHERE "); + for (ColumnValuePair idColumn : idColumns) { + sqlBuilder.append("\"").append(idColumn.column()).append("\" = ? AND "); + values.add(idColumn.value()); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + + return List.of(new SQlStatement( + sqlBuilder.toString(), + sqlBuilder.toString(), + values + )); + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/InsertStatement.java b/core/src/main/java/net/staticstudios/data/util/InsertStatement.java index 04c5b8ac..9ed8b0bb 100644 --- a/core/src/main/java/net/staticstudios/data/util/InsertStatement.java +++ b/core/src/main/java/net/staticstudios/data/util/InsertStatement.java @@ -30,7 +30,7 @@ public InsertStatement(DataManager dataManager, SQLTable table, ColumnValuePairs public static boolean checkForCycles(List statements) { Set visited = new HashSet<>(); - Set recursionStack = new HashSet<>(); + Set recursionStack = new LinkedHashSet<>(); for (InsertStatement statement : statements) { if (detectCycleDFS(statement, visited, recursionStack)) { @@ -42,7 +42,7 @@ public static boolean checkForCycles(List statements) { private static boolean detectCycleDFS(InsertStatement current, Set visited, Set stack) { if (stack.contains(current)) { - throw new IllegalStateException("Dependency cycle detected involving table: " + current.getTable().getName()); + throw new IllegalStateException(buildCycleErrorMessage(stack, current)); } if (visited.contains(current)) { return false; @@ -61,6 +61,44 @@ private static boolean detectCycleDFS(InsertStatement current, Set stack, InsertStatement current) { + StringBuilder sb = new StringBuilder(); + sb.append("Dependency cycle detected:\n"); + + boolean cycleStarted = false; + for (InsertStatement node : stack) { + if (node == current) { + cycleStarted = true; + } + if (cycleStarted) { + sb.append(formatStatementForError(node)).append(" -> \n"); + } + } + sb.append(formatStatementForError(current)); + + return sb.toString(); + } + + private static String formatStatementForError(InsertStatement stmt) { + StringBuilder sb = new StringBuilder(); + sb.append(stmt.getTable().getName()); + sb.append("["); + + ColumnValuePair[] pairs = stmt.getIdColumns().getPairs(); + for (int i = 0; i < pairs.length; i++) { + ColumnValuePair pair = pairs[i]; + sb.append(pair.column()).append("=").append(pair.value()); + if (i < pairs.length - 1) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + public static List sort(List statements) { List sortedList = new ArrayList<>(); Set visited = new HashSet<>(); From ada04d3c9f41dbe3539367777f2032b6de7ed2a9 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 29 Dec 2025 15:20:30 -0500 Subject: [PATCH 65/75] fix more ambiguity --- .../impl/data/PersistentOneToManyValueCollectionImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ebae240e..055a9017 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 @@ -424,7 +424,7 @@ private Set getValues() { UniqueDataMetadata holderMetadata = holder.getMetadata(); DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); StringBuilder sqlBuilder = new StringBuilder(); - sqlBuilder.append("SELECT ").append("\"").append(dataColumn).append("\", "); + sqlBuilder.append("SELECT ").append("\"").append(dataSchema).append("\".\"").append(dataTable).append("\".\"").append(dataColumn).append("\", "); for (ColumnMetadata columnMetadata : holderMetadata.idColumns()) { sqlBuilder.append("\"").append(columnMetadata.schema()).append("\".\"").append(columnMetadata.table()).append("\".\"").append(columnMetadata.name()).append("\", "); } @@ -441,7 +441,7 @@ private Set getValues() { for (Link entry : link) { String theirColumn = entry.columnInReferencedTable(); - sqlBuilder.append("\"").append(theirColumn).append("\" = \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(entry.columnInReferringTable()).append("\" AND "); + sqlBuilder.append("\"").append(dataSchema).append("\".\"").append(dataTable).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 "); From dd1b074f1e3f554da0b188e58aacebb210c9641d Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 29 Dec 2025 15:48:51 -0500 Subject: [PATCH 66/75] do not create/enforce insert dependencies. this is not needed and introduced lots of issues --- .../net/staticstudios/data/DataManager.java | 3 +- .../data/util/InsertStatement.java | 45 +++------- .../javac/javac/BuilderProcessor.java | 88 ++++++++----------- 3 files changed, 50 insertions(+), 86 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index e7eba754..e8d83374 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -725,7 +725,6 @@ public final void finishLoading() { finishedLoading = true; dataAccessor.resync(); - //todo: can't call load() after this, and only after this is called do we sync data. } @SafeVarargs @@ -1138,7 +1137,7 @@ public void insert(BatchInsert batch, InsertMode insertMode) { for (InsertStatement insertStatement : List.copyOf(insertStatements)) { insertStatement.calculateRequiredDependencies(); - insertStatements.addAll(insertStatement.createUnmetDependencyStatements(insertStatements)); + insertStatement.satisfyDependencies(insertStatements); } InsertStatement.checkForCycles(insertStatements); diff --git a/core/src/main/java/net/staticstudios/data/util/InsertStatement.java b/core/src/main/java/net/staticstudios/data/util/InsertStatement.java index 9ed8b0bb..c12d5678 100644 --- a/core/src/main/java/net/staticstudios/data/util/InsertStatement.java +++ b/core/src/main/java/net/staticstudios/data/util/InsertStatement.java @@ -135,25 +135,26 @@ public void set(String column, InsertStrategy insertStrategy, Object value) { columnValues.put(column, new Value(insertStrategy, value)); } - public List createUnmetDependencyStatements(List existingStatements) { + + public void satisfyDependencies(List existingStatements) { Preconditions.checkState(dependencyRequirements != null, "Must call calculateRequiredDependencies() before checking for unmet dependencies."); - List unmetStatements = new ArrayList<>(); for (DependencyRequirement requirement : dependencyRequirements) { - boolean satisfied = false; +// boolean satisfied = false; for (InsertStatement existingStatement : existingStatements) { if (requirement.isSatisfiedBy(existingStatement)) { - satisfied = true; +// satisfied = true; dependantOn.add(existingStatement); break; } } - if (!satisfied) { - InsertStatement satisfyingStatement = requirement.createSatisfyingStatement(dataManager); - unmetStatements.add(satisfyingStatement); - dependantOn.add(satisfyingStatement); - } + + // the data might be present in the database, do not enforce this here. this will be the user's responsibility. +// if (!satisfied) { +// throw new IllegalStateException("Unmet dependency for statement: \"" + asStatement().getPgSql() + "\" requiring " + +// "schema \"" + requirement.schema + "\", table \"" + requirement.table + "\", with column values " + +// requirement.requiredColumnValues); +// } } - return unmetStatements; } public void calculateRequiredDependencies() { @@ -317,30 +318,6 @@ public boolean isSatisfiedBy(InsertStatement statement) { }); return lookingFor.isEmpty(); } - - public InsertStatement createSatisfyingStatement(DataManager dataManager) { - SQLSchema sqlSchema = Objects.requireNonNull(dataManager.getSQLBuilder().getSchema(schema)); - SQLTable sqlTable = Objects.requireNonNull(sqlSchema.getTable(table)); - ColumnValuePair[] idColumns = new ColumnValuePair[sqlTable.getIdColumns().size()]; - - for (ColumnMetadata idColumn : sqlTable.getIdColumns()) { - Optional matchingPair = requiredColumnValues.stream() - .filter(pair -> pair.column().equals(idColumn.name())) - .findFirst(); - Preconditions.checkState(matchingPair.isPresent(), "Cannot create satisfying statement for dependency requirement: missing id column value for " + idColumn.name()); - idColumns[sqlTable.getIdColumns().indexOf(idColumn)] = matchingPair.get(); - } - - InsertStatement statement = new InsertStatement(dataManager, sqlTable, new ColumnValuePairs(idColumns)); - for (ColumnValuePair pair : requiredColumnValues) { - if (sqlTable.getIdColumns().stream().noneMatch(col -> col.name().equals(pair.column()))) { - statement.set(pair.column(), InsertStrategy.PREFER_EXISTING, pair.value()); - } - } - - return statement; - } - } } diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java index 96e703ee..5bf639d8 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java @@ -504,58 +504,46 @@ private void makeInsertContextMethod(Collection parsedPer } - bodyStatements.add( - If( - Binary( - JCTree.Tag.NE, - Ident(names.fromString(pv.getFieldName())), - Literal(TypeTag.BOT, null) - ), - Block(0, - List.from( - fpv.getLinks().stream().map(link -> { - String linkingColumnReferencesFieldName = fpv.getFieldName() + "$" + link.columnInReferringTable() + "_references"; - String linkingColumnReferredBySchemaFieldName = fpv.getFieldName() + "$_referredBySchema"; - String linkingColumnReferredByTableFieldName = fpv.getFieldName() + "$_referredByTable"; - String linkingColumnReferredByColumnFieldName = fpv.getFieldName() + "$" + link.columnInReferringTable() + "_referredByColumn"; - - - return Exec( - Apply( - List.nil(), - Select( - Ident(names.fromString("ctx")), - names.fromString("set") - ), - List.of( - Ident(names.fromString(pv.getFieldName() + "$schema")), - Ident(names.fromString(pv.getFieldName() + "$table")), - Ident(names.fromString(linkingColumnReferencesFieldName)), - Apply( - List.nil(), - Select( - Ident(names.fromString("ctx")), - names.fromString("getValue") - ), - List.of( - Ident(names.fromString(linkingColumnReferredBySchemaFieldName)), - Ident(names.fromString(linkingColumnReferredByTableFieldName)), - Ident(names.fromString(linkingColumnReferredByColumnFieldName)) + bodyStatements.addAll( + fpv.getLinks().stream().map(link -> { + String linkingColumnReferencesFieldName = fpv.getFieldName() + "$" + link.columnInReferringTable() + "_references"; + String linkingColumnReferredBySchemaFieldName = fpv.getFieldName() + "$_referredBySchema"; + String linkingColumnReferredByTableFieldName = fpv.getFieldName() + "$_referredByTable"; + String linkingColumnReferredByColumnFieldName = fpv.getFieldName() + "$" + link.columnInReferringTable() + "_referredByColumn"; + + + return Exec( + Apply( + List.nil(), + Select( + Ident(names.fromString("ctx")), + names.fromString("set") + ), + List.of( + Ident(names.fromString(pv.getFieldName() + "$schema")), + Ident(names.fromString(pv.getFieldName() + "$table")), + Ident(names.fromString(linkingColumnReferencesFieldName)), + Apply( + List.nil(), + Select( + Ident(names.fromString("ctx")), + names.fromString("getValue") + ), + List.of( + Ident(names.fromString(linkingColumnReferredBySchemaFieldName)), + Ident(names.fromString(linkingColumnReferredByTableFieldName)), + Ident(names.fromString(linkingColumnReferredByColumnFieldName)) - ) - ), - Select( - chainDots("net", "staticstudios", "data", "InsertStrategy"), - names.fromString("PREFER_EXISTING") - ) - ) ) - ); - }).toList() - ) - ), - null - ) + ), + Select( + chainDots("net", "staticstudios", "data", "InsertStrategy"), + names.fromString("PREFER_EXISTING") + ) + ) + ) + ); + }).toList() ); } From 6d48a74dfffa4933a01198d6272eef08fbebf4e5 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 29 Dec 2025 18:56:56 -0500 Subject: [PATCH 67/75] ignore empty updates --- .../java/net/staticstudios/data/impl/h2/H2DataAccessor.java | 4 ++++ 1 file changed, 4 insertions(+) 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 40f15996..638fd390 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 @@ -103,6 +103,10 @@ public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener } } + if (index == 0) { + return; // nothing changed + } + for (Pair changed : changedValues) { if (changed == null) break; String column = changed.first(); From 53fac7c246d791988855e61a6b95075aef4bbd9c Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Mon, 29 Dec 2025 22:08:39 -0500 Subject: [PATCH 68/75] Avoid adding methods for references to the builder. while seemly convenient, it can be misleading. For example, do i update the values in the referenced table, or the referring table? --- .../ide/intellij/DataPsiAugmentProvider.java | 17 +- .../javac/javac/BuilderProcessor.java | 512 +++++++++--------- 2 files changed, 268 insertions(+), 261 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 cac747e3..a509dee2 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 @@ -174,15 +174,18 @@ private SyntheticBuilderClass createBuilderBuilderClass(PsiClass parentClass) { setterMethod.addModifier(PsiModifier.PUBLIC); setterMethod.addModifier(PsiModifier.FINAL); - builderClass.addMethod(setterMethod); - } else if (IntelliJPluginUtils.isValidReference(psiField)) { - SyntheticMethod setterMethod = new SyntheticMethod(parentClass, builderClass, psiField.getName(), builderType); - setterMethod.addParameter(psiField.getName(), innerType); - setterMethod.addModifier(PsiModifier.PUBLIC); - setterMethod.addModifier(PsiModifier.FINAL); - builderClass.addMethod(setterMethod); } + // Avoid adding methods for references to the builder. while seemly convenient, it can be misleading. + // For example, do i update the values in the referenced table, or the referring table? +// else if (IntelliJPluginUtils.isValidReference(psiField)) { +// SyntheticMethod setterMethod = new SyntheticMethod(parentClass, builderClass, psiField.getName(), builderType); +// setterMethod.addParameter(psiField.getName(), innerType); +// setterMethod.addModifier(PsiModifier.PUBLIC); +// setterMethod.addModifier(PsiModifier.FINAL); +// +// builderClass.addMethod(setterMethod); +// } //todo: support CachedValues, similar to PVs } diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java index 5bf639d8..b605c8c0 100644 --- a/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java @@ -33,9 +33,9 @@ protected void process() { processValue(pv); } - for (ParsedReference ref : references) { - processReference(ref); - } +// for (ParsedReference ref : references) { +// processReference(ref); +// } makeInsertContextMethod(persistentValues, references); makeInsertModeMethod(); @@ -92,164 +92,166 @@ private void processValue(ParsedPersistentValue pv) { } } - private void processReference(ParsedReference ref) { - String idColumnValuePairsFieldName = ref.getFieldName() + "_reference$idColumnValuePairs"; - String schemaFieldName = ref.getFieldName() + "_reference$schema"; - String tableFieldName = ref.getFieldName() + "_reference$table"; - - createField(VarDef( - Modifiers(Flags.PRIVATE), - names.fromString(idColumnValuePairsFieldName), - TypeArray(chainDots("net", "staticstudios", "data", "util", "ColumnValuePair")), - Literal(TypeTag.BOT, null) - ), builderClassDecl); - - createField(VarDef( - Modifiers(Flags.PRIVATE), - names.fromString(schemaFieldName), - Ident(names.fromString("String")), - Literal(TypeTag.BOT, null) - ), builderClassDecl); - - createField(VarDef( - Modifiers(Flags.PRIVATE), - names.fromString(tableFieldName), - Ident(names.fromString("String")), - Literal(TypeTag.BOT, null) - ), builderClassDecl); - - var handleNotNull = Block(0, List.of( - Exec( - Assign( - Select( - Ident(names.fromString("this")), - names.fromString(idColumnValuePairsFieldName) - ), - Apply( - List.nil(), - Select( - Apply( - List.nil(), - Select( - Ident(names.fromString(ref.getFieldName())), - names.fromString("getIdColumns") - ), - List.nil() - ), - names.fromString("getPairs") - ), - List.nil() - ) - ) - ), - VarDef( - Modifiers(0), - names.fromString("__$metadata"), - chainDots("net", "staticstudios", "data", "util", "UniqueDataMetadata"), - Apply( - List.nil(), - Select( - Ident(names.fromString(ref.getFieldName())), - names.fromString("getMetadata") - ), - List.nil() - ) - ), - Exec( - Assign( - Select( - Ident(names.fromString("this")), - names.fromString(schemaFieldName) - ), - Apply( - List.nil(), - Select( - Ident(names.fromString("__$metadata")), - names.fromString("schema") - ), - List.nil() - ) - ) - ), - Exec( - Assign( - Select( - Ident(names.fromString("this")), - names.fromString(tableFieldName) - ), - Apply( - List.nil(), - Select( - Ident(names.fromString("__$metadata")), - names.fromString("table") - ), - List.nil() - ) - ) - ) - )); - - var handleNull = Block(0, List.of( - Exec( - Assign( - Select( - Ident(names.fromString("this")), - names.fromString(idColumnValuePairsFieldName) - ), - Literal(TypeTag.BOT, null) - ) - ), - Exec( - Assign( - Select( - Ident(names.fromString("this")), - names.fromString(schemaFieldName) - ), - Literal(TypeTag.BOT, null) - ) - ), - Exec( - Assign( - Select( - Ident(names.fromString("this")), - names.fromString(tableFieldName) - ), - Literal(TypeTag.BOT, null) - ) - ) - )); - - createMethod(MethodDef( - Modifiers(Flags.PUBLIC | Flags.FINAL), - names.fromString(ref.getFieldName()), - Ident(names.fromString(getBuilderClassName())), - List.nil(), - List.of( - VarDef( - Modifiers(Flags.PARAMETER), - names.fromString(ref.getFieldName()), - chainDots(ref.getTypeFQNParts()), - null - ) - ), - List.nil(), - Block(0, List.of( - If( - Binary( - JCTree.Tag.NE, - Ident(names.fromString(ref.getFieldName())), - Literal(TypeTag.BOT, null) - ), - handleNotNull, - handleNull - ), - Return( - Ident(names.fromString("this")) - ) - )), - null - ), builderClassDecl); - } + // Avoid adding methods for references to the builder. while seemly convenient, it can be misleading. + // For example, do i update the values in the referenced table, or the referring table? +// private void processReference(ParsedReference ref) { +// String idColumnValuePairsFieldName = ref.getFieldName() + "_reference$idColumnValuePairs"; +// String schemaFieldName = ref.getFieldName() + "_reference$schema"; +// String tableFieldName = ref.getFieldName() + "_reference$table"; +// +// createField(VarDef( +// Modifiers(Flags.PRIVATE), +// names.fromString(idColumnValuePairsFieldName), +// TypeArray(chainDots("net", "staticstudios", "data", "util", "ColumnValuePair")), +// Literal(TypeTag.BOT, null) +// ), builderClassDecl); +// +// createField(VarDef( +// Modifiers(Flags.PRIVATE), +// names.fromString(schemaFieldName), +// Ident(names.fromString("String")), +// Literal(TypeTag.BOT, null) +// ), builderClassDecl); +// +// createField(VarDef( +// Modifiers(Flags.PRIVATE), +// names.fromString(tableFieldName), +// Ident(names.fromString("String")), +// Literal(TypeTag.BOT, null) +// ), builderClassDecl); +// +// var handleNotNull = Block(0, List.of( +// Exec( +// Assign( +// Select( +// Ident(names.fromString("this")), +// names.fromString(idColumnValuePairsFieldName) +// ), +// Apply( +// List.nil(), +// Select( +// Apply( +// List.nil(), +// Select( +// Ident(names.fromString(ref.getFieldName())), +// names.fromString("getIdColumns") +// ), +// List.nil() +// ), +// names.fromString("getPairs") +// ), +// List.nil() +// ) +// ) +// ), +// VarDef( +// Modifiers(0), +// names.fromString("__$metadata"), +// chainDots("net", "staticstudios", "data", "util", "UniqueDataMetadata"), +// Apply( +// List.nil(), +// Select( +// Ident(names.fromString(ref.getFieldName())), +// names.fromString("getMetadata") +// ), +// List.nil() +// ) +// ), +// Exec( +// Assign( +// Select( +// Ident(names.fromString("this")), +// names.fromString(schemaFieldName) +// ), +// Apply( +// List.nil(), +// Select( +// Ident(names.fromString("__$metadata")), +// names.fromString("schema") +// ), +// List.nil() +// ) +// ) +// ), +// Exec( +// Assign( +// Select( +// Ident(names.fromString("this")), +// names.fromString(tableFieldName) +// ), +// Apply( +// List.nil(), +// Select( +// Ident(names.fromString("__$metadata")), +// names.fromString("table") +// ), +// List.nil() +// ) +// ) +// ) +// )); +// +// var handleNull = Block(0, List.of( +// Exec( +// Assign( +// Select( +// Ident(names.fromString("this")), +// names.fromString(idColumnValuePairsFieldName) +// ), +// Literal(TypeTag.BOT, null) +// ) +// ), +// Exec( +// Assign( +// Select( +// Ident(names.fromString("this")), +// names.fromString(schemaFieldName) +// ), +// Literal(TypeTag.BOT, null) +// ) +// ), +// Exec( +// Assign( +// Select( +// Ident(names.fromString("this")), +// names.fromString(tableFieldName) +// ), +// Literal(TypeTag.BOT, null) +// ) +// ) +// )); +// +// createMethod(MethodDef( +// Modifiers(Flags.PUBLIC | Flags.FINAL), +// names.fromString(ref.getFieldName()), +// Ident(names.fromString(getBuilderClassName())), +// List.nil(), +// List.of( +// VarDef( +// Modifiers(Flags.PARAMETER), +// names.fromString(ref.getFieldName()), +// chainDots(ref.getTypeFQNParts()), +// null +// ) +// ), +// List.nil(), +// Block(0, List.of( +// If( +// Binary( +// JCTree.Tag.NE, +// Ident(names.fromString(ref.getFieldName())), +// Literal(TypeTag.BOT, null) +// ), +// handleNotNull, +// handleNull +// ), +// Return( +// Ident(names.fromString("this")) +// ) +// )), +// null +// ), builderClassDecl); +// } private void processFpv(ParsedForeignPersistentValue fpv) { @@ -547,100 +549,102 @@ private void makeInsertContextMethod(Collection parsedPer ); } - for (ParsedReference ref : parsedReferences) { - String idColumnValuePairsFieldName = ref.getFieldName() + "_reference$idColumnValuePairs"; - String schemaFieldName = ref.getFieldName() + "_reference$schema"; - String tableFieldName = ref.getFieldName() + "_reference$table"; - - bodyStatements.add( - If( - Binary( - JCTree.Tag.NE, - Ident(names.fromString(idColumnValuePairsFieldName)), - Literal(TypeTag.BOT, null) - ), - ForLoop( - List.of( - VarDef( - Modifiers(0), - names.fromString("i"), - TypeIdent(TypeTag.INT), - Literal(0) - ) - ), - Binary( - JCTree.Tag.LT, - Ident(names.fromString("i")), - Select( - Ident(names.fromString(idColumnValuePairsFieldName)), - names.fromString("length") - ) - ), - List.of( - Exec( - Unary( - JCTree.Tag.POSTINC, - Ident(names.fromString("i")) - ) - ) - ), - Block( - 0, - List.of( - Exec( - Apply( - List.nil(), - Select( - Ident(names.fromString("ctx")), - names.fromString("set") - ), - List.of( - Select( - Ident(names.fromString("this")), - names.fromString(schemaFieldName) - ), - Select( - Ident(names.fromString("this")), - names.fromString(tableFieldName) - ), - Apply( - List.nil(), - Select( - Indexed( - Ident(names.fromString(idColumnValuePairsFieldName)), - Ident(names.fromString("i")) - ), - names.fromString("column") - ), - List.nil() - ), - Apply( - List.nil(), - Select( - Indexed( - Ident(names.fromString(idColumnValuePairsFieldName)), - Ident(names.fromString("i")) - ), - names.fromString("value") - ), - List.nil() - ), - Select( - chainDots("net", "staticstudios", "data", "InsertStrategy"), - names.fromString("OVERWRITE_EXISTING") - ) - ) - ) - ) - - ) - ) - ), - null - ) - ); - } + // Avoid adding methods for references to the builder. while seemly convenient, it can be misleading. + // For example, do i update the values in the referenced table, or the referring table? +// for (ParsedReference ref : parsedReferences) { +// String idColumnValuePairsFieldName = ref.getFieldName() + "_reference$idColumnValuePairs"; +// String schemaFieldName = ref.getFieldName() + "_reference$schema"; +// String tableFieldName = ref.getFieldName() + "_reference$table"; +// +// bodyStatements.add( +// If( +// Binary( +// JCTree.Tag.NE, +// Ident(names.fromString(idColumnValuePairsFieldName)), +// Literal(TypeTag.BOT, null) +// ), +// ForLoop( +// List.of( +// VarDef( +// Modifiers(0), +// names.fromString("i"), +// TypeIdent(TypeTag.INT), +// Literal(0) +// ) +// ), +// Binary( +// JCTree.Tag.LT, +// Ident(names.fromString("i")), +// Select( +// Ident(names.fromString(idColumnValuePairsFieldName)), +// names.fromString("length") +// ) +// ), +// List.of( +// Exec( +// Unary( +// JCTree.Tag.POSTINC, +// Ident(names.fromString("i")) +// ) +// ) +// ), +// Block( +// 0, +// List.of( +// Exec( +// Apply( +// List.nil(), +// Select( +// Ident(names.fromString("ctx")), +// names.fromString("set") +// ), +// List.of( +// Select( +// Ident(names.fromString("this")), +// names.fromString(schemaFieldName) +// ), +// Select( +// Ident(names.fromString("this")), +// names.fromString(tableFieldName) +// ), +// Apply( +// List.nil(), +// Select( +// Indexed( +// Ident(names.fromString(idColumnValuePairsFieldName)), +// Ident(names.fromString("i")) +// ), +// names.fromString("column") +// ), +// List.nil() +// ), +// Apply( +// List.nil(), +// Select( +// Indexed( +// Ident(names.fromString(idColumnValuePairsFieldName)), +// Ident(names.fromString("i")) +// ), +// names.fromString("value") +// ), +// List.nil() +// ), +// Select( +// chainDots("net", "staticstudios", "data", "InsertStrategy"), +// names.fromString("OVERWRITE_EXISTING") +// ) +// ) +// ) +// ) +// +// ) +// ) +// ), +// null +// ) +// ); +// } createMethod(MethodDef( Modifiers(Flags.PUBLIC | Flags.FINAL), From 5fe96f01bfbd7b39abdfa53ac8f818e548d2833f Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 30 Dec 2025 15:17:52 -0500 Subject: [PATCH 69/75] references now properly select the referenced object's id column values rather than the linking column values --- .../data/impl/data/ReferenceImpl.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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 7c561807..6231079a 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 @@ -103,18 +103,33 @@ public ColumnValuePairs getReferencedColumnValuePairs() { ColumnValuePair[] idColumns = new ColumnValuePair[link.size()]; int i = 0; 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("\"").append(myColumn).append("\", "); + sqlBuilder.append("_referring.\"").append(myColumn).append("\", "); } sqlBuilder.setLength(sqlBuilder.length() - 2); - sqlBuilder.append(" FROM \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" WHERE "); + + 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); + + sqlBuilder.append(" WHERE "); for (ColumnValuePair columnValuePair : holder.getIdColumns()) { - sqlBuilder.append("\"").append(columnValuePair.column()).append("\" = ? AND "); + sqlBuilder.append("_referring.\"").append(columnValuePair.column()).append("\" = ? AND "); } sqlBuilder.setLength(sqlBuilder.length() - 5); From df67311f092e158a2e7f224e1bb5828fce949df0 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 30 Dec 2025 22:49:45 -0500 Subject: [PATCH 70/75] when loading a class account for recursively extracted metadata --- .../net/staticstudios/data/DataManager.java | 17 ++++++------- .../data/impl/h2/H2DataAccessor.java | 24 +++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index e8d83374..10e312cf 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -733,7 +733,7 @@ public final void load(Class... classes) { List extracted = new ArrayList<>(); for (Class clazz : classes) { - extracted.add(extractMetadata(clazz)); + extracted.addAll(extractMetadata(clazz)); } List defs = new ArrayList<>(); for (Class clazz : classes) { @@ -763,12 +763,14 @@ public final void load(Class... classes) { dataAccessor.discoverRedisKeys(partialRedisKeys); } - public UniqueDataMetadata extractMetadata(Class clazz) { + public List extractMetadata(Class clazz) { Data dataAnnotation = clazz.getAnnotation(Data.class); - return extractMetadata(clazz, dataAnnotation); + List extracted = new ArrayList<>(); + extractMetadata(clazz, dataAnnotation, extracted); + return extracted; } - public UniqueDataMetadata extractMetadata(Class clazz, Data fallbackDataAnnotation) { + public void extractMetadata(Class clazz, Data fallbackDataAnnotation, List extracted) { logger.debug("Extracting metadata for UniqueData class {}", clazz.getName()); Preconditions.checkArgument(!uniqueDataMetadataMap.containsKey(clazz), "UniqueData class %s has already been parsed", clazz.getName()); Data dataAnnotation = clazz.getAnnotation(Data.class); @@ -814,6 +816,7 @@ public UniqueDataMetadata extractMetadata(Class clazz, Dat persistentCollectionMetadataMap ); uniqueDataMetadataMap.put(clazz, metadata); + extracted.add(metadata); } for (Field field : ReflectionUtils.getFields(clazz, Relation.class)) { @@ -821,7 +824,7 @@ public UniqueDataMetadata extractMetadata(Class clazz, Dat if (genericType != null && !Modifier.isAbstract(genericType.getModifiers()) && UniqueData.class.isAssignableFrom(genericType)) { Class dependencyClass = genericType.asSubclass(UniqueData.class); if (!uniqueDataMetadataMap.containsKey(dependencyClass)) { - extractMetadata(dependencyClass); + extractMetadata(dependencyClass, null, extracted); } } } @@ -830,11 +833,9 @@ public UniqueDataMetadata extractMetadata(Class clazz, Dat if (superClass != null && UniqueData.class.isAssignableFrom(superClass) && superClass != UniqueData.class) { Class superUniqueDataClass = superClass.asSubclass(UniqueData.class); if (!uniqueDataMetadataMap.containsKey(superUniqueDataClass)) { - extractMetadata(superUniqueDataClass, dataAnnotation); + extractMetadata(superUniqueDataClass, dataAnnotation, extracted); } } - - return metadata; } public UniqueDataMetadata getMetadata(Class clazz) { 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 638fd390..6f424e26 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 @@ -296,22 +296,20 @@ public synchronized void sync(List schemaTables, List redis } } } - if (!redisPartialKeys.isEmpty()) { - for (String partialKey : redisPartialKeys) { - String cursor = ScanParams.SCAN_POINTER_START; - ScanParams scanParams = new ScanParams().match(partialKey).count(1000); + for (String partialKey : redisPartialKeys) { + String cursor = ScanParams.SCAN_POINTER_START; + ScanParams scanParams = new ScanParams().match(partialKey).count(1000); - do { - ScanResult scanResult = jedis.scan(cursor, scanParams); - cursor = scanResult.getCursor(); + do { + ScanResult scanResult = jedis.scan(cursor, scanParams); + cursor = scanResult.getCursor(); - for (String key : scanResult.getResult()) { - redisCache.put(key, new RedisCacheEntry(jedis.get(key), Instant.now())); - } - } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); + for (String key : scanResult.getResult()) { + redisCache.put(key, new RedisCacheEntry(jedis.get(key), Instant.now())); + } + } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); - redisListener.listen(partialKey, this::handleRedisEvent); - } + redisListener.listen(partialKey, this::handleRedisEvent); } }).join(); From d360ef7173f00f6bffc2efadd7d040ec7ec59df7 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 30 Dec 2025 23:42:37 -0500 Subject: [PATCH 71/75] update build.gradle --- build.gradle | 7 +++++-- processor/build.gradle | 7 ------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 120a0c3b..1eedf040 100644 --- a/build.gradle +++ b/build.gradle @@ -28,9 +28,12 @@ subprojects { publishing { repositories { maven { + credentials(org.gradle.api.credentials.PasswordCredentials.class) name = "StaticStudios" - url = "https://repo.staticstudios.net/private/" - credentials(org.gradle.api.credentials.PasswordCredentials) + var base = "https://repo.staticstudios.net" + var releasesRepoUrl = "$base/releases/" + var snapshotsRepoUrl = "$base/snapshots/" + setUrl((version.toString().endsWith("SNAPSHOT")) ? snapshotsRepoUrl : releasesRepoUrl) } } } diff --git a/processor/build.gradle b/processor/build.gradle index 912902bf..0e799d9e 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -44,13 +44,6 @@ tasks.named("publish") { publishing { - repositories { - maven { - credentials(org.gradle.api.credentials.PasswordCredentials.class) - name = "StaticStudios" - setUrl("https://repo.staticstudios.net/private/") - } - } publications { maven(MavenPublication) { artifactId = 'static-data-processor' From 46191f1781b1188eb63437683553ca049f09dbe3 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 31 Dec 2025 01:19:26 -0500 Subject: [PATCH 72/75] fix one to many value collections --- .../impl/data/PersistentOneToManyValueCollectionImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 055a9017..1d56dc0c 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 @@ -201,7 +201,7 @@ public boolean addAll(@NotNull Collection c) { for (T entry : c) { transaction.update(insertStatement, () -> { List values = new ArrayList<>(holderLinkingValues); - values.add(entry); + values.add(holder.getDataManager().serialize(entry)); return values; }); } @@ -452,7 +452,7 @@ private Set getValues() { try (ResultSet rs = dataAccessor.executeQuery(sql, holder.getIdColumns().stream().map(ColumnValuePair::value).toList())) { while (rs.next()) { Object value = rs.getObject(dataColumn); - values.add(type.cast(value)); + values.add(holder.getDataManager().deserialize(type, value)); } } catch (SQLException e) { throw new RuntimeException(e); From 84ceabe720a9e9cf6e7404b56f2b99849d8ba980 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 31 Dec 2025 02:01:28 -0500 Subject: [PATCH 73/75] add error logging to the task queue --- .../main/java/net/staticstudios/data/util/TaskQueue.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/net/staticstudios/data/util/TaskQueue.java b/core/src/main/java/net/staticstudios/data/util/TaskQueue.java index e81d618a..1afb4846 100644 --- a/core/src/main/java/net/staticstudios/data/util/TaskQueue.java +++ b/core/src/main/java/net/staticstudios/data/util/TaskQueue.java @@ -4,6 +4,8 @@ import com.zaxxer.hikari.pool.HikariPool; import net.staticstudios.utils.ShutdownStage; import net.staticstudios.utils.ThreadUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; @@ -12,6 +14,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public class TaskQueue { + private static final Logger LOGGER = LoggerFactory.getLogger(TaskQueue.class); private final BlockingDeque taskQueue = new LinkedBlockingDeque<>(); private final AtomicBoolean isShutdown = new AtomicBoolean(false); private final ExecutorService executor; @@ -57,6 +60,7 @@ public CompletableFuture submitTask(ConnectionJedisConsumer task) { task.accept(connection, jedis); future.complete(null); } catch (Exception e) { + LOGGER.error("Error executing task in TaskQueue", e); future.completeExceptionally(e); } }); @@ -84,7 +88,7 @@ private void start() { connection.setAutoCommit(true); } } catch (Exception e) { - e.printStackTrace(); + LOGGER.error("Error executing task in TaskQueue", e); } } }); @@ -105,7 +109,7 @@ private void shutdown() { executor.shutdownNow(); } } catch (InterruptedException e) { - e.printStackTrace(); + LOGGER.error("Interrupted while shutting down TaskQueue", e); } } } From ce9cf6edc0ac67317ec97c06b396964a26951fd4 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 31 Dec 2025 07:12:02 -0500 Subject: [PATCH 74/75] only call update handlers and collection add/remove handlers after a transaction has been committed. this also means that update handlers are called instantly, and no longer require being submitted to another thread. --- .../net/staticstudios/data/DataManager.java | 178 ++++++----- .../data/impl/h2/DelayedDatabaseTask.java | 4 - .../data/impl/h2/H2DataAccessor.java | 96 ++++-- .../data/impl/h2/H2ProxyConnection.java | 296 ++++++++++++++++++ .../h2/trigger/H2UpdateHandlerTrigger.java | 35 ++- .../data/impl/redis/RedisCacheEntry.java | 6 - .../data/impl/redis/RedisEncodedValue.java | 5 + .../staticstudios/data/CachedValueTest.java | 20 +- .../PersistentManyToManyCollectionTest.java | 3 - .../PersistentOneToManyCollectionTest.java | 6 - ...ersistentOneToManyValueCollectionTest.java | 3 - .../data/PersistentValueTest.java | 10 - .../net/staticstudios/data/ReferenceTest.java | 5 - .../net/staticstudios/data/SnapshotTest.java | 1 - .../net/staticstudios/data/misc/DataTest.java | 14 +- 15 files changed, 499 insertions(+), 183 deletions(-) delete mode 100644 core/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/h2/H2ProxyConnection.java delete mode 100644 core/src/main/java/net/staticstudios/data/impl/redis/RedisCacheEntry.java create mode 100644 core/src/main/java/net/staticstudios/data/impl/redis/RedisEncodedValue.java diff --git a/core/src/main/java/net/staticstudios/data/DataManager.java b/core/src/main/java/net/staticstudios/data/DataManager.java index 10e312cf..d31c1add 100644 --- a/core/src/main/java/net/staticstudios/data/DataManager.java +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -36,6 +36,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; +@ApiStatus.Internal public class DataManager { private static Boolean useGlobal = null; private static DataManager instance; @@ -127,7 +128,6 @@ private void addRedisUpdateHandler(String partialKey, CachedValueUpdateHandlerWr .add(handler); } - @ApiStatus.Internal public void callPersistentValueUpdateHandlers(List columnNames, String schema, String table, String column, Object[] oldSerializedValues, Object[] newSerializedValues) { logger.trace("Calling update handlers for {}.{}.{} with old values {} and new values {}", schema, table, column, Arrays.toString(oldSerializedValues), Arrays.toString(newSerializedValues)); Map, List>> handlersForColumn = persistentValueUpdateHandlers.get(schema + "." + table + "." + column); @@ -166,7 +166,6 @@ public void callPersistentValueUpdateHandlers(List columnNames, String s } } - @ApiStatus.Internal public void callCachedValueUpdateHandlers(String partialKey, List encodedIdNames, List encodedIdValues, @Nullable String oldValue, @Nullable String newValue) { if (Objects.equals(oldValue, newValue)) { return; @@ -218,8 +217,53 @@ public void callCachedValueUpdateHandlers(String partialKey, List encode } } - @ApiStatus.Internal - public void callCollectionChangeHandlers(List columnNames, String schema, String table, List changedColumns, Object[] oldSerializedValues, Object[] newSerializedValues, TriggerCause cause) { + /** + * Called when an entry is deleted from the database, but a remove handler will want this snapshot of the data later, when update handlers are called. + */ + public @Nullable UniqueData createSnapshotForCollectionRemoveHandlers(List columnNames, String schema, String table, Object[] oldSerializedValues) { + Map, List>> handlersForTable = collectionChangeHandlers.get(schema + "." + table); + if (handlersForTable == null) { + return null; + } + + for (Map.Entry, List>> entry : handlersForTable.entrySet()) { + List> handlers = entry.getValue(); + for (CollectionChangeHandlerWrapper wrapper : handlers) { + PersistentCollectionMetadata collectionMetadata = wrapper.getCollectionMetadata(); + Preconditions.checkNotNull(collectionMetadata, "Collection metadata not set for collection change handler"); + if (wrapper.getType() != CollectionChangeHandlerWrapper.Type.REMOVE) { + continue; + } + + UniqueDataMetadata referencedMetadata; + + if (collectionMetadata instanceof PersistentOneToManyCollectionMetadata oneToManyCollectionMetadata) { + referencedMetadata = getMetadata(oneToManyCollectionMetadata.getReferencedType()); + } else if (collectionMetadata instanceof PersistentManyToManyCollectionMetadata manyToManyCollectionMetadata) { + referencedMetadata = getMetadata(manyToManyCollectionMetadata.getReferencedType()); + + if (!Objects.equals(schema, referencedMetadata.schema()) || !Objects.equals(table, referencedMetadata.table())) { + continue; //don't need a snapshot if it wasn't the referenced object being deleted + } + } else { + continue; + } + + ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + Object serializedValue = oldSerializedValues[columnNames.indexOf(idColumn.name())]; + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + + return createSnapshot(referencedMetadata.clazz(), new ColumnValuePairs(idColumns)); + } + } + + return null; + } + + public void callCollectionChangeHandlers(List columnNames, String schema, String table, List changedColumns, Object[] oldSerializedValues, Object[] newSerializedValues, TriggerCause cause, @Nullable UniqueData snapshot) { logger.trace("Calling collection change handlers for {}.{} on changed columns {} with old values {} and new values {}", schema, table, changedColumns, Arrays.toString(oldSerializedValues), Arrays.toString(newSerializedValues)); Map, List>> handlersForTable = collectionChangeHandlers.get(schema + "." + table); if (handlersForTable == null) { @@ -233,11 +277,11 @@ public void callCollectionChangeHandlers(List columnNames, String schema Preconditions.checkNotNull(collectionMetadata, "Collection metadata not set for collection change handler"); switch (collectionMetadata) { case PersistentOneToManyCollectionMetadata oneToManyCollectionMetadata -> - handleOneToManyCollectionChange(wrapper, oneToManyCollectionMetadata, columnNames, oldSerializedValues, newSerializedValues, cause); + handleOneToManyCollectionChange(wrapper, oneToManyCollectionMetadata, columnNames, oldSerializedValues, newSerializedValues, cause, snapshot); case PersistentOneToManyValueCollectionMetadata oneToManyValueCollectionMetadata -> handleOneToManyValuedCollectionChange(wrapper, oneToManyValueCollectionMetadata, columnNames, oldSerializedValues, newSerializedValues); case PersistentManyToManyCollectionMetadata manyToManyCollectionMetadata -> - handleManyToManyCollectionChange(wrapper, manyToManyCollectionMetadata, columnNames, oldSerializedValues, newSerializedValues, cause); + handleManyToManyCollectionChange(wrapper, manyToManyCollectionMetadata, schema, table, columnNames, oldSerializedValues, newSerializedValues, cause, snapshot); default -> throw new IllegalStateException("Unknown collection metadata type: " + collectionMetadata.getClass().getName()); } @@ -245,7 +289,7 @@ public void callCollectionChangeHandlers(List columnNames, String schema } } - private void handleOneToManyCollectionChange(CollectionChangeHandlerWrapper handler, PersistentOneToManyCollectionMetadata metadata, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues, TriggerCause cause) { + private void handleOneToManyCollectionChange(CollectionChangeHandlerWrapper handler, PersistentOneToManyCollectionMetadata metadata, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues, TriggerCause cause, @Nullable UniqueData snapshot) { List links = metadata.getLinks(); Object[] oldLinkValues = new Object[links.size()]; Object[] newLinkValues = new Object[links.size()]; @@ -266,23 +310,9 @@ private void handleOneToManyCollectionChange(CollectionChangeHandlerWrapper oldValues = new ArrayList<>(); List newValues = new ArrayList<>(); for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { - if (!oldValues.isEmpty()) { - sqlBuilder.append(" AND "); - } - sqlBuilder.append("\"").append(idColumn.name()).append("\" = ? "); int columnIndex = columnNames.indexOf(idColumn.name()); - Object oldDeserializedValue = deserialize(idColumn.type(), oldSerializedValues[columnIndex]); - oldValues.add(oldDeserializedValue); Object newDeserializedValue = deserialize(idColumn.type(), newSerializedValues[columnIndex]); newValues.add(newDeserializedValue); } @@ -291,48 +321,36 @@ private void handleOneToManyCollectionChange(CollectionChangeHandlerWrapper handler.unsafeHandle(instance, oldInstance)); - } + UniqueData oldInstance; + if (cause == TriggerCause.DELETE) { + //the handler probably cares about the data that was removed, so we create a snapshot since the actual data is no longer available + oldInstance = snapshot; + } else { + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + Object serializedValue = newValues.get(columnNames.indexOf(idColumn.name())); + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); } - } catch (SQLException e) { - throw new RuntimeException(e); + + oldInstance = getInstance(referencedMetadata.clazz(), idColumns); + } + if (oldInstance != null) { + submitUpdateHandler(() -> handler.unsafeHandle(instance, oldInstance)); } } if (handler.getType() == CollectionChangeHandlerWrapper.Type.ADD) { - try (ResultSet rs = dataAccessor.executeQuery(sqlBuilder.toString(), newValues)) { - if (rs.next()) { - ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; - for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { - Object serializedValue = rs.getObject(idColumn.name()); - Object deserializedValue = deserialize(idColumn.type(), serializedValue); - idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); - } - UniqueData newInstance = getInstance(referencedMetadata.clazz(), idColumns); - if (newInstance != null) { - submitUpdateHandler(() -> handler.unsafeHandle(instance, newInstance)); - } - } - } catch (SQLException e) { - throw new RuntimeException(e); + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + Object serializedValue = newValues.get(columnNames.indexOf(idColumn.name())); + Object deserializedValue = deserialize(idColumn.type(), serializedValue); + idColumns[referencedMetadata.idColumns().indexOf(idColumn)] = new ColumnValuePair(idColumn.name(), deserializedValue); + } + UniqueData newInstance = getInstance(referencedMetadata.clazz(), idColumns); + if (newInstance != null) { + submitUpdateHandler(() -> handler.unsafeHandle(instance, newInstance)); } } } @@ -377,7 +395,7 @@ private void handleOneToManyValuedCollectionChange(CollectionChangeHandlerWrappe } } - private void handleManyToManyCollectionChange(CollectionChangeHandlerWrapper handler, PersistentManyToManyCollectionMetadata metadata, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues, TriggerCause cause) { + private void handleManyToManyCollectionChange(CollectionChangeHandlerWrapper handler, PersistentManyToManyCollectionMetadata metadata, String schema, String table, List columnNames, Object[] oldSerializedValues, Object[] newSerializedValues, TriggerCause cause, @Nullable UniqueData snapshot) { List links = metadata.getJoinTableToReferencedTableLinks(this); Object[] oldLinkValues = new Object[links.size()]; Object[] newLinkValues = new Object[links.size()]; @@ -438,35 +456,35 @@ private void handleManyToManyCollectionChange(CollectionChangeHandlerWrapper handler.unsafeHandle(instance, oldInstance)); - } + } catch (SQLException e) { + throw new RuntimeException(e); } - } catch (SQLException e) { - throw new RuntimeException(e); + } + if (oldInstance != null) { + UniqueData finalOldInstance = oldInstance; + submitUpdateHandler(() -> handler.unsafeHandle(instance, finalOldInstance)); } } if (handler.getType() == CollectionChangeHandlerWrapper.Type.ADD) { try (ResultSet rs = dataAccessor.executeQuery(sqlBuilder.toString(), newValues)) { if (rs.next()) { - ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { Object serializedValue = rs.getObject(idColumn.name()); Object deserializedValue = deserialize(idColumn.type(), serializedValue); @@ -546,7 +564,6 @@ private UniqueData getInstanceForReferenceUpdateHandler(Class columnNames, String schema, String table, List changedColumns, Object[] oldSerializedValues, Object[] newSerializedValues) { logger.trace("Calling reference update handlers for {}.{} on changed columns {} with old values {} and new values {}", schema, table, changedColumns, Arrays.toString(oldSerializedValues), Arrays.toString(newSerializedValues)); Map, List>> handlersForTable = referenceUpdateHandlers.get(schema + "." + table); @@ -656,7 +673,6 @@ private void submitUpdateHandler(Runnable runnable) { updateHandlerExecutor.accept(runnable); } - @ApiStatus.Internal public void registerPersistentValueUpdateHandlers(PersistentValueMetadata metadata, Collection> handlers) { if (registeredUpdateHandlersForColumns.add(metadata)) { for (ValueUpdateHandlerWrapper handler : handlers) { @@ -665,7 +681,6 @@ public void registerPersistentValueUpdateHandlers(PersistentValueMetadata metada } } - @ApiStatus.Internal public void registerCachedValueUpdateHandlers(CachedValueMetadata metadata, Collection> handlers) { if (registeredUpdateHandlersForRedis.add(metadata)) { UniqueDataMetadata holderMetadata = getMetadata(metadata.holderClass()); @@ -676,7 +691,6 @@ public void registerCachedValueUpdateHandlers(CachedValueMetadata metadata, Coll } } - @ApiStatus.Internal public void registerCollectionChangeHandlers(PersistentCollectionMetadata metadata, Collection> handlers) { if (registeredChangeHandlersForCollection.add(metadata)) { handlers.forEach(h -> h.setCollectionMetadata(metadata)); @@ -698,7 +712,6 @@ public void registerCollectionChangeHandlers(PersistentCollectionMetadata metada } } - @ApiStatus.Internal public void registerReferenceUpdateHandlers(ReferenceMetadata metadata, Collection> handlers) { if (registeredUpdateHandlersForReference.add(metadata)) { handlers.forEach(h -> h.setReferenceMetadata(metadata)); @@ -844,7 +857,6 @@ public UniqueDataMetadata getMetadata(Class clazz) { return metadata; } - @ApiStatus.Internal public void handleDelete(List columnNames, String schema, String table, Object[] values) { uniqueDataMetadataMap.values().forEach(uniqueDataMetadata -> { if (!uniqueDataMetadata.schema().equals(schema) || !uniqueDataMetadata.table().equals(table)) { @@ -872,7 +884,6 @@ public void handleDelete(List columnNames, String schema, String table, }); } - @ApiStatus.Internal public synchronized void updateIdColumns(List columnNames, String schema, String table, String column, Object[] oldValues, Object[] newValues) { uniqueDataMetadataMap.values().forEach(uniqueDataMetadata -> { if (!uniqueDataMetadata.schema().equals(schema) || !uniqueDataMetadata.table().equals(table)) { @@ -1229,7 +1240,6 @@ public T get(String schema, String table, String column, ColumnValuePairs id } } - @ApiStatus.Internal public void set(String schema, String table, String column, ColumnValuePairs idColumns, List idColumnLinks, Object value, int delay) { StringBuilder sqlBuilder; if (idColumnLinks.isEmpty()) { diff --git a/core/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java b/core/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java deleted file mode 100644 index 694b1402..00000000 --- a/core/src/main/java/net/staticstudios/data/impl/h2/DelayedDatabaseTask.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.staticstudios.data.impl.h2; - -public record DelayedDatabaseTask(String sql, Object[] params) { -} 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 6f424e26..146d5c01 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 @@ -1,13 +1,14 @@ package net.staticstudios.data.impl.h2; import com.google.common.base.Preconditions; +import com.google.gson.Gson; import com.impossibl.postgres.api.jdbc.PGConnection; import net.staticstudios.data.DataManager; import net.staticstudios.data.InsertMode; import net.staticstudios.data.impl.DataAccessor; import net.staticstudios.data.impl.h2.trigger.H2UpdateHandlerTrigger; import net.staticstudios.data.impl.pg.PostgresListener; -import net.staticstudios.data.impl.redis.RedisCacheEntry; +import net.staticstudios.data.impl.redis.RedisEncodedValue; import net.staticstudios.data.impl.redis.RedisEvent; import net.staticstudios.data.impl.redis.RedisListener; import net.staticstudios.data.parse.DDLStatement; @@ -32,7 +33,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.sql.*; -import java.time.Instant; import java.util.*; import java.util.concurrent.*; import java.util.function.Consumer; @@ -46,6 +46,7 @@ public class H2DataAccessor implements DataAccessor { private static final String SET_REFERENTIAL_INTEGRITY_FALSE = "SET REFERENTIAL_INTEGRITY FALSE"; @Language("SQL") private static final String SET_REFERENTIAL_INTEGRITY_TRUE = "SET REFERENTIAL_INTEGRITY TRUE"; + private static final Gson GSON = new Gson(); private final TaskQueue taskQueue; private final String jdbcUrl; private final ThreadLocal threadConnection = new ThreadLocal<>(); @@ -60,11 +61,13 @@ 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(); - public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener, RedisListener redisListener, TaskQueue taskQueue) { + private final ThreadLocal> commitCallbacks = ThreadLocal.withInitial(LinkedList::new); + private final ThreadLocal> rollbackCallbacks = ThreadLocal.withInitial(LinkedList::new); + public H2DataAccessor(DataManager dataManager, PostgresListener postgresListener, RedisListener redisListener, TaskQueue taskQueue) { try { Class.forName("org.h2.Driver"); } catch (ClassNotFoundException e) { @@ -305,7 +308,7 @@ public synchronized void sync(List schemaTables, List redis cursor = scanResult.getCursor(); for (String key : scanResult.getResult()) { - redisCache.put(key, new RedisCacheEntry(jedis.get(key), Instant.now())); + redisCache.put(key, decodeRedis(jedis.get(key)).value()); } } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); @@ -325,7 +328,7 @@ private Connection getConnection() throws SQLException { if (connection == null) { connection = DriverManager.getConnection(jdbcUrl); connection.setAutoCommit(false); - threadConnection.set(connection); + threadConnection.set(connection = new H2ProxyConnection(connection, this)); ThreadUtils.onShutdownRunSync(ShutdownStage.CLEANUP, () -> { Connection _connection = threadConnection.get(); @@ -498,39 +501,30 @@ public void postDDL() throws SQLException { @Override public @Nullable String getRedisValue(String key) { - RedisCacheEntry cacheEntry = redisCache.get(key); - if (cacheEntry != null) { - return cacheEntry.value(); - } - return null; + return redisCache.get(key); } @Override public void setRedisValue(String key, String value, int expirationSeconds) { - RedisCacheEntry prev; + String prev; if (value == null) { prev = redisCache.remove(key); taskQueue.submitTask((connection, jedis) -> { jedis.del(key); }); } else { - prev = redisCache.put(key, new RedisCacheEntry(value, Instant.now())); + prev = redisCache.put(key, value); taskQueue.submitTask((connection, jedis) -> { if (expirationSeconds > 0) { - jedis.setex(key, expirationSeconds, value); + jedis.setex(key, expirationSeconds, encodeRedis(value)); } else { - jedis.set(key, value); + jedis.set(key, encodeRedis(value)); } }); } - String prevValue = null; - - if (prev != null) { - prevValue = prev.value(); - } RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); - dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), prevValue, value); + dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), prev, value); } @Override @@ -661,16 +655,68 @@ private void runDatabaseTask(SQLTransaction transaction, int delay) { } private void handleRedisEvent(RedisEvent event, String key, @Nullable String value) { + RedisEncodedValue redisEncoded = value == null ? null : decodeRedis(value); + + if (redisEncoded != null && Objects.equals(redisEncoded.staticDataAppName(), dataManager.getApplicationName())) { + return; // ignore events from ourselves + } + if (event == RedisEvent.SET) { - RedisCacheEntry entry = redisCache.get(key); - if (entry != null && (entry.entryInstant().isAfter(Instant.now()) || Objects.equals(entry.value(), value))) { + String entry = redisCache.get(key); + if (entry != null && Objects.equals(entry, value)) { return; } - redisCache.put(key, new RedisCacheEntry(value, Instant.now())); + redisCache.put(key, value); RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); - dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), entry == null ? null : entry.value(), value); + dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), entry, value); } else if (event == RedisEvent.DEL || event == RedisEvent.EXPIRED) { redisCache.remove(key); } } + + public void onCommit(Runnable callback) { + commitCallbacks.get().add(callback); + } + + public void onRollback(Runnable callback) { + rollbackCallbacks.get().add(callback); + } + + protected void handleCommit() { + List callbacks = commitCallbacks.get(); + commitCallbacks.set(new LinkedList<>()); + rollbackCallbacks.get().clear(); + if (callbacks != null) { + for (Runnable callback : callbacks) { + try { + callback.run(); + } catch (Exception e) { + logger.error("Error executing commit callback", e); + } + } + } + } + + protected void handleRollback() { + List callbacks = rollbackCallbacks.get(); + rollbackCallbacks.set(new LinkedList<>()); + commitCallbacks.get().clear(); + if (callbacks != null) { + for (Runnable callback : callbacks) { + try { + callback.run(); + } catch (Exception e) { + logger.error("Error executing rollback callback", e); + } + } + } + } + + private String encodeRedis(Object value) { + return GSON.toJson(new RedisEncodedValue(dataManager.getApplicationName(), value.toString())); + } + + private RedisEncodedValue decodeRedis(String encoded) { + return GSON.fromJson(encoded, RedisEncodedValue.class); + } } diff --git a/core/src/main/java/net/staticstudios/data/impl/h2/H2ProxyConnection.java b/core/src/main/java/net/staticstudios/data/impl/h2/H2ProxyConnection.java new file mode 100644 index 00000000..dff2cd62 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/h2/H2ProxyConnection.java @@ -0,0 +1,296 @@ +package net.staticstudios.data.impl.h2; + +import java.sql.*; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +public class H2ProxyConnection implements Connection { + private final Connection delegate; + private final H2DataAccessor dataAccessor; + + public H2ProxyConnection(Connection delegate, H2DataAccessor dataAccessor) { + this.delegate = delegate; + this.dataAccessor = dataAccessor; + } + + @Override + public Statement createStatement() throws SQLException { + return delegate.createStatement(); + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return delegate.prepareStatement(sql); + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + return delegate.prepareCall(sql); + } + + @Override + public String nativeSQL(String sql) throws SQLException { + return delegate.nativeSQL(sql); + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + boolean previousAutoCommit = delegate.getAutoCommit(); + delegate.setAutoCommit(autoCommit); + if (autoCommit && !previousAutoCommit) { + dataAccessor.handleCommit(); + } + } + + @Override + public boolean getAutoCommit() throws SQLException { + return delegate.getAutoCommit(); + } + + @Override + public void commit() throws SQLException { + delegate.commit(); + dataAccessor.handleCommit(); + } + + @Override + public void rollback() throws SQLException { + delegate.rollback(); + dataAccessor.handleRollback(); + } + + @Override + public void close() throws SQLException { + delegate.close(); + } + + @Override + public boolean isClosed() throws SQLException { + return delegate.isClosed(); + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + return delegate.getMetaData(); + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + delegate.setReadOnly(readOnly); + } + + @Override + public boolean isReadOnly() throws SQLException { + return delegate.isReadOnly(); + } + + @Override + public void setCatalog(String catalog) throws SQLException { + delegate.setCatalog(catalog); + } + + @Override + public String getCatalog() throws SQLException { + return delegate.getCatalog(); + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + delegate.setTransactionIsolation(level); + } + + @Override + public int getTransactionIsolation() throws SQLException { + return delegate.getTransactionIsolation(); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return delegate.getWarnings(); + } + + @Override + public void clearWarnings() throws SQLException { + delegate.clearWarnings(); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return delegate.createStatement(resultSetType, resultSetConcurrency); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return delegate.prepareStatement(sql, resultSetType, resultSetConcurrency); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return delegate.prepareCall(sql, resultSetType, resultSetConcurrency); + } + + @Override + public Map> getTypeMap() throws SQLException { + return delegate.getTypeMap(); + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + delegate.setTypeMap(map); + } + + @Override + public void setHoldability(int holdability) throws SQLException { + delegate.setHoldability(holdability); + } + + @Override + public int getHoldability() throws SQLException { + return delegate.getHoldability(); + } + + @Override + public Savepoint setSavepoint() throws SQLException { + return delegate.setSavepoint(); + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + return delegate.setSavepoint(name); + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + delegate.rollback(savepoint); + } + + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + delegate.releaseSavepoint(savepoint); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + return delegate.prepareStatement(sql, autoGeneratedKeys); + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + return delegate.prepareStatement(sql, columnIndexes); + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + return delegate.prepareStatement(sql, columnNames); + } + + @Override + public Clob createClob() throws SQLException { + return delegate.createClob(); + } + + @Override + public Blob createBlob() throws SQLException { + return delegate.createBlob(); + } + + @Override + public NClob createNClob() throws SQLException { + return delegate.createNClob(); + } + + @Override + public SQLXML createSQLXML() throws SQLException { + return delegate.createSQLXML(); + } + + @Override + public boolean isValid(int timeout) throws SQLException { + return delegate.isValid(timeout); + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + delegate.setClientInfo(name, value); + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + delegate.setClientInfo(properties); + } + + @Override + public String getClientInfo(String name) throws SQLException { + return delegate.getClientInfo(name); + } + + @Override + public Properties getClientInfo() throws SQLException { + return delegate.getClientInfo(); + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + return delegate.createArrayOf(typeName, elements); + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + return delegate.createStruct(typeName, attributes); + } + + @Override + public void setSchema(String schema) throws SQLException { + delegate.setSchema(schema); + } + + @Override + public String getSchema() throws SQLException { + return delegate.getSchema(); + } + + @Override + public void abort(Executor executor) throws SQLException { + delegate.abort(executor); + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + delegate.setNetworkTimeout(executor, milliseconds); + } + + @Override + public int getNetworkTimeout() throws SQLException { + return delegate.getNetworkTimeout(); + } + + @Override + public T unwrap(Class iface) throws SQLException { + return delegate.unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return delegate.isWrapperFor(iface); + } + + public Connection getDelegate() { + return delegate; + } +} 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 1b550013..63a6a186 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 @@ -1,8 +1,11 @@ package net.staticstudios.data.impl.h2.trigger; import net.staticstudios.data.DataManager; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.impl.h2.H2DataAccessor; import net.staticstudios.data.util.TriggerCause; import org.h2.api.Trigger; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,6 +21,7 @@ public class H2UpdateHandlerTrigger implements Trigger { private final Logger logger = LoggerFactory.getLogger(H2UpdateHandlerTrigger.class); private final List columnNames = new ArrayList<>(); private DataManager dataManager; + private H2DataAccessor dataAccessor; private String schema; private String table; @@ -30,6 +34,7 @@ public void init(Connection conn, String schemaName, String triggerName, String 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; } @@ -69,7 +74,7 @@ public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws } private void handleInsert(Object[] newRow) { - dataManager.callCollectionChangeHandlers(columnNames, schema, table, columnNames, new Object[newRow.length], newRow, TriggerCause.INSERT); + dataAccessor.onCommit(() -> dataManager.callCollectionChangeHandlers(columnNames, schema, table, columnNames, new Object[newRow.length], newRow, TriggerCause.INSERT, null)); } private void handleUpdate(Object[] oldRow, Object[] newRow) { @@ -82,21 +87,29 @@ private void handleUpdate(Object[] oldRow, Object[] newRow) { } } - for (String changedColumn : changedColumns) { - dataManager.updateIdColumns(columnNames, schema, table, changedColumn, oldRow, newRow); - } + dataAccessor.onCommit(() -> { + for (String changedColumn : changedColumns) { + dataManager.updateIdColumns(columnNames, schema, table, changedColumn, oldRow, newRow); + } - for (String changedColumn : changedColumns) { - dataManager.callPersistentValueUpdateHandlers(columnNames, schema, table, changedColumn, oldRow, newRow); - } + for (String changedColumn : changedColumns) { + dataManager.callPersistentValueUpdateHandlers(columnNames, schema, table, changedColumn, oldRow, newRow); + } - dataManager.callCollectionChangeHandlers(columnNames, schema, table, changedColumns, oldRow, newRow, TriggerCause.UPDATE); - dataManager.callReferenceUpdateHandlers(columnNames, schema, table, changedColumns, oldRow, newRow); + dataManager.callCollectionChangeHandlers(columnNames, schema, table, changedColumns, oldRow, newRow, TriggerCause.UPDATE, null); + dataManager.callReferenceUpdateHandlers(columnNames, schema, table, changedColumns, oldRow, newRow); + }); } private void handleDelete(Object[] oldRow) { - dataManager.callCollectionChangeHandlers(columnNames, schema, table, columnNames, oldRow, new Object[oldRow.length], TriggerCause.DELETE); - dataManager.handleDelete(columnNames, schema, table, oldRow); + // we might want a snapshot later. before the data is actually gone, so create it now, if it'll be used later. + @Nullable UniqueData snapshot = dataManager.createSnapshotForCollectionRemoveHandlers(columnNames, schema, table, oldRow); + + dataAccessor.onCommit(() -> { + dataManager.callCollectionChangeHandlers(columnNames, schema, table, columnNames, oldRow, new Object[oldRow.length], TriggerCause.DELETE, snapshot); + + dataManager.handleDelete(columnNames, schema, table, oldRow); + }); } } diff --git a/core/src/main/java/net/staticstudios/data/impl/redis/RedisCacheEntry.java b/core/src/main/java/net/staticstudios/data/impl/redis/RedisCacheEntry.java deleted file mode 100644 index 0fb082d7..00000000 --- a/core/src/main/java/net/staticstudios/data/impl/redis/RedisCacheEntry.java +++ /dev/null @@ -1,6 +0,0 @@ -package net.staticstudios.data.impl.redis; - -import java.time.Instant; - -public record RedisCacheEntry(String value, Instant entryInstant) { -} diff --git a/core/src/main/java/net/staticstudios/data/impl/redis/RedisEncodedValue.java b/core/src/main/java/net/staticstudios/data/impl/redis/RedisEncodedValue.java new file mode 100644 index 00000000..c2e802e2 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/redis/RedisEncodedValue.java @@ -0,0 +1,5 @@ +package net.staticstudios.data.impl.redis; + +public record RedisEncodedValue(String staticDataAppName, String value) { + +} \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/CachedValueTest.java b/core/src/test/java/net/staticstudios/data/CachedValueTest.java index 20dc455b..98cd16f6 100644 --- a/core/src/test/java/net/staticstudios/data/CachedValueTest.java +++ b/core/src/test/java/net/staticstudios/data/CachedValueTest.java @@ -1,5 +1,7 @@ package net.staticstudios.data; +import com.google.gson.Gson; +import net.staticstudios.data.impl.redis.RedisEncodedValue; import net.staticstudios.data.misc.DataTest; import net.staticstudios.data.mock.user.MockUser; import net.staticstudios.data.util.ColumnValuePair; @@ -15,6 +17,8 @@ public class CachedValueTest extends DataTest { + private Gson gson = new Gson(); + @Test public void testBasic() { DataManager dataManager = getMockEnvironments().getFirst().dataManager(); @@ -69,21 +73,18 @@ public void testUpdateHandler() { .name("john doe") .insert(InsertMode.ASYNC); - waitForUpdateHandlers(); assertEquals(0, user.cooldownUpdates.get()); user.onCooldown.set(false); assertEquals(false, user.onCooldown.get()); //the fallback value is false, so we didn't change anything. update handlers shouldn't be fired - waitForUpdateHandlers(); assertEquals(0, user.cooldownUpdates.get()); user.onCooldown.set(true); assertEquals(true, user.onCooldown.get()); - waitForUpdateHandlers(); assertEquals(1, user.cooldownUpdates.get()); @@ -91,14 +92,12 @@ public void testUpdateHandler() { assertEquals(true, user.onCooldown.get()); //didnt change, so no update - waitForUpdateHandlers(); assertEquals(1, user.cooldownUpdates.get()); user.onCooldown.set(false); assertEquals(false, user.onCooldown.get()); - waitForUpdateHandlers(); assertEquals(2, user.cooldownUpdates.get()); } @@ -118,21 +117,18 @@ public void testUpdateRedis() { String cooldownUpdatesKey = RedisUtils.buildRedisKey("public", "users", "cooldown_updates", user.getIdColumns()); user.onCooldown.set(true); - waitForUpdateHandlers(); user.cooldownUpdates.set(1); waitForDataPropagation(); - assertEquals("true", jedis.get(onCooldownKey)); - assertEquals("1", jedis.get(cooldownUpdatesKey)); + assertEquals("true", gson.fromJson(jedis.get(onCooldownKey), RedisEncodedValue.class).value()); + assertEquals("1", gson.fromJson(jedis.get(cooldownUpdatesKey), RedisEncodedValue.class).value()); user.onCooldown.set(null); - waitForUpdateHandlers(); user.cooldownUpdates.set(null); waitForDataPropagation(); assertNull(jedis.get(onCooldownKey)); assertNull(jedis.get(cooldownUpdatesKey)); user.onCooldown.set(false); //fallback - waitForUpdateHandlers(); user.cooldownUpdates.set(0); //fallback waitForDataPropagation(); assertNull(jedis.get(onCooldownKey)); @@ -149,8 +145,8 @@ public void testLoadCachedValues() { String onCooldownKey = RedisUtils.buildRedisKey("public", "users", "on_cooldown", columnValuePairs); String cooldownUpdatesKey = RedisUtils.buildRedisKey("public", "users", "cooldown_updates", columnValuePairs); - jedis.set(onCooldownKey, "true"); - jedis.set(cooldownUpdatesKey, "5"); + 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); diff --git a/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java index 63a2dfa6..2a5c634d 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java @@ -377,7 +377,6 @@ public void testAddHandlerInsert() { int i = 0; for (MockUser friend : friends) { user.friends.add(friend); - waitForUpdateHandlers(); assertEquals(++i, user.friendAdditions.get()); } @@ -392,7 +391,6 @@ public void testRemoveHandlerDelete() { List friends = createFriends(5); user.friends.addAll(friends); - waitForUpdateHandlers(); assertEquals(5, user.friends.size()); assertEquals(0, user.friendRemovals.get()); @@ -400,7 +398,6 @@ public void testRemoveHandlerDelete() { int i = 0; for (MockUser friend : friends) { user.friends.remove(friend); - waitForUpdateHandlers(); assertEquals(++i, user.friendRemovals.get()); } diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java index eb9630e1..416f6d69 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -317,7 +317,6 @@ public void testAddHandlerUpdate() { int i = 0; for (MockUserSession session : sessions) { user.sessions.add(session); - waitForUpdateHandlers(); assertEquals(++i, user.sessionAdditions.get()); } @@ -332,7 +331,6 @@ public void testRemoveHandlerUpdate() { List sessions = createSessions(5); user.sessions.addAll(sessions); - waitForUpdateHandlers(); assertEquals(5, user.sessions.size()); assertEquals(0, user.sessionRemovals.get()); @@ -340,7 +338,6 @@ public void testRemoveHandlerUpdate() { int i = 0; for (MockUserSession session : sessions) { user.sessions.remove(session); - waitForUpdateHandlers(); assertEquals(++i, user.sessionRemovals.get()); } @@ -360,7 +357,6 @@ public void testAddHandlerInsert() { .userId(user.id.get()) .timestamp(Timestamp.from(Instant.now())) .insert(InsertMode.ASYNC); - waitForUpdateHandlers(); assertEquals(i + 1, user.sessionAdditions.get()); } @@ -375,7 +371,6 @@ public void testRemoveHandlerDelete() { List sessions = createSessions(5); user.sessions.addAll(sessions); - waitForUpdateHandlers(); assertEquals(5, user.sessions.size()); assertEquals(0, user.sessionRemovals.get()); @@ -383,7 +378,6 @@ public void testRemoveHandlerDelete() { int i = 0; for (MockUserSession session : sessions) { session.delete(); - waitForUpdateHandlers(); assertEquals(++i, user.sessionRemovals.get()); } diff --git a/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java index 771c4ec3..b61f8338 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java @@ -392,7 +392,6 @@ public void testAddHandlerInsert() { int i = 0; for (Integer number : numbers) { user.favoriteNumbers.add(number); - waitForUpdateHandlers(); assertEquals(++i, user.favoriteNumberAdditions.get()); } } @@ -407,7 +406,6 @@ public void testRemoveHandlerDelete() { List numbers = createNumbers(5); user.favoriteNumbers.addAll(numbers); waitForDataPropagation(); - waitForUpdateHandlers(); assertEquals(5, user.favoriteNumbers.size()); assertEquals(0, user.favoriteNumberRemovals.get()); @@ -415,7 +413,6 @@ public void testRemoveHandlerDelete() { int i = 0; for (Integer number : numbers) { user.favoriteNumbers.remove(number); - waitForUpdateHandlers(); assertEquals(++i, user.favoriteNumberRemovals.get()); } diff --git a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java index a97b3138..fd34ef90 100644 --- a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java +++ b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -204,20 +204,15 @@ public void testUpdateHandlerRegistration() { mockUser = dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id)); // should have a cache miss //the handler for this pv should not have been registered again assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); - waitForUpdateHandlers(); assertEquals(0, mockUser.getNameUpdates()); mockUser.name.set("new name"); - waitForUpdateHandlers(); assertEquals(1, mockUser.getNameUpdates()); mockUser.name.set("new name"); - waitForUpdateHandlers(); assertEquals(1, mockUser.getNameUpdates()); mockUser.name.set("new name2"); - waitForUpdateHandlers(); assertEquals(2, mockUser.getNameUpdates()); mockUser.name.set("new name"); - waitForUpdateHandlers(); assertEquals(3, mockUser.getNameUpdates()); } @@ -331,7 +326,6 @@ public void testChangeIdColumn() { assertEquals(id, mockUser.id.get()); assertEquals(0, mockUser.nameUpdates.get()); mockUser.name.set("new name"); - waitForUpdateHandlers(); assertEquals(1, mockUser.nameUpdates.get()); UUID newId = UUID.randomUUID(); mockUser.id.set(newId); @@ -341,7 +335,6 @@ public void testChangeIdColumn() { assertSame(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", newId)), mockUser); assertEquals(1, mockUser.nameUpdates.get()); mockUser.name.set("new name2"); - waitForUpdateHandlers(); assertEquals(2, mockUser.nameUpdates.get()); } @@ -364,7 +357,6 @@ public void testChangeIdColumnInPostgres() { assertEquals(0, mockUser.nameUpdates.get()); mockUser.name.set("new name"); - waitForUpdateHandlers(); assertEquals(1, mockUser.nameUpdates.get()); UUID newId = UUID.randomUUID(); @@ -383,10 +375,8 @@ public void testChangeIdColumnInPostgres() { assertNull(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", id))); assertNotNull(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", newId))); assertSame(dataManager.getInstance(MockUser.class, ColumnValuePair.of("id", newId)), mockUser); - waitForUpdateHandlers(); assertEquals(1, mockUser.nameUpdates.get()); mockUser.name.set("new name2"); - waitForUpdateHandlers(); assertEquals(2, mockUser.nameUpdates.get()); } diff --git a/core/src/test/java/net/staticstudios/data/ReferenceTest.java b/core/src/test/java/net/staticstudios/data/ReferenceTest.java index 45036a9c..b21c5c12 100644 --- a/core/src/test/java/net/staticstudios/data/ReferenceTest.java +++ b/core/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -190,22 +190,18 @@ public void testUpdateHandlerUpdate() { assertEquals(0, user.settingsUpdates.get()); user.settings.set(settings); - waitForUpdateHandlers(); assertEquals(1, user.settingsUpdates.get()); user.settings.set(settings); - waitForUpdateHandlers(); assertEquals(1, user.settingsUpdates.get()); user.settings.set(null); - waitForUpdateHandlers(); assertEquals(2, user.settingsUpdates.get()); user.settings.set(settings); - waitForUpdateHandlers(); assertEquals(3, user.settingsUpdates.get()); @@ -214,7 +210,6 @@ public void testUpdateHandlerUpdate() { .insert(InsertMode.SYNC); user.settings.set(settings2); - waitForUpdateHandlers(); assertEquals(4, user.settingsUpdates.get()); } diff --git a/core/src/test/java/net/staticstudios/data/SnapshotTest.java b/core/src/test/java/net/staticstudios/data/SnapshotTest.java index dfb4b7f5..d8f9b058 100644 --- a/core/src/test/java/net/staticstudios/data/SnapshotTest.java +++ b/core/src/test/java/net/staticstudios/data/SnapshotTest.java @@ -35,7 +35,6 @@ public void testPersistentValues() { assertEquals(0, snapshot.nameUpdates.get()); user.name.set("new name"); - waitForUpdateHandlers(); assertEquals("new name", user.name.get()); assertEquals("some name", snapshot.name.get()); diff --git a/core/src/test/java/net/staticstudios/data/misc/DataTest.java b/core/src/test/java/net/staticstudios/data/misc/DataTest.java index f36e756d..7d654c9e 100644 --- a/core/src/test/java/net/staticstudios/data/misc/DataTest.java +++ b/core/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -52,7 +52,7 @@ static void initPostgres() throws IOException, SQLException, InterruptedExceptio .postgresPassword(postgres.getPassword()) .redisHost(redis.getHost()) .redisPort(redis.getFirstMappedPort()) - .updateHandlerExecutor(ThreadUtils::submit) + .updateHandlerExecutor(Runnable::run) .build(); connection = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); @@ -136,18 +136,6 @@ public int getWaitForDataPropagationTime() { return 500 + (Objects.equals(System.getenv("GITHUB_ACTIONS"), "true") ? 1000 : 0); } - public int getWaitForUpdateHandlersTime() { - return 100 + (Objects.equals(System.getenv("GITHUB_ACTIONS"), "true") ? 500 : 0); - } - - public void waitForUpdateHandlers() { - try { - Thread.sleep(getWaitForUpdateHandlersTime()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - public void waitForDataPropagation() { try { Thread.sleep(getWaitForDataPropagationTime()); From 94003ad33d85e06eef9628898d7fca6a90ac5df2 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Wed, 31 Dec 2025 07:18:51 -0500 Subject: [PATCH 75/75] update build.gradle --- build.gradle | 2 +- core/build.gradle | 12 +++++++----- processor/build.gradle | 13 ++++++------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index 1eedf040..edd35ddb 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { allprojects { group = 'net.staticstudios' - version = '3.0.0-alpha.0-SNAPSHOT' + version = '3.0.0-SNAPSHOT' repositories { mavenCentral() diff --git a/core/build.gradle b/core/build.gradle index 1235e878..fcc14ceb 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -36,14 +36,14 @@ tasks.named('build') { dependsOn(shadowJar) } -tasks.named("publish") { - dependsOn(build) -} - test { useJUnitPlatform() } +tasks.withType(GenerateModuleMetadata).configureEach { + enabled = false +} + java { withSourcesJar() withJavadocJar() @@ -58,7 +58,9 @@ publishing { maven(MavenPublication) { artifactId = 'static-data' - from components.shadow + artifact(tasks.shadowJar) { + classifier = null + } artifact sourcesJar artifact javadocJar diff --git a/processor/build.gradle b/processor/build.gradle index 0e799d9e..fbd9042a 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -32,15 +32,14 @@ build { dependsOn(shadowJar) } -tasks.named("publish") { - dependsOn(build) +tasks.withType(GenerateModuleMetadata).configureEach { + enabled = false } - -//java { -// withSourcesJar() -// withJavadocJar() -//} +java { + withSourcesJar() + withJavadocJar() +} publishing {