diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e3a8dfe..781aa989 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,5 @@ name: Test build + on: pull_request: branches: [ "master" ] @@ -6,22 +7,45 @@ 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: 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 (skip tests) + 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 + + - 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/ + + - name: Fail if tests failed + if: steps.test.outcome != 'success' + run: exit 1 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 diff --git a/README.md b/README.md index 45a49f84..5ec5ece9 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,440 @@ -# 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 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. -### 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 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: -## 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 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. -## 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 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 + +`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. 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. + +## Annotations and Data Wrappers + +`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. 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. +- `@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 + 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 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). 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. **Builder**: Provides a builder pattern for creating and inserting instances + ``` + // Example: Creating and inserting a new user + User user = User.builder() + .id(UUID.randomUUID()) + .name("John Doe") + .age(30) + .insert(InsertMode.ASYNC); + ``` + +2. **Query Builder**: Provides a fluent API for querying instances + ``` + // Example: Finding users by criteria + List users = User.query() + .where(w -> w + .nameIsLike("John%") + .and() + .ageIsGreaterThan(25) + ) + .orderByName(Order.ASCENDING) + .limit(10) + .findAll(); + ``` + +Note: Currently only support for Intellij IDEA is available for IDE integration. You should install the appropriate +plugin for your IDE. + +## 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 + +[//]: # (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);) + +[//]: # ( }) + +[//]: # (}) + +[//]: # () + +[//]: # (```) + +## 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, 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. + +## 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) + +[//]: # (TODO: talk about update handlers, & add/remove handlers) \ No newline at end of file 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/annotations/src/main/java/net/staticstudios/data/Column.java b/annotations/src/main/java/net/staticstudios/data/Column.java new file mode 100644 index 00000000..6045f880 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/Column.java @@ -0,0 +1,18 @@ +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(); + + boolean index() default false; + + boolean nullable() default false; + + boolean unique() default false; +} diff --git a/annotations/src/main/java/net/staticstudios/data/Data.java b/annotations/src/main/java/net/staticstudios/data/Data.java new file mode 100644 index 00000000..834cb1e2 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/Data.java @@ -0,0 +1,14 @@ +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(); + + String table(); +} 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 new file mode 100644 index 00000000..365e82e2 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/DeleteStrategy.java @@ -0,0 +1,18 @@ +package net.staticstudios.data; + +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. + *

+ * 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/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/ForeignColumn.java b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.java new file mode 100644 index 00000000..0efb1acc --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/ForeignColumn.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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ForeignColumn { + String name(); + + String table() default ""; + + String schema() default ""; + + boolean nullable() default false; + + boolean index() default false; + + String link(); +} diff --git a/annotations/src/main/java/net/staticstudios/data/IdColumn.java b/annotations/src/main/java/net/staticstudios/data/IdColumn.java new file mode 100644 index 00000000..5c0db8ae --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/IdColumn.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 IdColumn { + String name(); +} 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/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/InsertMode.java b/annotations/src/main/java/net/staticstudios/data/InsertMode.java new file mode 100644 index 00000000..b22ba267 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/InsertMode.java @@ -0,0 +1,16 @@ +package net.staticstudios.data; + +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/util/InsertionStrategy.java b/annotations/src/main/java/net/staticstudios/data/InsertStrategy.java similarity index 73% rename from src/main/java/net/staticstudios/data/util/InsertionStrategy.java rename to annotations/src/main/java/net/staticstudios/data/InsertStrategy.java index 844c88e6..82d52874 100644 --- a/src/main/java/net/staticstudios/data/util/InsertionStrategy.java +++ b/annotations/src/main/java/net/staticstudios/data/InsertStrategy.java @@ -1,6 +1,6 @@ -package net.staticstudios.data.util; +package net.staticstudios.data; -public enum InsertionStrategy { +public enum InsertStrategy { /** * Overwrite existing data with new data. */ 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..436bb961 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/OneToMany.java @@ -0,0 +1,65 @@ +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(); + + /** + * 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/annotations/src/main/java/net/staticstudios/data/OneToOne.java b/annotations/src/main/java/net/staticstudios/data/OneToOne.java new file mode 100644 index 00000000..0f7e58c5 --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/OneToOne.java @@ -0,0 +1,29 @@ +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? + * Format "localColumn=foreignColumn" + * + * @return The link format + */ + String link(); + + //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/annotations/src/main/java/net/staticstudios/data/Order.java b/annotations/src/main/java/net/staticstudios/data/Order.java new file mode 100644 index 00000000..3fc1067d --- /dev/null +++ b/annotations/src/main/java/net/staticstudios/data/Order.java @@ -0,0 +1,6 @@ +package net.staticstudios.data; + +public enum Order { + ASCENDING, + DESCENDING +} 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..3908c178 --- /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 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/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/benchmark/build.gradle b/benchmark/build.gradle new file mode 100644 index 00000000..ca884342 --- /dev/null +++ b/benchmark/build.gradle @@ -0,0 +1,47 @@ +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(project(":utils")) + compileOnly project(':processor') + jmhAnnotationProcessor project(':processor') + + 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") +} + +tasks.named('jmhRunBytecodeGenerator') { + outputs.upToDateWhen { false } +} + +tasks.named('jmh') { + jvmArgs = [ + '-Xms1g', + '-Xmx1g', + '-XX:+AlwaysPreTouch', + '-Djmh.ignoreLock=true' + ] +} + +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 new file mode 100644 index 00000000..cc4cfacc --- /dev/null +++ b/benchmark/src/jmh/java/net/staticstudios/data/benchmark/StaticDataBenchmark.java @@ -0,0 +1,44 @@ +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) +@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) { + for (int i = 0; i < 100; i++) { + SkyblockPlayer player = SkyblockPlayer.builder() + .id(UUID.randomUUID()) + .name("Player" + i) + .insert(InsertMode.ASYNC); //todo: this seems broken, the bench takes oddly long. + } + } + +// @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/benchmark/src/jmh/resources/log4j.properties b/benchmark/src/jmh/resources/log4j.properties new file mode 100644 index 00000000..d54b8ca4 --- /dev/null +++ b/benchmark/src/jmh/resources/log4j.properties @@ -0,0 +1,6 @@ +log4j.rootLogger=INFO, STDOUT +log4j.logger.net.staticstudios=INFO +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/build.gradle b/build.gradle index 628027e6..edd35ddb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,98 +1,59 @@ plugins { - id 'java-library' - id 'maven-publish' - id 'com.gradleup.shadow' version '8.3.3' + id 'java' } -group = 'net.staticstudios' -version = '2.0.10-SNAPSHOT' +allprojects { + 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' - - 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.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 { + mavenCentral() maven { - credentials(org.gradle.api.credentials.PasswordCredentials.class) name = "StaticStudios" - setUrl("https://repo.staticstudios.net/private/") + url = "https://repo.staticstudios.net/snapshots/" } } - 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' + + java { + targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_21 + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } +} + +subprojects { + plugins.withId('maven-publish') { + publishing { + repositories { + maven { + credentials(org.gradle.api.credentials.PasswordCredentials.class) + name = "StaticStudios" + var base = "https://repo.staticstudios.net" + var releasesRepoUrl = "$base/releases/" + var snapshotsRepoUrl = "$base/snapshots/" + setUrl((version.toString().endsWith("SNAPSHOT")) ? snapshotsRepoUrl : releasesRepoUrl) } } } + + rootProject.tasks.named("publish").configure { + dependsOn(tasks.named("publish")) + } + + rootProject.tasks.named("publishToMavenLocal").configure { + dependsOn(tasks.named("publishToMavenLocal")) + } } } -def targetJavaVersion = 21 -java { - def javaVersion = JavaVersion.toVersion(targetJavaVersion) - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - if (JavaVersion.current() < javaVersion) { - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) - } +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 new file mode 100644 index 00000000..fcc14ceb --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,87 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'com.gradleup.shadow' version '8.3.3' +} + +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' + implementation 'net.staticstudios:static-utils:1.0.6-SNAPSHOT' + implementation 'com.h2database:h2:2.3.232' + implementation 'org.jetbrains:annotations:24.0.1' + + 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(':processor') + annotationProcessor project(':processor') + testCompileOnly project(':processor') + testAnnotationProcessor project(':processor') +} + +shadowJar { + archiveClassifier.set('') +} + +tasks.named('build') { + dependsOn(shadowJar) +} + +test { + useJUnitPlatform() +} + +tasks.withType(GenerateModuleMetadata).configureEach { + enabled = false +} + +java { + withSourcesJar() + withJavadocJar() +} + +javadoc { + options.tags = ["implSpec", "apiNote", "implNote"] +} + +publishing { + publications { + maven(MavenPublication) { + artifactId = 'static-data' + + artifact(tasks.shadowJar) { + classifier = null + } + + artifact sourcesJar + artifact javadocJar + + 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' + } + } + } + } +} \ No newline at end of file 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..224d4780 --- /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.AbstractCachedValue; +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, AbstractCachedValue 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().registerCachedValueUpdateHandlers(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 new file mode 100644 index 00000000..d31c1add --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/DataManager.java @@ -0,0 +1,1475 @@ +package net.staticstudios.data; + +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.BatchInsert; +import net.staticstudios.data.insert.InsertContext; +import net.staticstudios.data.insert.PostInsertAction; +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; +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; +import org.slf4j.LoggerFactory; + +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.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +@ApiStatus.Internal +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; + private final SQLBuilder sqlBuilder; + private final TaskQueue taskQueue; + private final ConcurrentHashMap, UniqueDataMetadata> uniqueDataMetadataMap = new ConcurrentHashMap<>(); + 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 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 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) { + DataSourceConfig dataSourceConfig = new DataSourceConfig( + config.postgresHost(), + config.postgresPort(), + config.postgresDatabase(), + config.postgresUsername(), + config.postgresPassword(), + config.redisHost(), + config.redisPort() + ); + this.updateHandlerExecutor = config.updateHandlerExecutor(); + + if (setGlobal) { + if (Boolean.FALSE.equals(DataManager.useGlobal)) { + 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); + this.taskQueue = new TaskQueue(dataSourceConfig, applicationName); + 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 + } + + public static DataManager getInstance() { + Preconditions.checkState(DataManager.instance != null, "Global DataManager instance has not been initialized"); + return DataManager.instance; + } + + public String getApplicationName() { + return applicationName; + } + + public DataAccessor getDataAccessor() { + return dataAccessor; + } + + 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; + 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); + } + + 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); + if (handlersForColumn == null) { + return; + } + + int columnIndex = columnNames.indexOf(column); + 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(); + 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 columnsInReferringTable were provided for UniqueData class " + holderClass.getName() + ". Required: " + metadata.idColumns() + ", Provided: " + columnNames); + } + } + UniqueData instance = getInstance(holderClass, idColumns); + for (ValueUpdateHandlerWrapper wrapper : handlers) { + Class dataType = wrapper.getDataType(); + Object deserializedOldValue = deserialize(dataType, oldSerializedValues[columnIndex]); + Object deserializedNewValue = deserialize(dataType, newSerializedValues[columnIndex]); + submitUpdateHandler(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); + } + } + } + + 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); + } + submitUpdateHandler(() -> wrapper.unsafeHandle(instance, deserializedOldValue, deserializedNewValue)); + } + } + } + + /** + * 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) { + 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, snapshot); + case PersistentOneToManyValueCollectionMetadata oneToManyValueCollectionMetadata -> + handleOneToManyValuedCollectionChange(wrapper, oneToManyValueCollectionMetadata, columnNames, oldSerializedValues, newSerializedValues); + case PersistentManyToManyCollectionMetadata manyToManyCollectionMetadata -> + handleManyToManyCollectionChange(wrapper, manyToManyCollectionMetadata, schema, table, columnNames, oldSerializedValues, newSerializedValues, cause, snapshot); + 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, @Nullable UniqueData snapshot) { + 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()); + List newValues = new ArrayList<>(); + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + int columnIndex = columnNames.indexOf(idColumn.name()); + Object newDeserializedValue = deserialize(idColumn.type(), newSerializedValues[columnIndex]); + newValues.add(newDeserializedValue); + } + + UniqueData instance = getInstanceForCollectionChangeHandler(metadata.getHolderClass(), links, columnNames, oldSerializedValues, newSerializedValues, handler.getType() == CollectionChangeHandlerWrapper.Type.ADD); + if (instance == null) { + return; + } + ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; + + if (handler.getType() == CollectionChangeHandlerWrapper.Type.REMOVE) { + 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); + } + + oldInstance = getInstance(referencedMetadata.clazz(), idColumns); + } + if (oldInstance != null) { + submitUpdateHandler(() -> handler.unsafeHandle(instance, oldInstance)); + } + } + + if (handler.getType() == CollectionChangeHandlerWrapper.Type.ADD) { + 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)); + } + } + } + + 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.getType() == CollectionChangeHandlerWrapper.Type.ADD); + + 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, 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()]; + 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.getType() == CollectionChangeHandlerWrapper.Type.ADD); + if (instance == null) { + return; + } + + ColumnValuePair[] idColumns = new ColumnValuePair[referencedMetadata.idColumns().size()]; + if (handler.getType() == CollectionChangeHandlerWrapper.Type.REMOVE) { + UniqueData oldInstance = null; + if (cause == TriggerCause.DELETE && Objects.equals(schema, referencedMetadata.schema()) && Objects.equals(table, referencedMetadata.table())) { + //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 { + try (ResultSet rs = dataAccessor.executeQuery(sqlBuilder.toString(), oldValues)) { + if (rs.next()) { + 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); + } + } + 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()) { + 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, 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 "); + 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 (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 = useNewValues + ? 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 UniqueData getInstanceForReferenceUpdateHandler(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); + } + + 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) { + updateHandlerExecutor.accept(runnable); + } + + public void registerPersistentValueUpdateHandlers(PersistentValueMetadata metadata, Collection> handlers) { + if (registeredUpdateHandlersForColumns.add(metadata)) { + for (ValueUpdateHandlerWrapper handler : handlers) { + addUpdateHandler(metadata.getSchema(), metadata.getTable(), metadata.getColumn(), handler); + } + } + } + + 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()); + for (CachedValueUpdateHandlerWrapper handler : handlers) { + addRedisUpdateHandler(partialKey, handler); + } + } + } + + 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 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)) { + return persistentValueUpdateHandlers.get(key).get(holderClass); + } + return Collections.emptyList(); + } + + public final void finishLoading() { + Preconditions.checkState(!finishedLoading, "finishLoading() has already been called"); + + finishedLoading = true; + dataAccessor.resync(); + } + + @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.addAll(extractMetadata(clazz)); + } + List defs = new ArrayList<>(); + for (Class clazz : classes) { + defs.addAll(sqlBuilder.parse(clazz)); + } + + for (DDLStatement ddl : defs) { + try { + dataAccessor.runDDL(ddl); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + try { + dataAccessor.postDDL(); + } 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 List extractMetadata(Class clazz) { + Data dataAnnotation = clazz.getAnnotation(Data.class); + List extracted = new ArrayList<>(); + extractMetadata(clazz, dataAnnotation, extracted); + return extracted; + } + + 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); + if (dataAnnotation == null) { + dataAnnotation = fallbackDataAnnotation; + } + Preconditions.checkNotNull(dataAnnotation, "UniqueData class %s is missing @Data annotation", clazz.getName()); + + 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); + extracted.add(metadata); + } + + for (Field field : ReflectionUtils.getFields(clazz, Relation.class)) { + Class genericType = ReflectionUtils.getGenericType(field); + if (genericType != null && !Modifier.isAbstract(genericType.getModifiers()) && UniqueData.class.isAssignableFrom(genericType)) { + Class dependencyClass = genericType.asSubclass(UniqueData.class); + if (!uniqueDataMetadataMap.containsKey(dependencyClass)) { + extractMetadata(dependencyClass, null, extracted); + } + } + } + + 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, extracted); + } + } + } + + 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; + } + + 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; + } + + 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 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)); + if (instance == null) { + return; + } + instance.markDeleted(); + }); + } + + 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 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 columnsInReferringTable 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); + }); + } + + 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); + } + } + + public T getInstance(Class clazz, ColumnValuePair... 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; + 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; + if (uniqueDataInstanceCache.containsKey(clazz) && uniqueDataInstanceCache.get(clazz).containsKey(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; + } + return instance; + } + + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + instance = constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + instance.setDataManager(this, false); + 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); + } + + PersistentValueImpl.delegate(instance); + CachedValueImpl.delegate(instance); + ReferenceImpl.delegate(instance); + PersistentOneToManyCollectionImpl.delegate(instance); + PersistentManyToManyCollectionImpl.delegate(instance); + PersistentOneToManyValueCollectionImpl.delegate(instance); + + uniqueDataInstanceCache.computeIfAbsent(clazz, k -> new MapMaker().weakValues().makeMap()) + .put(idColumns, instance); + + logger.trace("Cache miss for UniqueData class {} with ID columnsInReferringTable {}. Created new instance.", clazz.getName(), idColumns); + + 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 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 insertStatements = new LinkedList<>(); + for (InsertContext context : batch.getInsertContexts()) { + insertStatements.addAll(generateInsertStatements(context)); + } + + for (InsertStatement insertStatement : List.copyOf(insertStatements)) { + insertStatement.calculateRequiredDependencies(); + insertStatement.satisfyDependencies(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()) { + 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 generateInsertStatements(InsertContext insertContext) { + List insertStatements = new LinkedList<>(); + + Map> tableColumnsMap = new HashMap<>(); + insertContext.getEntries().forEach((simpleColumnMetadata, o) -> { + SQLTable table = Objects.requireNonNull(sqlBuilder.getSchema(simpleColumnMetadata.schema())).getTable(simpleColumnMetadata.table()); + tableColumnsMap.computeIfAbsent(table, k -> new LinkedList<>()) + .add(simpleColumnMetadata); + }); + + 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 (table.getIdColumns().stream().anyMatch(c -> c.name().equals(column.name()))) { + idColumns.add(new ColumnValuePair(column.name(), value)); + } else { + otherColumnValues.put(column, value); + } + } + InsertStatement statement = new InsertStatement(this, table, new ColumnValuePairs(idColumns.toArray(new ColumnValuePair[0]))); + + otherColumnValues.forEach((column, value) -> { + InsertStrategy strategy = insertContext.getInsertStrategy(column); + statement.set(column.name(), strategy, value); + }); + + insertStatements.add(statement); + } + + return insertStatements; + } + + 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 (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 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, 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); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + 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 = columnValuePair.column(); + sqlBuilder.append("\"").append(name).append("\" = ? AND "); + } + 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("\""); + 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("\""); + } + sqlBuilder.append(") ON "); + for (ColumnValuePair columnValuePair : idColumns) { + String name = columnValuePair.column(); + for (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 = columnValuePair.column(); + for (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 = columnValuePair.column(); + for (Link link : idColumnLinks) { + if (link.columnInReferringTable().equals(columnValuePair.column())) { + name = link.columnInReferencedTable(); + break; + } + } + sqlBuilder.append(", source.\"").append(name).append("\""); + } + sqlBuilder.append(")"); + } + @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(SQLTransaction.Statement.of(h2Sql, pgSql), values, delay); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + 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; + } + if (visited.contains(table)) { + return false; + } + visited.add(table); + stack.add(table); + for (SQLTable dep : dependencyGraph.getOrDefault(table.getName(), Collections.emptySet())) { + 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.getOrDefault(table.getName(), Collections.emptySet())) { + topoSort(dep, dependencyGraph, visited, ordered); + } + 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()); + } + + public T deserialize(Class clazz, Object serialized) { + if (serialized == null || Primitives.isPrimitive(clazz)) { + return (T) serialized; + } + return (T) deserialize(getValueSerializer(clazz), serialized); + } + + public T serialize(Object deserialized) { + if (deserialized == null || Primitives.isPrimitive(deserialized.getClass())) { + return (T) deserialized; + } + return (T) serialize(getValueSerializer(deserialized.getClass()), 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(); + } + + 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. +// * +// * @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; +// } + + /** + * 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/PersistentCollection.java b/core/src/main/java/net/staticstudios/data/PersistentCollection.java new file mode 100644 index 00000000..66f489c2 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/PersistentCollection.java @@ -0,0 +1,177 @@ +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 { + + static PersistentCollection of(UniqueData holder, Class referenceType) { + return new ProxyPersistentCollection<>(holder, referenceType); + } + + 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) { + 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; + } + + 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)); + 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 getDataType() { + return referenceType; + } + + 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 + 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(); + } + + @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 new file mode 100644 index 00000000..a56328c6 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/PersistentValue.java @@ -0,0 +1,84 @@ +package net.staticstudios.data; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.util.PersistentValueMetadata; +import net.staticstudios.data.util.Value; +import net.staticstudios.data.util.ValueUpdateHandler; +import net.staticstudios.data.util.ValueUpdateHandlerWrapper; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * A persistent value represents a single cell in a database referringTable. + * + * @param + */ +public interface PersistentValue extends Value { + //todo: use caffeine to further cache pvs, provided we are using the H2 data accessor. allow us to toggle this on and off when setting up the data manager + + static PersistentValue of(UniqueData holder, Class dataType) { + return new ProxyPersistentValue<>(holder, dataType); + } + + UniqueData getHolder(); + + Class getDataType(); + + PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler); + + class ProxyPersistentValue implements PersistentValue { + protected final UniqueData holder; + protected final Class dataType; + private final List> updateHandlers = new ArrayList<>(); + private @Nullable PersistentValue delegate; + + public ProxyPersistentValue(UniqueData holder, Class dataType) { + this.holder = holder; + this.dataType = dataType; + } + + 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; + holder.getDataManager().registerPersistentValueUpdateHandlers(metadata, updateHandlers); + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + public Class getDataType() { + return dataType; + } + + @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; + } + + @Override + public T get() { + if (delegate != null) { + return delegate.get(); + } + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void set(T value) { + if (delegate != null) { + delegate.set(value); + return; + } + throw new UnsupportedOperationException("Not implemented"); + } + } +} diff --git a/core/src/main/java/net/staticstudios/data/Reference.java b/core/src/main/java/net/staticstudios/data/Reference.java new file mode 100644 index 00000000..d426560a --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/Reference.java @@ -0,0 +1,78 @@ +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 { + + static Reference of(UniqueData holder, Class referenceType) { + return new ProxyReference<>(holder, referenceType); + } + + UniqueData getHolder(); + + Class getReferenceType(); + + @Nullable T get(); + + void set(@Nullable T value); + + 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) { + 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; + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + 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"); + 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(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 new file mode 100644 index 00000000..d5ce8f02 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/StaticData.java @@ -0,0 +1,93 @@ +package net.staticstudios.data; + +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. + */ +public class StaticData { + private static boolean initialized = false; + + /** + * Initializes the StaticData system with the provided configuration. + * + * @param config the configuration to use for initialization + */ + public static void init(StaticDataConfig config) { + initialized = true; + new DataManager(config, true); + } + + /** + * Loads the specified UniqueData classes into the StaticData system. + * Loading a class does two main things:
+ * 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 + */ + @SafeVarargs + public static void load(Class... classes) { + assertInit(); + 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. + * + * @return a new BatchInsert instance + */ + public static BatchInsert createBatchInsert() { + assertInit(); + return DataManager.getInstance().createBatchInsert(); + } + + private static void assertInit() { + Preconditions.checkState(initialized, "StaticData has not been initialized! Please call StaticData.init(...) before using any other methods."); + } + + /** + * 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 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); + } + + /** + * 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/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/main/java/net/staticstudios/data/UniqueData.java b/core/src/main/java/net/staticstudios/data/UniqueData.java new file mode 100644 index 00000000..996b5d7c --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/UniqueData.java @@ -0,0 +1,109 @@ +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.SQLTransaction; +import net.staticstudios.data.util.UniqueDataMetadata; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.ApiStatus; + +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, boolean isSnapshot) { + this.dataManager = dataManager; + this.isSnapshot = isSnapshot; + } + + @ApiStatus.Internal + 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; + } + + public synchronized ColumnValuePairs getIdColumns() { + return idColumns; + } + + 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 stringBuilder = new StringBuilder("DELETE FROM \"" + metadata.schema() + "\".\"" + metadata.table() + "\" WHERE "); + List values = new ArrayList<>(); + for (ColumnValuePair idColumn : idColumns) { + stringBuilder.append("\"").append(idColumn.column()).append("\" = ? AND "); + values.add(idColumn.value()); + } + stringBuilder.setLength(stringBuilder.length() - 5); + @Language("SQL") String sql = stringBuilder.toString(); + + try { + dataManager.getDataAccessor().executeUpdate(SQLTransaction.Statement.of(sql, sql), values, 0); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public final boolean isSnapshot() { + return isSnapshot; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.getClass().getSimpleName()).append("{"); + if (this.idColumns != null) { + for (ColumnValuePair idColumn : idColumns) { + sb.append(idColumn.column()).append("=").append(idColumn.value()).append(", "); + } + } + if (isDeleted) { + sb.append("deleted=true, "); + } + if (isSnapshot) { + sb.append("snapshot=true, "); + } + sb.append("dataManager=").append(dataManager); + sb.append("}"); + return sb.toString(); + } + + @Override + public final int hashCode() { + return Objects.hash(dataManager, idColumns); + } + + @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/ValueSerializer.java b/core/src/main/java/net/staticstudios/data/ValueSerializer.java similarity index 56% rename from src/main/java/net/staticstudios/data/ValueSerializer.java rename to core/src/main/java/net/staticstudios/data/ValueSerializer.java index 68e1bf1d..472c14b9 100644 --- a/src/main/java/net/staticstudios/data/ValueSerializer.java +++ b/core/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 @@ -38,26 +40,4 @@ public interface ValueSerializer { * @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/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java new file mode 100644 index 00000000..93a4c6af --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/DataAccessor.java @@ -0,0 +1,37 @@ +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; +import java.util.List; + +public interface DataAccessor { + + ResultSet executeQuery(@Language("SQL") String sql, List values) throws SQLException; + + 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; + + void insert(List sqlStatements, InsertMode insertMode) throws SQLException; + + 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); + + void resync(); +} 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 new file mode 100644 index 00000000..7d09804a --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java @@ -0,0 +1,133 @@ +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 extends AbstractCachedValue { + private final UniqueData holder; + private final Class dataType; + private final CachedValueMetadata metadata; + + 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); + } + + + @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 getFallback(); + } + return value; + } + + @Override + public void set(@Nullable T value) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set value on a deleted UniqueData instance"); + T fallback = getFallback(); + 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 new file mode 100644 index 00000000..af972ed9 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentManyToManyCollectionImpl.java @@ -0,0 +1,739 @@ +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; +import net.staticstudios.data.impl.DataAccessor; +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.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 PersistentManyToManyCollectionMetadata metadata; + + @SuppressWarnings("unchecked") + public PersistentManyToManyCollectionImpl(UniqueData holder, PersistentManyToManyCollectionMetadata metadata) { + this.holder = holder; + this.type = (Class) metadata.getReferencedType(); + this.metadata = metadata; + } + + public static void createAndDelegate(ProxyPersistentCollection proxy, PersistentManyToManyCollectionMetadata metadata) { + PersistentManyToManyCollectionImpl impl = new PersistentManyToManyCollectionImpl<>(proxy.getHolder(), metadata); + proxy.setDelegate(metadata, impl); + } + + public static PersistentManyToManyCollectionImpl create(UniqueData holder, PersistentManyToManyCollectionMetadata metadata) { + return new PersistentManyToManyCollectionImpl<>(holder, metadata); + } + + 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(clazz, 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 referencedTable; + } + + public static List getJoinTableToDataTableLinks(String dataTable, String links) { + List joinTableToDataTableLinks = new ArrayList<>(); + String dataTableColumnPrefix = getDataTableColumnPrefix(dataTable); + for (Link link : SQLBuilder.parseLinks(links)) { + String columnInDataTable = link.columnInReferringTable(); + String dataColumnInJoinTable = dataTableColumnPrefix + "_" + columnInDataTable; + + joinTableToDataTableLinks.add(new 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 (Link link : SQLBuilder.parseLinks(links)) { + String columnInReferencedTable = link.columnInReferencedTable(); + String referencedColumnInJoinTable = referencedTableColumnPrefix + "_" + columnInReferencedTable; + + joinTableToReferencedTableLinks.add(new Link(columnInReferencedTable, referencedColumnInJoinTable)); + } + 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"); + } + + @Override + public PersistentCollection onRemove(Class holderClass, CollectionChangeHandler removeHandler) { + throw new UnsupportedOperationException("Dynamically adding change handlers is not supported for PersistentCollections"); + } + + public PersistentManyToManyCollectionMetadata getMetadata() { + return this.metadata; + } + + @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(); + ColumnValuePairs thatIdColumns = data.getIdColumns(); + return ids.contains(thatIdColumns); + } + + @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 (ColumnValuePairs 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 (ColumnValuePairs 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); + ColumnValuePairs thatIdColumns = data.getIdColumns(); + if (!ids.contains(thatIdColumns)) { + 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 = 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(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (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 (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); + ColumnValuePairs idColumns = data.getIdColumns(); + 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); + ColumnValuePairs thatIdColumns = data.getIdColumns(); + idsToRetain.add(thatIdColumns); + } + + List idsToRemove = new ArrayList<>(); + for (ColumnValuePairs idColumns : currentIds) { + if (!idsToRetain.contains(idColumns)) { + 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 = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); + + 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 (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 = 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(); + + SQLTransaction transaction = new SQLTransaction(); + transaction.query(selectDataIdsStatement, () -> holderIdValues, rs -> { + try { + Preconditions.checkState(rs.next(), "Could not find holder row in database"); + for (Link entry : joinTableToDataTableLinks) { + String dataColumn = entry.columnInReferencedTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + + for (ColumnValuePairs idColumns : idsToRemove) { + List referencedIdValues = idColumns.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 (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 + */ + public Set getIds() { + //todo: this method is slow, plan to cache this later. + Preconditions.checkArgument(!holder.isDeleted(), "Cannot get entries on a deleted UniqueData instance"); + Set ids = new HashSet<>(); + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata target = holder.getDataManager().getMetadata(type); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + + 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 "); + 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 (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 (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(new ColumnValuePairs(idColumns)); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return ids; + } + + private SQLTransaction.Statement buildSelectDataIdsStatement() { + UniqueDataMetadata holderMetadata = holder.getMetadata(); + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (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 = metadata.getJoinTableToReferencedTableLinks(holder.getDataManager()); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (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() { + return buildUpdateStatement(holder.getDataManager(), metadata); + } + + private SQLTransaction.Statement buildRemoveStatement() { + 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("DELETE FROM \"").append(joinTableSchema).append("\".\"").append(joinTableName).append("\" WHERE "); + for (Link entry : joinTableToDataTableLinks) { + String joinColumn = entry.columnInReferringTable(); + sqlBuilder.append("\"").append(joinColumn).append("\" = ? AND "); + } + for (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() { + List joinTableToDataTableLinks = metadata.getJoinTableToDataTableLinks(holder.getDataManager()); + + 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) { + 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); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof PersistentManyToManyCollectionImpl that)) return false; + boolean equals = Objects.equals(type, that.type) && + 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(); + + return ids.equals(thatIds); + } + + @Override + public int hashCode() { + int hash = Objects.hash(type, + 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 (ColumnValuePairs idColumns : getIds()) { + arrayHash += idColumns.hashCode(); + } + + 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(); + } + ColumnValuePairs 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/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java new file mode 100644 index 00000000..436223c5 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyCollectionImpl.java @@ -0,0 +1,516 @@ +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; +import net.staticstudios.data.impl.DataAccessor; +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.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, PersistentOneToManyCollectionMetadata metadata) { + PersistentOneToManyCollectionImpl delegate = new PersistentOneToManyCollectionImpl<>( + proxy.getHolder(), + proxy.getDataType(), + link + ); + proxy.setDelegate(metadata, 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(), oneToManyMetadata); + } else { + pair.field().setAccessible(true); + try { + pair.field().set(instance, create(instance, oneToManyMetadata.getReferencedType(), oneToManyMetadata.getLinks())); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + 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); + 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(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() { + 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); + return getIds().contains(data.getIdColumns()); + } + + @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 (ColumnValuePairs 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 (ColumnValuePairs 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); + if (!ids.contains(data.getIdColumns())) { + 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()) { + return false; + } + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + + 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 (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 true; + } + + @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); + ColumnValuePairs thatIdColumns = data.getIdColumns(); + ids.add(thatIdColumns); + } + removeIds(ids); + + return !ids.isEmpty(); + } + + @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); + ColumnValuePairs thatIdColumns = data.getIdColumns(); + idsToRetain.add(thatIdColumns); + } + + List idsToRemove = new ArrayList<>(); + for (ColumnValuePairs idColumns : currentIds) { + if (!idsToRetain.contains(idColumns)) { + idsToRemove.add(idColumns); + } + } + + if (!idsToRemove.isEmpty()) { + removeIds(idsToRemove); + 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 removeIds(List ids) { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot set entries on a deleted UniqueData instance"); + if (ids.isEmpty()) { + return; + } + + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + + 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 (Link entry : link) { + String dataColumn = entry.columnInReferringTable(); + Object value = rs.getObject(dataColumn); + holderLinkingValues.add(value); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + for (ColumnValuePairs idColumns : ids) { + transaction.update(updateStatement, () -> { + List values = new ArrayList<>(); + for (Object holderLinkingValue : holderLinkingValues) { //set them to null + values.add(null); + } + for (ColumnValuePair idColumn : idColumns) { + values.add(idColumn.value()); + } + return values; + }); + } + 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 buildUpdateStatement() { + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); + for (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 sql = sqlBuilder.toString(); + return SQLTransaction.Statement.of(sql, sql); + } + + private SQLTransaction.Statement buildClearStatement() { + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("UPDATE \"").append(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\" SET "); + for (Link entry : link) { + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("\"").append(theirColumn).append("\" = NULL, "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + sqlBuilder.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); + } + + public Set getIds() { + // note: we need the join since we support linking on non-id columnsInReferringTable + Preconditions.checkArgument(!holder.isDeleted(), "Cannot get entries on a deleted UniqueData instance"); + Set ids = new HashSet<>(); + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata typeMetadata = holder.getDataManager().getMetadata(type); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ColumnMetadata columnMetadata : typeMetadata.idColumns()) { + sqlBuilder.append("\"").append(columnMetadata.schema()).append("\".\"").append(columnMetadata.table()).append("\".\"").append(columnMetadata.name()).append("\", "); + } + for (ColumnMetadata columnMetadata : holderMetadata.idColumns()) { + 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("\" 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("\" = \"") + .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(typeMetadata.schema()).append("\".\"").append(typeMetadata.table()).append("\".\"").append(theirColumn).append("\" = \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(entry.columnInReferringTable()).append("\" AND "); + } + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("\"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\".\"").append(columnValuePair.column()).append("\" = ? AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + + @Language("SQL") String sql = sqlBuilder.toString(); + try (ResultSet rs = dataAccessor.executeQuery(sql, holder.getIdColumns().stream().map(ColumnValuePair::value).toList())) { + 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(new ColumnValuePairs(idColumns)); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return ids; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof PersistentOneToManyCollectionImpl 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 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(); + } + ColumnValuePairs 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/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..1d56dc0c --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentOneToManyValueCollectionImpl.java @@ -0,0 +1,521 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Preconditions; +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; +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, PersistentOneToManyValueCollectionMetadata metadata) { + PersistentOneToManyValueCollectionImpl delegate = new PersistentOneToManyValueCollectionImpl<>( + proxy.getHolder(), + proxy.getDataType(), + dataSchema, + dataTable, + dataColumn, + link + ); + proxy.setDelegate(metadata, 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(), + oneToManyValueMetadata + ); + } 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(clazz, genericType, schema, table, column, 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() { + 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(holder.getDataManager().serialize(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(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("\", "); + } + 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("\" ON "); + for (Link entry : link) { + String myColumn = entry.columnInReferringTable(); + String theirColumn = entry.columnInReferencedTable(); + 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(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 "); + } + 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(holder.getDataManager().deserialize(type, 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/data/PersistentValueImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java new file mode 100644 index 00000000..e2d1e52a --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/PersistentValueImpl.java @@ -0,0 +1,161 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Preconditions; +import net.staticstudios.data.*; +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; +import java.util.*; + +public class PersistentValueImpl implements PersistentValue { + private final UniqueData holder; + private final Class dataType; + private final PersistentValueMetadata metadata; + + private PersistentValueImpl(UniqueData holder, Class dataType, PersistentValueMetadata metadata) { + this.holder = holder; + this.dataType = dataType; + this.metadata = metadata; + } + + public static void createAndDelegate(ProxyPersistentValue proxy, PersistentValueMetadata metadata) { + PersistentValueImpl delegate = new PersistentValueImpl<>( + proxy.getHolder(), + proxy.getDataType(), + metadata + ); + + proxy.setDelegate(metadata, delegate); + } + + 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()); + if (pair.instance() instanceof PersistentValue.ProxyPersistentValue proxyPv) { + PersistentValueImpl.createAndDelegate(proxyPv, pvMetadata); + } else { + pair.field().setAccessible(true); + try { + pair.field().set(instance, PersistentValueImpl.create(instance, ReflectionUtils.getGenericType(pair.field()), pvMetadata)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + public static Map extractMetadata(String schema, String table, Class clazz) { + Map metadataMap = new HashMap<>(); + for (Field field : ReflectionUtils.getFields(clazz, PersistentValue.class)) { + 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( + schema, + 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 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; + } + + @Override + public Class getDataType() { + return dataType; + } + + @Override + public PersistentValue onUpdate(Class holderClass, ValueUpdateHandler updateHandler) { + throw new UnsupportedOperationException("Dynamically adding update handlers is not supported"); + } + + @Override + public T get() { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot get value from a deleted UniqueData instance"); + 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(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(); + } + + @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/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..4ce0ba1c --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyReference.java @@ -0,0 +1,83 @@ +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.links()).getReferencedColumnValuePairs() + ); + + proxy.setDelegate(metadata, delegate); + } + + private static Reference create(UniqueData holder, Class referenceType, ReferenceMetadata metadata) { + return new ReadOnlyReference<>(holder, referenceType, ReferenceImpl.create(holder, referenceType, metadata.links()).getReferencedColumnValuePairs()); + } + + public static void delegate(U instance) { + UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); + for (FieldInstancePair<@Nullable Reference> pair : ReflectionUtils.getFieldInstancePairs(instance, Reference.class)) { + ReferenceMetadata refMetadata = metadata.referenceMetadata().get(pair.field()); + if (pair.instance() instanceof Reference.ProxyReference proxyRef) { + createAndDelegate(proxyRef, refMetadata); + } else { + pair.field().setAccessible(true); + try { + pair.field().set(instance, create(instance, refMetadata.referencedClass(), refMetadata)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + 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()); + } + + @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 new file mode 100644 index 00000000..6231079a --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReferenceImpl.java @@ -0,0 +1,203 @@ +package net.staticstudios.data.impl.data; + +import com.google.common.base.Preconditions; +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; +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; + +public class ReferenceImpl implements Reference { + private final UniqueData holder; + private final Class type; + private final 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, ReferenceMetadata metadata) { + ReferenceImpl delegate = new ReferenceImpl<>( + proxy.getHolder(), + proxy.getReferenceType(), + metadata.links() + ); + proxy.setDelegate(metadata, delegate); + } + + public static ReferenceImpl create(UniqueData holder, Class type, List link) { + return new ReferenceImpl<>(holder, type, link); + } + + public static void delegate(T instance) { + UniqueDataMetadata metadata = instance.getDataManager().getMetadata(instance.getClass()); + for (FieldInstancePair<@Nullable Reference> pair : ReflectionUtils.getFieldInstancePairs(instance, Reference.class)) { + ReferenceMetadata refMetadata = metadata.referenceMetadata().get(pair.field()); + + if (pair.instance() instanceof Reference.ProxyReference proxyRef) { + createAndDelegate(proxyRef, refMetadata); + } else { + pair.field().setAccessible(true); + try { + pair.field().set(instance, create(instance, refMetadata.referencedClass(), refMetadata.links())); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + public static Map extractMetadata(Class clazz) { + Map metadataMap = new HashMap<>(); + for (Field field : ReflectionUtils.getFields(clazz, Reference.class)) { + 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())); + metadataMap.put(field, new ReferenceMetadata(clazz, (Class) referencedClass, SQLBuilder.parseLinks(oneToOneAnnotation.link()), oneToOneAnnotation.fkey())); + } + + return metadataMap; + } + + @Override + public UniqueData getHolder() { + return holder; + } + + @Override + 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(); + 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; + UniqueDataMetadata holderMetadata = holder.getMetadata(); + UniqueDataMetadata referencedMetadata = holder.getDataManager().getMetadata(type); + DataAccessor dataAccessor = holder.getDataManager().getDataAccessor(); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("SELECT "); + for (ColumnMetadata idColumn : referencedMetadata.idColumns()) { + sqlBuilder.append("_referenced.\"").append(idColumn.name()).append("\", "); + } + for (Link entry : link) { + String myColumn = entry.columnInReferringTable(); + sqlBuilder.append("_referring.\"").append(myColumn).append("\", "); + } + sqlBuilder.setLength(sqlBuilder.length() - 2); + + sqlBuilder.append(" FROM \"").append(referencedMetadata.schema()).append("\".\"").append(referencedMetadata.table()).append("\" _referenced"); + sqlBuilder.append(" INNER JOIN \"").append(holderMetadata.schema()).append("\".\"").append(holderMetadata.table()).append("\" _referring ON "); + for (Link entry : link) { + String myColumn = entry.columnInReferringTable(); + String theirColumn = entry.columnInReferencedTable(); + sqlBuilder.append("_referenced.\"").append(theirColumn).append("\" = "); + sqlBuilder.append("_referring.\"").append(myColumn).append("\" AND "); + } + sqlBuilder.setLength(sqlBuilder.length() - 5); + + sqlBuilder.append(" WHERE "); + + for (ColumnValuePair columnValuePair : holder.getIdColumns()) { + sqlBuilder.append("_referring.\"").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()) { + return null; + } + + for (Link entry : link) { + String myColumn = entry.columnInReferringTable(); + String theirColumn = entry.columnInReferencedTable(); + if (rs.getObject(myColumn) == null) { + return null; + } + idColumns[i++] = new ColumnValuePair(theirColumn, rs.getObject(myColumn)); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return new ColumnValuePairs(idColumns); + } + + @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 (Link entry : link) { + String myColumn = entry.columnInReferringTable(); + if (value == null) { + sqlBuilder.append("\"").append(myColumn).append("\" = NULL, "); + continue; + } + + String theirColumn = entry.columnInReferencedTable(); + 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 name %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()); + } + + @Language("SQL") String sql = sqlBuilder.toString(); + + try { + holder.getDataManager().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/h2/H2DataAccessor.java b/core/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java new file mode 100644 index 00000000..146d5c01 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/h2/H2DataAccessor.java @@ -0,0 +1,722 @@ +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.RedisEncodedValue; +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.*; +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.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. + */ +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 static final Gson GSON = new Gson(); + 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; + 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"); + return t; + }); + private final RedisListener redisListener; + private final Map redisCache = new ConcurrentHashMap<>(); + private final Set knownRedisPartialKeys = ConcurrentHashMap.newKeySet(); + + private final ThreadLocal> commitCallbacks = ThreadLocal.withInitial(LinkedList::new); + private final ThreadLocal> rollbackCallbacks = ThreadLocal.withInitial(LinkedList::new); + + 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; + 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 -> { + try { + SQLSchema sqlSchema = dataManager.getSQLBuilder().getSchema(notification.getSchema()); + if (sqlSchema == null) { + return; // we don't care about this schema + } + SQLTable sqlTable = sqlSchema.getTable(notification.getTable()); + if (sqlTable == null) { + return; // we don't care about this table + } + switch (notification.getOperation()) { + 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()]; + + 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); + } + } + + if (index == 0) { + return; // nothing changed + } + + for (Pair changed : changedValues) { + if (changed == null) break; + String column = changed.first(); + String encoded = changed.second(); + SQLColumn sqlColumn = sqlTable.getColumn(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("\" = ?, "); + } + 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); + 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("\", "); + } + 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); + } + }); + + 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); + } + }); + + + 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, 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); + } + logger.trace("[H2] {}", SET_REFERENTIAL_INTEGRITY_TRUE); + h2Statement.execute(SET_REFERENTIAL_INTEGRITY_TRUE); + } finally { + if (autoCommit) { + h2Connection.setAutoCommit(true); + } else { + h2Connection.commit(); + } + } + } + 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(); + + for (String key : scanResult.getResult()) { + redisCache.put(key, decodeRedis(jedis.get(key)).value()); + } + } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); + + redisListener.listen(partialKey, this::handleRedisEvent); + } + }).join(); + + //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 = new H2ProxyConnection(connection, this)); + + 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; + } + + 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(List sqlStatements, InsertMode insertMode) throws SQLException { + Connection connection = getConnection(); + boolean autoCommit = connection.getAutoCommit(); + try { + connection.setAutoCommit(false); + + for (SQlStatement sqlStatement : sqlStatements) { + try (PreparedStatement preparedStatement = connection.prepareStatement(sqlStatement.getH2Sql())) { + int i = 1; + for (Object value : sqlStatement.getValues()) { + preparedStatement.setObject(i++, value); + } + logger.trace("[H2] {}", sqlStatement.getH2Sql()); + 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.getPgSql())) { + List values = statement.getValues(); + for (int i = 0; i < values.size(); i++) { + Object value = values.get(i); + preparedStatement.setObject(i + 1, value); + } + logger.debug("[DB] {}", statement.getPgSql()); + preparedStatement.executeUpdate(); + } + } + } finally { + if (realAutoCommit) { + realConnection.setAutoCommit(true); + } else { + realConnection.commit(); + } + } + }); + + if (insertMode == InsertMode.SYNC) { + try { + 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(); + } + } + } + + @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.trace("[H2] {}", sql); + return cachePreparedStatement.executeQuery(); + } + + @Override + 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.trace("[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(transaction, delay); + } + + @Override + public void runDDL(DDLStatement ddl) { + taskQueue.submitTask(connection -> { + if (!ddl.postgresqlStatement().isEmpty()) { + + logger.debug("[DB] {}", 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(); + + } + + @Override + public void postDDL() throws SQLException { + updateKnownTables(); + } + + @Override + public @Nullable String getRedisValue(String key) { + return redisCache.get(key); + } + + @Override + public void setRedisValue(String key, String value, int expirationSeconds) { + String prev; + if (value == null) { + prev = redisCache.remove(key); + taskQueue.submitTask((connection, jedis) -> { + jedis.del(key); + }); + } else { + prev = redisCache.put(key, value); + taskQueue.submitTask((connection, jedis) -> { + if (expirationSeconds > 0) { + jedis.setex(key, expirationSeconds, encodeRedis(value)); + } else { + jedis.set(key, encodeRedis(value)); + } + }); + } + + RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); + dataManager.callCachedValueUpdateHandlers(deconstructedKey.partialKey(), deconstructedKey.encodedIdNames(), deconstructedKey.encodedIdValues(), prev, value); + } + + @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(new ArrayList<>(knownTables), new ArrayList<>(knownRedisPartialKeys)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + 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"); + SchemaTable schemaTable = new SchemaTable(schema, table); + currentTables.add(schemaTable); + + 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'"; + + 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()); + logger.trace("[H2] {}", formatted); + H2UpdateHandlerTrigger.registerDataManager(randomId, dataManager); + createTrigger.execute(formatted); + } + + taskQueue.submitTask(realDbConnection -> postgresListener.ensureTableHasTrigger(realDbConnection, schema, table)).join(); + } + } + } + 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; + } + + 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 -> { + 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); + } + } + }); + + if (delay <= 0) { + runnable.run(); + return; + } + if (delayedTasks.put(key, runnable) == null) { + scheduledExecutorService.schedule(() -> { + Runnable removed = delayedTasks.remove(key); + if (removed != null) { + removed.run(); + } + }, delay, TimeUnit.MILLISECONDS); + } + } + + 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) { + String entry = redisCache.get(key); + if (entry != null && Objects.equals(entry, value)) { + return; + } + redisCache.put(key, value); + RedisUtils.DeconstructedKey deconstructedKey = RedisUtils.deconstruct(key); + 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/H2DeleteStrategyCascadeTrigger.java b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2DeleteStrategyCascadeTrigger.java new file mode 100644 index 00000000..98b22fd6 --- /dev/null +++ b/core/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.utils.Link; +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 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 (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/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 new file mode 100644 index 00000000..63a6a186 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/impl/h2/trigger/H2UpdateHandlerTrigger.java @@ -0,0 +1,115 @@ +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; + +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 H2UpdateHandlerTrigger implements Trigger { + private static final Map dataManagerMap = new ConcurrentHashMap<>(); + private final Logger logger = LoggerFactory.getLogger(H2UpdateHandlerTrigger.class); + private final List columnNames = new ArrayList<>(); + private DataManager dataManager; + private H2DataAccessor dataAccessor; + private String schema; + private String table; + + public static void registerDataManager(UUID id, DataManager dataManager) { + dataManagerMap.put(id, dataManager); + } + + @Override + public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException { + UUID dataManagerId = UUID.fromString(triggerName.substring(triggerName.length() - 36).replace('_', '-')); + this.table = triggerName.substring(triggerName.indexOf("_trg_") + 5, triggerName.length() - 37); //dont use referringTable name since it might be a copy for an internal referringTable (very odd behavior i must say h2) + this.dataManager = dataManagerMap.get(dataManagerId); + this.dataAccessor = (H2DataAccessor) dataManager.getDataAccessor(); + this.schema = schemaName; + } + + @Override + public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException { + //todo: when were syncing data, we should ignore all triggers. we should globally pause basically everything. + int dataLength = oldRow != null ? oldRow.length : (newRow != null ? newRow.length : 0); + if (columnNames.size() != dataLength) { + List columns = new ArrayList<>(dataLength); + try (PreparedStatement ps = connection.prepareStatement( + "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION" + )) { + ps.setString(1, schema); + ps.setString(2, table); // H2 stores names in uppercase by default + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + columns.add(rs.getString("COLUMN_NAME")); + } + } + } + logger.trace("Schema change detected (or first run). Old name names: {}, new name names: {}", columnNames, columns); + columnNames.clear(); + columnNames.addAll(columns); + } + + + if (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) { + dataAccessor.onCommit(() -> dataManager.callCollectionChangeHandlers(columnNames, schema, table, columnNames, new Object[newRow.length], newRow, TriggerCause.INSERT, null)); + } + + private void handleUpdate(Object[] oldRow, Object[] newRow) { + List changedColumns = new ArrayList<>(); + for (int i = 0; i < oldRow.length; i++) { + Object oldValue = oldRow[i]; + Object newValue = newRow[i]; + if (!Objects.equals(oldValue, newValue)) { + changedColumns.add(columnNames.get(i)); + } + } + + dataAccessor.onCommit(() -> { + for (String changedColumn : changedColumns) { + dataManager.updateIdColumns(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, null); + dataManager.callReferenceUpdateHandlers(columnNames, schema, table, changedColumns, oldRow, newRow); + }); + } + + private void handleDelete(Object[] 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/src/main/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 src/main/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 87% 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 index c41315d6..59188a48 100644 --- a/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java +++ b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresListener.java @@ -29,7 +29,7 @@ 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 $$ + create or replace function propagate_data_update_v3() returns trigger as $$ declare notification text; begin @@ -39,7 +39,7 @@ create or replace function propagate_data_update() returns trigger as $$ 'new', (case when TG_OP = 'DELETE' then '{}' else row_to_json(NEW) end) )::text; - perform pg_notify('data_notification', notification); + perform pg_notify('data_notification_v3', notification); return new; end; @@ -51,12 +51,12 @@ create or replace function propagate_data_update() returns trigger as $$ IF NOT EXISTS ( SELECT 1 FROM pg_trigger - WHERE tgname = 'propagate_data_update_trigger' + WHERE tgname = 'propagate_data_update_v3_trigger' AND tgrelid = '%s'::regclass ) THEN - CREATE TRIGGER propagate_data_update_trigger + CREATE TRIGGER propagate_data_update_v3_trigger AFTER INSERT OR UPDATE OR DELETE ON %s - FOR EACH ROW EXECUTE PROCEDURE propagate_data_update(); + FOR EACH ROW EXECUTE PROCEDURE propagate_data_update_v3(); END IF; END; $$ @@ -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); } @@ -115,7 +116,7 @@ private void setPgConnection(DataManager dataManager, DataSourceConfig ds) throw statement.execute(CREATE_DATA_NOTIFY_FUNCTION); } - pgConnection.addNotificationListener("data_notification", new PGNotificationListener() { + 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); @@ -156,7 +157,7 @@ public void notification(int processId, String channelName, String payload) { }); try (Statement statement = pgConnection.createStatement()) { - statement.execute("LISTEN data_notification"); + statement.execute("LISTEN data_notification_v3"); } } @@ -166,18 +167,20 @@ 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 schemaTable the table to add the trigger to + * @param connection the connection to the database + * @param schema the referringSchema of the referringTable + * @param table the referringTable to ensure has the trigger */ - public void ensureTableHasTrigger(Connection connection, String schemaTable) { + 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); + logger.debug("Adding propagate_data_update_trigger to referringTable: {}", schemaTable); try (Statement statement = connection.createStatement()) { statement.execute(sql); @@ -187,4 +190,4 @@ public void ensureTableHasTrigger(Connection connection, String schemaTable) { tablesTriggered.add(schemaTable); } -} +} \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java b/core/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java similarity index 91% rename from src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java rename to core/src/main/java/net/staticstudios/data/impl/pg/PostgresNotification.java index 0571efc9..914e523e 100644 --- a/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/src/main/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 src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java rename to core/src/main/java/net/staticstudios/data/impl/pg/PostgresOperation.java 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/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/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 new file mode 100644 index 00000000..a67c66dc --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/insert/InsertContext.java @@ -0,0 +1,135 @@ +package net.staticstudios.data.insert; + +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; +import net.staticstudios.data.parse.SQLTable; +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.*; +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; + } + + 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); + 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 referringTable: " + table + " referringSchema: " + schema); + + SimpleColumnMetadata columnMetadata = new SimpleColumnMetadata( + schema, + table, + 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 referringTable " + table + " referringSchema " + schema + ". Expected: " + sqlColumn.getType().getName() + ", got: " + Objects.requireNonNull(value).getClass().getName()); + + entries.put(columnMetadata, dataManager.serialize(value)); + 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 InsertStrategy getInsertStrategy(SimpleColumnMetadata columnMetadata) { + return insertStrategies.get(columnMetadata); + } + + public void markInserted() { + inserted.set(true); + } + + public boolean isInserted() { + return inserted.get(); + } + + public InsertContext insert(InsertMode insertMode) { + dataManager.insert(this, insertMode); + return this; + } + + /** + * 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. + * @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 = 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 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)))); + } + 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..116de83c --- /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 = ValueUtils.parseValue(joinTableSchema); + return this; + } + + public Builder joinTableName(String joinTableName) { + this.joinTableName = ValueUtils.parseValue(joinTableName); + return this; + } + + public Builder referringId(String column, Object value) { + this.referringIds.add(new ColumnValuePair(ValueUtils.parseValue(column), value)); + return this; + } + + public Builder referencedId(String column, Object value) { + this.referencedIds.add(new ColumnValuePair(ValueUtils.parseValue(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..3b472566 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/insert/PostInsertAction.java @@ -0,0 +1,109 @@ +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.*; + +import java.util.List; +import java.util.Map; + +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; + } + + 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; + 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)); + } + + static 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/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/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/parse/DDLStatement.java b/core/src/main/java/net/staticstudios/data/parse/DDLStatement.java new file mode 100644 index 00000000..0c531282 --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/parse/ForeignKey.java b/core/src/main/java/net/staticstudios/data/parse/ForeignKey.java new file mode 100644 index 00000000..1fdcfe41 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/parse/ForeignKey.java @@ -0,0 +1,102 @@ +package net.staticstudios.data.parse; + +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; +import java.util.Objects; +import java.util.Set; + +public class ForeignKey { + private final Set links = new LinkedHashSet<>(); + 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 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.CASCADE; + } + + public void addLink(Link link) { + links.add(link); + } + + public Set getLinkingColumns() { + return Collections.unmodifiableSet(links); + } + + public String getReferencedSchema() { + return referencedSchema; + } + + public String getReferencedTable() { + return referencedTable; + } + + public String getReferringSchema() { + return referringSchema; + } + + public String getReferringTable() { + return referringTable; + } + + public OnDelete getOnDelete() { + return onDelete; + } + + public OnUpdate getOnUpdate() { + return onUpdate; + } + + public String getName() { + return "fk_" +// + referringSchema + "_" + referringTable + "_" + + String.join("_", links.stream().map(Link::columnInReferringTable).toList()) + + "_to_" +// + referencedSchema + "_" + referencedTable + "_" + + String.join("_", links.stream().map(Link::columnInReferencedTable).toList()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ForeignKey that)) return false; + return Objects.equals(onDelete, that.onDelete) && + Objects.equals(onUpdate, that.onUpdate) && + Objects.equals(links, that.links) && + 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, 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 + + '}'; + } + +} diff --git a/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java new file mode 100644 index 00000000..90b59f2e --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/parse/SQLBuilder.java @@ -0,0 +1,696 @@ +package net.staticstudios.data.parse; + +import com.google.common.base.Preconditions; +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; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +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); + private final Map parsedSchemas; + private final DataManager dataManager; + + public SQLBuilder(DataManager dataManager) { + this.dataManager = dataManager; + this.parsedSchemas = new HashMap<>(); + } + + public static void parseLinks(ForeignKey foreignKey, String links) { + for (Link link : parseLinks(links)) { + foreignKey.addLink(link); + } + } + + public static void parseLinksReversed(ForeignKey foreignKey, String links) { + for (Link link : parseLinksReversed(links)) { + foreignKey.addLink(link); + } + } + + 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 (Link rawLink : Link.parseRawLinks(links)) { + mappings.add(new Link(ValueUtils.parseValue(rawLink.columnInReferencedTable()), ValueUtils.parseValue(rawLink.columnInReferringTable()))); + } + return mappings; + } + + 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<>(); + + // 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); + } + for (Class visitedClass : visited) { + parseIndividualRelations(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) { + newTable.setSchema(existingSchema); + 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 referringTable " + newTable.getName() + " has conflicting definitions! Existing: " + existingColumn + ", New: " + newColumn); + continue; + } + newColumn.setTable(existingTable); + existingTable.addColumn(newColumn); + } + } + } + + return getDefs(schemas.values()); + } + + public @Nullable SQLSchema getSchema(String name) { + return parsedSchemas.get(name); + } + + 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 h2Sb; + StringBuilder pgSb; + for (SQLTable table : schema.getTables()) { + 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"); + boolean skipPKDef = false; + for (ColumnMetadata idColumn : table.getIdColumns()) { + 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"); + } + } + 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.append(");"); + pgSb.append(");"); + statements.add(DDLStatement.of(h2Sb.toString(), pgSb.toString())); + if (!table.getColumns().isEmpty()) { + for (SQLColumn column : table.getColumns()) { + 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()) { + h2Sb.append(" NOT NULL"); + pgSb.append(" NOT NULL"); + } + if (column.getDefaultValue() != null) { + h2Sb.append(" DEFAULT ").append(column.getDefaultValue()); + pgSb.append(" DEFAULT ").append(column.getDefaultValue()); + } + + if (column.isUnique()) { + h2Sb.append(" UNIQUE"); + pgSb.append(" UNIQUE"); + } + + h2Sb.append(";"); + pgSb.append(";"); + statements.add(DDLStatement.of(h2Sb.toString(), pgSb.toString())); + } + } + } + } + + 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 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()) { + if (foreignKey == null) { + continue; + } + 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(" "); + sb.append("FOREIGN KEY ("); + 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 (Link link : foreignKey.getLinkingColumns()) { + sb.append("\"").append(link.columnInReferencedTable()).append("\", "); + } + sb.setLength(sb.length() - 2); + sb.append(") ON DELETE ").append(foreignKey.getOnDelete()).append(" ON UPDATE ").append(foreignKey.getOnUpdate()).append(";"); + 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(foreignKey.getReferringTable()).append("' AND constraint_schema = '").append(foreignKey.getReferringSchema()).append("' AND constraint_type = 'FOREIGN KEY') THEN "); + + sb.append("ALTER TABLE \"").append(foreignKey.getReferringSchema()).append("\".\"").append(foreignKey.getReferringTable()).append("\" "); + sb.append("ADD CONSTRAINT ").append(fKeyName).append(" "); + sb.append("FOREIGN KEY ("); + 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 (Link link : foreignKey.getLinkingColumns()) { + sb.append("\"").append(link.columnInReferencedTable()).append("\", "); + } + sb.setLength(sb.length() - 2); + 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)); + } + } + } + for (SQLSchema schema : schemas) { + for (SQLTable table : schema.getTables()) { + for (SQLTrigger trigger : table.getTriggers()) { + statements.add(DDLStatement.of(trigger.getH2SQL(), trigger.getPgSQL())); + } + } + } + + 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 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, Relation.class)) { + Class genericType = ReflectionUtils.getGenericType(field); + if (genericType == null || !UniqueData.class.isAssignableFrom(genericType) || Modifier.isAbstract(genericType.getModifiers())) { + continue; + } + Class related = genericType.asSubclass(UniqueData.class); + walk(related, visited); + } + } + + private void parseIndividualColumns(Class clazz, Map schemas) { + 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"); + } + + Data dataAnnotation = clazz.getAnnotation(Data.class); + Preconditions.checkNotNull(dataAnnotation, "Data annotation is null for class " + clazz.getName()); + + for (Field field : ReflectionUtils.getFields(clazz)) { + parseColumn(clazz, schemas, dataAnnotation, metadata, field); + } + } + + private void parseIndividualRelations(Class clazz, Map schemas) { + logger.trace("Parsing relations for class {}", clazz.getName()); + UniqueDataMetadata metadata = dataManager.getMetadata(clazz); + 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)) { + Class genericType = ReflectionUtils.getGenericType(field); + if (genericType != null && Modifier.isAbstract(genericType.getModifiers())) { + continue; + } + parseReference(clazz, schemas, dataAnnotation, metadata, field); + parsePersistentCollection(clazz, schemas, dataAnnotation, metadata, field); + } + } + + private void parseColumn(Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { + if (!field.getType().equals(PersistentValue.class)) { + return; + } + + IdColumn idColumn = field.getAnnotation(IdColumn.class); + Column columnAnnotation = field.getAnnotation(Column.class); + ForeignColumn foreignColumn = field.getAnnotation(ForeignColumn.class); + DefaultValue defaultValueAnnotation = field.getAnnotation(DefaultValue.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 = defaultValueAnnotation != null ? ValueUtils.parseValue(defaultValueAnnotation.value()) : ""; + 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(dataAnnotation.schema()); + tableName = ValueUtils.parseValue(dataAnnotation.table()); + columnName = ValueUtils.parseValue(columnAnnotation.name()); + nullable = columnAnnotation.nullable(); + indexed = columnAnnotation.index(); + unique = columnAnnotation.unique(); + } 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; + } else { + return; + } + + String dataSchema = ValueUtils.parseValue(dataAnnotation.schema()); + String dataTable = ValueUtils.parseValue(dataAnnotation.table()); + + schemaName = schemaName.isEmpty() ? dataSchema : schemaName; + tableName = tableName.isEmpty() ? dataTable : tableName; + + if (foreignColumn != null) { + Preconditions.checkArgument(!(schemaName.equals(dataSchema) && tableName.equals(dataTable)), "ForeignColumn field %s in class %s cannot reference its own referringTable", 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, "")); + } + } + + table = new SQLTable(schema, tableName, idColumns); + schema.addTable(table); + + if (foreignColumn != null) { + for (ColumnMetadata idCol : table.getIdColumns()) { + Preconditions.checkState(table.getColumn(idCol.name()) == null, "ID column name " + idCol.name() + " in referringTable " + tableName + " is duplicated!"); + SQLColumn sqlColumn = new SQLColumn(table, idCol.type(), idCol.name(), false, false, true, null); + table.addColumn(sqlColumn); + } + } + } else if (foreignColumn != null) { + List links = parseLinks(foreignColumn.link()); + for (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()); + } + } + + 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 referencedSchema = ValueUtils.parseValue(foreignColumn.schema()); + if (referencedSchema.isEmpty()) { + referencedSchema = schemaName; + } + String referencedTable = ValueUtils.parseValue(foreignColumn.table()); + if (referencedTable.isEmpty()) { + referencedTable = tableName; + } + + Preconditions.checkArgument(!(referencedSchema.equals(dataSchema) && referencedTable.equals(dataTable)), "ForeignColumn field %s in class %s cannot reference its own referringTable", field.getName(), clazz.getName()); + + ForeignKey foreignKey = new ForeignKey(dataSchema, dataTable, referencedSchema, referencedTable, OnDelete.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.addForeignKey(foreignKey); + + Delete delete = field.getAnnotation(Delete.class); + DeleteStrategy deleteStrategy = delete != null ? delete.value() : DeleteStrategy.NO_ACTION; + dataSqlTable.addTrigger(new SQLDeleteStrategyTrigger(dataSchema, dataTable, referencedSchema, referencedTable, deleteStrategy, foreignKey.getLinkingColumns())); + } + + 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); + if (existingColumn != null) { + Preconditions.checkState(existingColumn.equals(sqlColumn), "Column " + columnName + " in referringTable " + 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); + Preconditions.checkNotNull(oneToOne, "Reference field " + field.getName() + " in class " + clazz.getName() + " is not annotated with @OneToOne"); + + 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); + } + + if (!oneToOne.fkey()) { + return; + } + + 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; + ForeignKey foreignKey = new ForeignKey(schema.getName(), table.getName(), referencedSchema.getName(), referencedTable.getName(), onDelete); + try { + parseLinks(foreignKey, oneToOne.link()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Error parsing @OneToOne link on field " + field.getName() + " in class " + clazz.getName() + ": " + e.getMessage(), e); + } + table.addForeignKey(foreignKey); + + table.addTrigger(new SQLDeleteStrategyTrigger(dataSchema, dataTable, referencedSchema.getName(), referencedTable.getName(), deleteStrategy, foreignKey.getLinkingColumns())); + } + + 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 void parseOneToManyValuePersistentCollection(OneToMany oneToMany, Class genericType, Class clazz, Map schemas, Data dataAnnotation, UniqueDataMetadata metadata, Field field) { + 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); + for (ColumnMetadata idCol : referencedTable.getIdColumns()) { + Preconditions.checkState(referencedTable.getColumn(idCol.name()) == null, "ID column name " + idCol.name() + " in referringTable " + referencedTableName + " is duplicated!"); + SQLColumn sqlColumn = new SQLColumn(referencedTable, dataManager.getSerializedType(idCol.type()), idCol.name(), false, false, true, null); + referencedTable.addColumn(sqlColumn); + } + referencedSchema.addTable(referencedTable); + referencedTable.addColumn(new SQLColumn(referencedTable, dataManager.getSerializedType(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, dataManager.getSerializedType(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) { + 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 = Objects.requireNonNull(schemas.get(dataSchema)); + SQLTable table = Objects.requireNonNull(schema.getTable(dataTable)); + + 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 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(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); + } + referencedTable.addForeignKey(foreignKey); + } + + 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()); + + String dataSchema = ValueUtils.parseValue(dataAnnotation.schema()); + String dataTable = ValueUtils.parseValue(dataAnnotation.table()); + + SQLSchema schema = Objects.requireNonNull(schemas.get(dataSchema)); + SQLTable table = Objects.requireNonNull(schema.getTable(dataTable)); + + SQLSchema referencedSchema = Objects.requireNonNull(schemas.get(referencedMetadata.schema())); + SQLTable referencedTable = Objects.requireNonNull(referencedSchema.getTable(referencedMetadata.table())); + + String joinTableSchemaName = PersistentManyToManyCollectionImpl.getJoinTableSchema(ValueUtils.parseValue(manyToMany.joinTableSchema()), dataSchema); + String joinTableName = PersistentManyToManyCollectionImpl.getJoinTableName(ValueUtils.parseValue(manyToMany.joinTable()), dataTable, referencedMetadata.table()); + + List joinTableToDataTableLinks; + List joinTableToReferencedTableLinks; + + try { + 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) { + List joinTableIdColumns = new ArrayList<>(); + for (Link dataLink : joinTableToDataTableLinks) { + 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() + ", join table: " + joinTableName); + joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, dataTableColumnPrefix + "_" + foundColumn.getName(), foundColumn.getType(), false, false, "")); + } + for (Link referencedLink : joinTableToReferencedTableLinks) { + 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() + ", join table: " + joinTableName); + joinTableIdColumns.add(new ColumnMetadata(joinTableSchemaName, joinTableName, referencedTableColumnPrefix + "_" + foundColumn.getName(), foundColumn.getType(), false, false, "")); + } + joinTable = new SQLTable(joinSchema, joinTableName, joinTableIdColumns); + for (ColumnMetadata idCol : joinTable.getIdColumns()) { + Preconditions.checkState(joinTable.getColumn(idCol.name()) == null, "ID column name " + idCol.name() + " in referringTable " + joinTableName + " is duplicated!"); + SQLColumn sqlColumn = new SQLColumn(joinTable, dataManager.getSerializedType(idCol.type()), idCol.name(), false, false, true, null); + joinTable.addColumn(sqlColumn); + } + 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 referringTable, but depending on the delete strategy, we may or may not delete the referenced data. + // impl with a trigger? + + 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/core/src/main/java/net/staticstudios/data/parse/SQLColumn.java b/core/src/main/java/net/staticstudios/data/parse/SQLColumn.java new file mode 100644 index 00000000..d6055f10 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/parse/SQLColumn.java @@ -0,0 +1,87 @@ +package net.staticstudios.data.parse; + +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class SQLColumn { + private final 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; + this.type = type; + this.name = name; + this.nullable = nullable; + this.indexed = indexed; + this.unique = unique; + this.defaultValue = defaultValue; + } + + public void setTable(SQLTable table) { + this.table = table; + } + + public SQLTable getTable() { + return table; + } + + public Class getType() { + return type; + } + + public String getName() { + return name; + } + + public boolean isNullable() { + return nullable; + } + + 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, unique, defaultValue); + } + + @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 && + unique == other.unique && + Objects.equals(defaultValue, other.defaultValue) && + Objects.equals(type, other.type) && + Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "SQLColumn{" + + ", type=" + type + + ", name='" + name + '\'' + + ", nullable=" + nullable + + ", indexed=" + indexed + + ", unique=" + unique + + ", defaultValue='" + defaultValue + '\'' + + '}'; + } +} diff --git a/core/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java b/core/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java new file mode 100644 index 00000000..472623ce --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/parse/SQLDeleteStrategyTrigger.java @@ -0,0 +1,79 @@ +package net.staticstudios.data.parse; + +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; + +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/core/src/main/java/net/staticstudios/data/parse/SQLSchema.java b/core/src/main/java/net/staticstudios/data/parse/SQLSchema.java new file mode 100644 index 00000000..917f97a7 --- /dev/null +++ b/core/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 referringSchema"); + } + if (tables.containsKey(table.getName())) { + 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 new file mode 100644 index 00000000..077a8791 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/parse/SQLTable.java @@ -0,0 +1,104 @@ +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.*; + +public class SQLTable { + 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; + this.name = name; + this.idColumns = idColumns; + this.columns = new HashMap<>(); + this.foreignKeys = new HashSet<>(); + this.triggers = new HashSet<>(); + } + + public void setSchema(SQLSchema schema) { + this.schema = schema; + } + + 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 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()) && + fk.getName().equals(foreignKey.getName()) + ) + .findFirst() + .orElse(null); + if (existingKey != null && !Objects.equals(existingKey, 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); + } + 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() { + return idColumns; + } + + public void addColumn(SQLColumn column) { + if (column.getTable() != this) { + 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 referringTable " + name + " in referringSchema " + schema.getName() + " and is different from the one being added"); + } + + 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/core/src/main/java/net/staticstudios/data/parse/SQLTrigger.java b/core/src/main/java/net/staticstudios/data/parse/SQLTrigger.java new file mode 100644 index 00000000..d27b96d7 --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/primative/Primitive.java b/core/src/main/java/net/staticstudios/data/primative/Primitive.java new file mode 100644 index 00000000..22f426f4 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/primative/Primitive.java @@ -0,0 +1,62 @@ +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<@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, 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; + } + + public static PrimitiveBuilder builder(Class runtimeType) { + return new PrimitiveBuilder<>(runtimeType); + } + + public @Nullable T decode(@Nullable String value) { + if (value == null) { + return null; + } + return decoder.apply(value); + } + + public @Nullable String encode(@Nullable T value) { + if (value == null) { + return null; + } + 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; + } + + public String getPgSQLType() { + return pgSQLType; + } + + public Class getRuntimeType() { + return runtimeType; + } +} 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 56% rename from src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java rename to core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java index e40bed41..9effd493 100644 --- a/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java +++ b/core/src/main/java/net/staticstudios/data/primative/PrimitiveBuilder.java @@ -1,6 +1,7 @@ package net.staticstudios.data.primative; import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; import java.util.function.Function; @@ -9,14 +10,15 @@ public class PrimitiveBuilder { private final Class runtimeType; private Function decoder; private Function encoder; - private Boolean nullable; - private T defaultValue; + private Function copier; + 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; } @@ -27,32 +29,36 @@ 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 nullable(boolean nullable) { - this.nullable = nullable; + public PrimitiveBuilder copier(Function<@NotNull T, @NotNull T> copier) { + this.copier = copier; return this; } - public PrimitiveBuilder defaultValue(T defaultValue) { - this.defaultValue = defaultValue; + 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(copier, "Copier is null"); Preconditions.checkNotNull(consumer, "Consumer is null"); - Preconditions.checkNotNull(nullable, "Nullable flag is null"); - - if (!nullable) { - Preconditions.checkNotNull(defaultValue, "Default value 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, nullable, defaultValue); + Primitive primitive = new Primitive<>(runtimeType, decoder, encoder, copier, h2SQLType, pgSQLType); consumer.accept(primitive); return primitive; 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 52% rename from src/main/java/net/staticstudios/data/primative/Primitives.java rename to core/src/main/java/net/staticstudios/data/primative/Primitives.java index 0441ab08..be491ab9 100644 --- a/src/main/java/net/staticstudios/data/primative/Primitives.java +++ b/core/src/main/java/net/staticstudios/data/primative/Primitives.java @@ -1,6 +1,8 @@ package net.staticstudios.data.primative; +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; @@ -12,7 +14,6 @@ @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") @@ -21,117 +22,105 @@ public class Primitives { private static Map, Primitive> primitives; public static final Primitive STRING = Primitive.builder(String.class) - .nullable(true) + .h2SQLType("TEXT") + .pgSQLType("TEXT") .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) + .copier(s -> s) .build(Primitives::register); public static final Primitive INTEGER = Primitive.builder(Integer.class) - .nullable(false) - .defaultValue(0) + .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) - .nullable(false) - .defaultValue(0L) + .h2SQLType("BIGINT") + .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) - .nullable(false) - .defaultValue(0.0f) + .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) - .nullable(false) - .defaultValue(0.0) + .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) - .nullable(false) - .defaultValue(false) + .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) - .nullable(true) - .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) + .copier(uuid -> uuid) .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()); - }) + .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) - .nullable(true) - .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) + .copier(bytes -> { + byte[] copy = new byte[bytes.length]; + System.arraycopy(bytes, 0, copy, 0, bytes.length); + return copy; }) .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) { + 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); + return getPrimitive(type).decode(value); } - public static String encode(Object value) { + public static String encode(@Nullable Object value) { if (value == null) { return null; } - return getPrimitive(value.getClass()).unsafeEncode(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)); } private static void register(Primitive primitive) { 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..64b8bbd2 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryBuilder.java @@ -0,0 +1,98 @@ +package net.staticstudios.data.query; + +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() { + StringBuilder sb = new StringBuilder(); + 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("WHERE "); + + 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..467c1030 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/BaseQueryWhere.java @@ -0,0 +1,188 @@ +package net.staticstudios.data.query; + +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; +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 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)); + } + + 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 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)); + } + + protected void notInClause(String referringSchema, String referringTable, String column, Object[] in) { + setValueClause(new NotInClause(referringSchema, referringTable, column, in)); + } + + 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 new file mode 100644 index 00000000..ae87fb70 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/InnerJoin.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.query; + +import java.util.Arrays; +import java.util.Objects; + +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(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(referringSchema, referringTable, Arrays.hashCode(columnsInReferringTable), referencedSchema, referencedTable, Arrays.hashCode(columnsInReferencedTable)); + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/Query.java b/core/src/main/java/net/staticstudios/data/query/Query.java new file mode 100644 index 00000000..db5c1a9e --- /dev/null +++ b/core/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/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/QueryLike.java b/core/src/main/java/net/staticstudios/data/query/QueryLike.java new file mode 100644 index 00000000..31bc5a87 --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/query/clause/AndClause.java b/core/src/main/java/net/staticstudios/data/query/clause/AndClause.java new file mode 100644 index 00000000..2b2f0151 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/AndClause.java @@ -0,0 +1,13 @@ +package net.staticstudios.data.query.clause; + +import java.util.Collections; +import java.util.List; + +public class AndClause implements ConditionalClause { + + @Override + public List append(StringBuilder sb) { + sb.append(" AND "); + 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 new file mode 100644 index 00000000..51ef4ddf --- /dev/null +++ b/core/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 Object min; + private final Object max; + + public BetweenClause(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("\" BETWEEN ? AND ?"); + return List.of(min, max); + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/Clause.java b/core/src/main/java/net/staticstudios/data/query/clause/Clause.java new file mode 100644 index 00000000..91b2a950 --- /dev/null +++ b/core/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/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/EqualsClause.java b/core/src/main/java/net/staticstudios/data/query/clause/EqualsClause.java new file mode 100644 index 00000000..652dd2e5 --- /dev/null +++ b/core/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/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/GreaterThanClause.java b/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanClause.java new file mode 100644 index 00000000..1cdb4532 --- /dev/null +++ b/core/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 Object value; + + public GreaterThanClause(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/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java b/core/src/main/java/net/staticstudios/data/query/clause/GreaterThanOrEqualToClause.java new file mode 100644 index 00000000..0e4c24df --- /dev/null +++ b/core/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 Object value; + + public GreaterThanOrEqualToClause(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/core/src/main/java/net/staticstudios/data/query/clause/InClause.java b/core/src/main/java/net/staticstudios/data/query/clause/InClause.java new file mode 100644 index 00000000..8cdee4bc --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/InClause.java @@ -0,0 +1,30 @@ +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; + } + + @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/core/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java b/core/src/main/java/net/staticstudios/data/query/clause/LessThanClause.java new file mode 100644 index 00000000..18a4e30b --- /dev/null +++ b/core/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 Object value; + + public LessThanClause(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/core/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java b/core/src/main/java/net/staticstudios/data/query/clause/LessThanOrEqualToClause.java new file mode 100644 index 00000000..53508b29 --- /dev/null +++ b/core/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 Object value; + + public LessThanOrEqualToClause(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/core/src/main/java/net/staticstudios/data/query/clause/LikeClause.java b/core/src/main/java/net/staticstudios/data/query/clause/LikeClause.java new file mode 100644 index 00000000..f849dd2c --- /dev/null +++ b/core/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/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/NotEqualsClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotEqualsClause.java new file mode 100644 index 00000000..507423e3 --- /dev/null +++ b/core/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/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/main/java/net/staticstudios/data/query/clause/NotInClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotInClause.java new file mode 100644 index 00000000..dabfe001 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/NotInClause.java @@ -0,0 +1,30 @@ +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; + } + + @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/core/src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotLikeClause.java new file mode 100644 index 00000000..8914e818 --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/query/clause/NotNullClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NotNullClause.java new file mode 100644 index 00000000..8f09fae2 --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/query/clause/NullClause.java b/core/src/main/java/net/staticstudios/data/query/clause/NullClause.java new file mode 100644 index 00000000..19f118b6 --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/query/clause/OrClause.java b/core/src/main/java/net/staticstudios/data/query/clause/OrClause.java new file mode 100644 index 00000000..2bab731d --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/query/clause/OrClause.java @@ -0,0 +1,13 @@ +package net.staticstudios.data.query.clause; + +import java.util.Collections; +import java.util.List; + +public class OrClause implements ConditionalClause { + + @Override + public List append(StringBuilder sb) { + sb.append(" OR "); + return Collections.emptyList(); + } +} diff --git a/core/src/main/java/net/staticstudios/data/query/clause/ValueClause.java b/core/src/main/java/net/staticstudios/data/query/clause/ValueClause.java new file mode 100644 index 00000000..0cc3eef5 --- /dev/null +++ b/core/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/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/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/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/ColumnMetadata.java b/core/src/main/java/net/staticstudios/data/util/ColumnMetadata.java new file mode 100644 index 00000000..c4ef4f23 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ColumnMetadata.java @@ -0,0 +1,86 @@ +package net.staticstudios.data.util; + +import org.jetbrains.annotations.NotNull; + +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/ColumnValuePair.java b/core/src/main/java/net/staticstudios/data/util/ColumnValuePair.java new file mode 100644 index 00000000..6f2d16cf --- /dev/null +++ b/core/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[" + + "name=" + column + ", " + + "value=" + value + ']'; + } + +} diff --git a/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java b/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java new file mode 100644 index 00000000..502f04d3 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ColumnValuePairs.java @@ -0,0 +1,69 @@ +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); + } + + public boolean isEmpty() { + return pairs.length == 0; + } + + @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/ConnectionConsumer.java b/core/src/main/java/net/staticstudios/data/util/ConnectionConsumer.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ConnectionConsumer.java rename to core/src/main/java/net/staticstudios/data/util/ConnectionConsumer.java diff --git a/src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java b/core/src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java rename to core/src/main/java/net/staticstudios/data/util/ConnectionJedisConsumer.java diff --git a/src/main/java/net/staticstudios/data/util/DataSourceConfig.java b/core/src/main/java/net/staticstudios/data/util/DataSourceConfig.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/DataSourceConfig.java rename to core/src/main/java/net/staticstudios/data/util/DataSourceConfig.java diff --git a/core/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java b/core/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java new file mode 100644 index 00000000..680fc178 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/EnvironmentVariableAccessor.java @@ -0,0 +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/FieldInstancePair.java b/core/src/main/java/net/staticstudios/data/util/FieldInstancePair.java new file mode 100644 index 00000000..28690668 --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java b/core/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java new file mode 100644 index 00000000..55f486fd --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ForeignPersistentValueMetadata.java @@ -0,0 +1,42 @@ +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 ForeignPersistentValueMetadata extends PersistentValueMetadata { + private final List links; + + public ForeignPersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata, int updateInterval, List links) { + super(holderClass, columnMetadata, updateInterval); + 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/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..c12d5678 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/InsertStatement.java @@ -0,0 +1,323 @@ +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 LinkedHashSet<>(); + + 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(buildCycleErrorMessage(stack, current)); + } + 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; + } + + /** + * Reconstructs the exact path of the cycle from the recursion stack. + */ + private static String buildCycleErrorMessage(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<>(); + + 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 void satisfyDependencies(List existingStatements) { + Preconditions.checkState(dependencyRequirements != null, "Must call calculateRequiredDependencies() before checking for unmet dependencies."); + for (DependencyRequirement requirement : dependencyRequirements) { +// boolean satisfied = false; + for (InsertStatement existingStatement : existingStatements) { + if (requirement.isSatisfiedBy(existingStatement)) { +// satisfied = true; + dependantOn.add(existingStatement); + break; + } + } + + // 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); +// } + } + } + + 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(); + } + } + +} 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/OnDelete.java b/core/src/main/java/net/staticstudios/data/util/OnDelete.java new file mode 100644 index 00000000..119d2854 --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/util/OnUpdate.java b/core/src/main/java/net/staticstudios/data/util/OnUpdate.java new file mode 100644 index 00000000..98373748 --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java new file mode 100644 index 00000000..98eb27ad --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/PersistentCollectionMetadata.java @@ -0,0 +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 new file mode 100644 index 00000000..c9140e2f --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/PersistentManyToManyCollectionMetadata.java @@ -0,0 +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 holderClass; + private final Class referencedType; + private final String parsedJoinTableSchema; + private final String parsedJoinTableName; + private final String rawLinks; + private String joinTableSchema; + private String joinTableName; + private List joinTableToDataTableLinks; + private List joinTableToReferencedTableLinks; + + 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.rawLinks = rawLinks; + } + + public Class getReferencedType() { + return referencedType; + } + + public synchronized String getJoinTableSchema(DataManager dataManager) { + if (joinTableSchema == null) { + UniqueDataMetadata holderMetadata = dataManager.getMetadata(holderClass); + joinTableSchema = PersistentManyToManyCollectionImpl.getJoinTableSchema(parsedJoinTableSchema, holderMetadata.schema()); + } + return joinTableSchema; + } + + 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 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(referencedType, that.referencedType) && + Objects.equals(parsedJoinTableSchema, that.parsedJoinTableSchema) && + Objects.equals(parsedJoinTableName, that.parsedJoinTableName) && + Objects.equals(rawLinks, that.rawLinks); + } + + @Override + public int hashCode() { + return Objects.hash(referencedType, parsedJoinTableSchema, parsedJoinTableName, rawLinks); + } + + @Override + public String toString() { + return "PersistentManyToManyCollectionMetadata{" + + "dataType=" + referencedType + + ", parsedJoinTableSchema='" + parsedJoinTableSchema + '\'' + + ", parsedJoinTableName='" + parsedJoinTableName + '\'' + + ", 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 new file mode 100644 index 00000000..067518d7 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyCollectionMetadata.java @@ -0,0 +1,53 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.DataManager; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.utils.Link; + +import java.util.List; +import java.util.Objects; + +public class PersistentOneToManyCollectionMetadata implements PersistentCollectionMetadata { + private final Class holderClass; + private final DataManager dataManager; + private final Class referencedType; + private final List links; + + public PersistentOneToManyCollectionMetadata(DataManager dataManager, Class holderClass, Class referencedType, List links) { + this.dataManager = dataManager; + this.holderClass = holderClass; + this.referencedType = referencedType; + this.links = links; + } + + 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(referencedType, that.referencedType) && Objects.equals(links, that.links); + } + + @Override + public int hashCode() { + return Objects.hash(referencedType, links); + } + + @Override + public String toString() { + return "PersistentOneToManyCollectionMetadata[" + + "links=" + links + ']'; + } +} 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..71ccc2ff --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/PersistentOneToManyValueCollectionMetadata.java @@ -0,0 +1,77 @@ +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 holderClass, Class dataType, String dataSchema, String dataTable, String dataColumn, List links) { + this.holderClass = holderClass; + this.dataType = dataType; + this.dataSchema = dataSchema; + this.dataTable = dataTable; + this.dataColumn = dataColumn; + this.links = links; + } + + @Override + public Class getHolderClass() { + return holderClass; + } + + 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/main/java/net/staticstudios/data/util/PersistentValueMetadata.java b/core/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java new file mode 100644 index 00000000..b78b15fb --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/PersistentValueMetadata.java @@ -0,0 +1,50 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +import java.util.Objects; + +public class PersistentValueMetadata { + private final Class holderClass; + private final ColumnMetadata columnMetadata; + private final int updateInterval; + + public PersistentValueMetadata(Class holderClass, ColumnMetadata columnMetadata, int updateInterval) { + this.holderClass = holderClass; + this.columnMetadata = columnMetadata; + this.updateInterval = updateInterval; + } + + public String getSchema() { + return columnMetadata.schema(); + } + + public String getTable() { + return columnMetadata.table(); + } + + public String getColumn() { + return columnMetadata.name(); + } + + public ColumnMetadata getColumnMetadata() { + return columnMetadata; + } + + public int getUpdateInterval() { + return updateInterval; + } + + @Override + public int hashCode() { + return Objects.hash(holderClass, columnMetadata, updateInterval); + } + + @Override + 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) && updateInterval == that.updateInterval; + } +} diff --git a/src/main/java/net/staticstudios/data/util/PostgresUtils.java b/core/src/main/java/net/staticstudios/data/util/PostgresUtils.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/PostgresUtils.java rename to core/src/main/java/net/staticstudios/data/util/PostgresUtils.java 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/util/ReferenceMetadata.java b/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java new file mode 100644 index 00000000..f0cf6550 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ReferenceMetadata.java @@ -0,0 +1,10 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.utils.Link; + +import java.util.List; + +public record ReferenceMetadata(Class holderClass, Class referencedClass, + List links, boolean generateFkey) { +} 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/main/java/net/staticstudios/data/util/ReflectionUtils.java b/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java new file mode 100644 index 00000000..d58d96a8 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ReflectionUtils.java @@ -0,0 +1,75 @@ +package net.staticstudios.data.util; + +import org.jetbrains.annotations.Nullable; + +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; + } + + /** + * 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)); + 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) { + if (parameterizedType.getActualTypeArguments()[0] instanceof Class) { + return (Class) parameterizedType.getActualTypeArguments()[0]; + } + } + return null; + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/Relation.java b/core/src/main/java/net/staticstudios/data/util/Relation.java new file mode 100644 index 00000000..fb940c3b --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/Relation.java @@ -0,0 +1,4 @@ +package net.staticstudios.data.util; + +public interface Relation { +} diff --git a/core/src/main/java/net/staticstudios/data/util/SQLTransaction.java b/core/src/main/java/net/staticstudios/data/util/SQLTransaction.java new file mode 100644 index 00000000..be187127 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/SQLTransaction.java @@ -0,0 +1,85 @@ +package net.staticstudios.data.util; + +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/core/src/main/java/net/staticstudios/data/util/SQLUtils.java b/core/src/main/java/net/staticstudios/data/util/SQLUtils.java new file mode 100644 index 00000000..d6f4ab21 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/SQLUtils.java @@ -0,0 +1,29 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.primative.Primitives; + +public class SQLUtils { + public static String getH2SqlType(Class clazz) { + if (Primitives.isPrimitive(clazz)) { + return Primitives.getPrimitive(clazz).getH2SQLType(); + } + 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) { + if (clazz == String.class) { + return "'" + defaultValue.replace("'", "''") + "'"; + } + if (clazz == Boolean.class) { + return Boolean.parseBoolean(defaultValue) ? "TRUE" : "FALSE"; + } + return defaultValue; + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/SQlStatement.java b/core/src/main/java/net/staticstudios/data/util/SQlStatement.java new file mode 100644 index 00000000..b0a9324f --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/SQlStatement.java @@ -0,0 +1,27 @@ +package net.staticstudios.data.util; + +import java.util.List; + +public class SQlStatement { + private final String h2Sql; + private final String pgSql; + private final List values; + + public SQlStatement(String h2Sql, String pgSql, List values) { + this.h2Sql = h2Sql; + this.pgSql = pgSql; + this.values = values; + } + + public String getH2Sql() { + return h2Sql; + } + + public String getPgSql() { + return pgSql; + } + + public List getValues() { + return values; + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/SchemaTable.java b/core/src/main/java/net/staticstudios/data/util/SchemaTable.java new file mode 100644 index 00000000..7a667acf --- /dev/null +++ b/core/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/core/src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java b/core/src/main/java/net/staticstudios/data/util/SimpleColumnMetadata.java new file mode 100644 index 00000000..89a3d736 --- /dev/null +++ b/core/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/util/TaskQueue.java b/core/src/main/java/net/staticstudios/data/util/TaskQueue.java similarity index 91% rename from src/main/java/net/staticstudios/data/util/TaskQueue.java rename to core/src/main/java/net/staticstudios/data/util/TaskQueue.java index e81d618a..1afb4846 100644 --- a/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); } } } 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/main/java/net/staticstudios/data/util/UniqueDataMetadata.java b/core/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java new file mode 100644 index 00000000..04465c7e --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/UniqueDataMetadata.java @@ -0,0 +1,15 @@ +package net.staticstudios.data.util; + +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, + Map cachedValueMetadata, + Map persistentValueMetadata, + Map referenceMetadata, + Map persistentCollectionMetadata) { +} diff --git a/core/src/main/java/net/staticstudios/data/util/Value.java b/core/src/main/java/net/staticstudios/data/util/Value.java new file mode 100644 index 00000000..de52834c --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/Value.java @@ -0,0 +1,7 @@ +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/ValueUpdate.java b/core/src/main/java/net/staticstudios/data/util/ValueUpdate.java similarity index 100% rename from src/main/java/net/staticstudios/data/util/ValueUpdate.java rename to core/src/main/java/net/staticstudios/data/util/ValueUpdate.java diff --git a/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java new file mode 100644 index 00000000..80ff9cb2 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java @@ -0,0 +1,13 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; + +public interface ValueUpdateHandler { + + void handle(U holder, ValueUpdate update); + + @SuppressWarnings("unchecked") + default void unsafeHandle(UniqueData holder, Object oldValue, Object newValue) { + handle((U) holder, new ValueUpdate<>((T) oldValue, (T) newValue)); + } +} diff --git a/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java new file mode 100644 index 00000000..9215590f --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ValueUpdateHandlerWrapper.java @@ -0,0 +1,51 @@ +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) { + 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; + } + + 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/core/src/main/java/net/staticstudios/data/util/ValueUtils.java b/core/src/main/java/net/staticstudios/data/util/ValueUtils.java new file mode 100644 index 00000000..42d95165 --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/ValueUtils.java @@ -0,0 +1,31 @@ +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 void setValue(String key, String value) { + ENVIRONMENT_VARIABLE_ACCESSOR.set(key, value); + } + + 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(); + while (matcher.find()) { + String varName = matcher.group(1); + String value = ENVIRONMENT_VARIABLE_ACCESSOR.getEnv(varName); + 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(); + } +} 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..f25897d2 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/BatchInsertTest.java @@ -0,0 +1,139 @@ +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); + dataManager.finishLoading(); + + 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); + dataManager.finishLoading(); + + 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); + dataManager.finishLoading(); + + 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/CachedValueTest.java b/core/src/test/java/net/staticstudios/data/CachedValueTest.java new file mode 100644 index 00000000..98cd16f6 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/CachedValueTest.java @@ -0,0 +1,162 @@ +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; +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 { + + private Gson gson = new Gson(); + + @Test + 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") + .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); + dataManager.finishLoading(); + 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); + dataManager.finishLoading(); + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("john doe") + .insert(InsertMode.ASYNC); + + 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 + assertEquals(0, user.cooldownUpdates.get()); + + + user.onCooldown.set(true); + assertEquals(true, user.onCooldown.get()); + + assertEquals(1, user.cooldownUpdates.get()); + + + user.onCooldown.set(true); + assertEquals(true, user.onCooldown.get()); + + //didnt change, so no update + assertEquals(1, user.cooldownUpdates.get()); + + + user.onCooldown.set(false); + assertEquals(false, user.onCooldown.get()); + + assertEquals(2, user.cooldownUpdates.get()); + } + + @Test + 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") + .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); + user.cooldownUpdates.set(1); + waitForDataPropagation(); + assertEquals("true", gson.fromJson(jedis.get(onCooldownKey), RedisEncodedValue.class).value()); + assertEquals("1", gson.fromJson(jedis.get(cooldownUpdatesKey), RedisEncodedValue.class).value()); + + user.onCooldown.set(null); + user.cooldownUpdates.set(null); + waitForDataPropagation(); + assertNull(jedis.get(onCooldownKey)); + assertNull(jedis.get(cooldownUpdatesKey)); + + user.onCooldown.set(false); //fallback + 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, gson.toJson(new RedisEncodedValue(null, "true"))); + jedis.set(cooldownUpdatesKey, gson.toJson(new RedisEncodedValue(null, "5"))); + + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + MockUser user = MockUser.builder(dataManager) + .id(userId) + .name("john doe") + .insert(InsertMode.ASYNC); + + 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/CustomTypeTest.java b/core/src/test/java/net/staticstudios/data/CustomTypeTest.java new file mode 100644 index 00000000..4fd9bfc2 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/CustomTypeTest.java @@ -0,0 +1,406 @@ +package net.staticstudios.data; + +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.BooleanWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapper; +import net.staticstudios.data.mock.wrapper.bytearrayprimitive.ByteArrayWrapperDataClass; +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.DoubleWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapper; +import net.staticstudios.data.mock.wrapper.floatprimitive.FloatWrapperDataClass; +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.IntegerWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapper; +import net.staticstudios.data.mock.wrapper.longprimitive.LongWrapperDataClass; +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.StringWrapperValueSerializer; +import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapper; +import net.staticstudios.data.mock.wrapper.timestampprimitive.TimestampWrapperDataClass; +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.UUIDWrapperValueSerializer; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.ResultSet; +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); + dataManager.finishLoading(); + + MockAccount account = MockAccount.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); + dataManager.finishLoading(); + + MockAccount account = MockAccount.query(dataManager).where(w -> w.idIs(1)).findOne(); + assertNotNull(account); + assertEquals(settings, account.settings.get()); + 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 = StringWrapperDataClass.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 = IntegerWrapperDataClass.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 = LongWrapperDataClass.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 = FloatWrapperDataClass.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 = DoubleWrapperDataClass.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 = BooleanWrapperDataClass.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 = UUIDWrapperDataClass.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 = TimestampWrapperDataClass.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 = ByteArrayWrapperDataClass.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. specifically ensure the encode and decode functions are correct +} \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java new file mode 100644 index 00000000..2a5c634d --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/PersistentManyToManyCollectionTest.java @@ -0,0 +1,405 @@ +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.sql.SQLException; +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); + dataManager.finishLoading(); + UUID id = UUID.randomUUID(); + mockUser = MockUser.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 = MockUser.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 = MockUser.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()); + } + + @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); + + 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); + + assertEquals(5, user.friends.size()); + assertEquals(0, user.friendRemovals.get()); + + int i = 0; + for (MockUser friend : friends) { + user.friends.remove(friend); + + 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 new file mode 100644 index 00000000..416f6d69 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyCollectionTest.java @@ -0,0 +1,385 @@ +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.MockUserSession; +import net.staticstudios.utils.RandomUtils; +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.*; + +public class PersistentOneToManyCollectionTest extends DataTest { + private static final int SESSION_COUNT = 20; + + private MockUser mockUser; + private DataManager dataManager; + + @BeforeEach + public void setUp() { + dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + UUID id = UUID.randomUUID(); + mockUser = MockUser.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); + } + + private List createSessions(int count) { + List sessions = new ArrayList<>(); + for (int i = 0; i < count; i++) { + MockUserSession session = MockUserSession.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() { + List sessions = createSessions(SESSION_COUNT); + + 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\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(sessions.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + 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\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(sessions.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + 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\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(sessions.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + int size = mockUser.sessions.size(); + for (MockUserSession session : sessions) { + assertTrue(mockUser.sessions.remove(session)); + assertEquals(--size, mockUser.sessions.size()); + } + + for (MockUserSession session : sessions) { + assertFalse(mockUser.sessions.contains(session)); + } + + assertTrue(mockUser.sessions.isEmpty()); + + waitForDataPropagation(); + + 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 testRemoveAll() { + 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\" = ?")) { + preparedStatement.setObject(1, mockUser.id.get()); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + assertEquals(sessions.size(), TestUtils.getResultCount(resultSet)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + assertTrue(mockUser.sessions.removeAll(sessions)); + assertEquals(0, mockUser.sessions.size()); + + for (MockUserSession session : sessions) { + assertFalse(mockUser.sessions.contains(session)); + } + + assertTrue(mockUser.sessions.isEmpty()); + + waitForDataPropagation(); + + 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() { + List sessions = createSessions(SESSION_COUNT); + for (MockUserSession session : sessions) { + assertFalse(mockUser.sessions.contains(session)); + mockUser.sessions.add(session); + assertTrue(mockUser.sessions.contains(session)); + } + + 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 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[] typedArr = mockUser.sessions.toArray(new MockUserSession[0]); + assertEquals(SESSION_COUNT, typedArr.length); + for (MockUserSession s : typedArr) { + assertTrue(sessions.contains(s)); + } + } + + @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 = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("another user") + .insert(InsertMode.SYNC); + assertEquals(mockUser.sessions, mockUser.sessions); + assertFalse(mockUser.sessions.equals(anotherMockUser.sessions)); + + } + + @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); + + 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); + + assertEquals(5, user.sessions.size()); + assertEquals(0, user.sessionRemovals.get()); + + int i = 0; + for (MockUserSession session : sessions) { + user.sessions.remove(session); + + 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); + + 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); + + assertEquals(5, user.sessions.size()); + assertEquals(0, user.sessionRemovals.get()); + + int i = 0; + for (MockUserSession session : sessions) { + session.delete(); + + 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 new file mode 100644 index 00000000..b61f8338 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/PersistentOneToManyValueCollectionTest.java @@ -0,0 +1,420 @@ +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); + dataManager.finishLoading(); + 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)); + } + + @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); + int i = 0; + for (Integer number : numbers) { + user.favoriteNumbers.add(number); + assertEquals(++i, 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(); + + assertEquals(5, user.favoriteNumbers.size()); + assertEquals(0, user.favoriteNumberRemovals.get()); + + int i = 0; + for (Integer number : numbers) { + user.favoriteNumbers.remove(number); + + assertEquals(++i, user.favoriteNumberRemovals.get()); + } + } +} \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/PersistentValueTest.java b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java new file mode 100644 index 00000000..fd34ef90 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/PersistentValueTest.java @@ -0,0 +1,590 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +import net.staticstudios.data.misc.MockEnvironment; +import net.staticstudios.data.mock.user.MockUser; +import net.staticstudios.data.util.ColumnValuePair; +import 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; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +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()); + } + + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + for (UUID id : userIds) { + MockUser.builder(dataManager) + .id(id) + .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()); + assertNull(user.age.get()); + } + + waitForDataPropagation(); + MockEnvironment environment2 = createMockEnvironment(); + DataManager dataManager2 = environment2.dataManager(); + dataManager2.load(MockUser.class); + for (UUID id : userIds) { + MockUser user = dataManager2.getInstance(MockUser.class, ColumnValuePair.of("id", id)); + assertEquals("user " + id, user.name.get()); + assertNull(user.age.get()); + } + } + + @Test + 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) + .name("test user") + .nameUpdates(0) + .insert(InsertMode.SYNC); + + 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); + dataManager.finishLoading(); + 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); + dataManager.finishLoading(); + UUID id = UUID.randomUUID(); + MockUser mockUser = MockUser.builder(dataManager) + .id(id) + .name("test user") + .favoriteColor("blue") + .insert(InsertMode.SYNC); + + assertEquals("blue", mockUser.favoriteColor.get()); + + 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 + 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) + .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 + assertEquals(1, dataManager.getUpdateHandlers("public", "users", "name", MockUser.class).size()); + mockUser = null; // remove strong reference + System.gc(); + 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()); + assertEquals(0, mockUser.getNameUpdates()); + + mockUser.name.set("new name"); + assertEquals(1, mockUser.getNameUpdates()); + mockUser.name.set("new name"); + assertEquals(1, mockUser.getNameUpdates()); + mockUser.name.set("new name2"); + assertEquals(2, mockUser.getNameUpdates()); + mockUser.name.set("new name"); + assertEquals(3, mockUser.getNameUpdates()); + } + + @Test + 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) + .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); + dataManager.finishLoading(); + 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.getInstance(MockUser.class, ColumnValuePair.of("id", id)); + + assertEquals("inserted from pg", mockUser.name.get()); + assertEquals(0, mockUser.getNameUpdates()); + } + + @Test + 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) + .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.getInstance(MockUser.class, ColumnValuePair.of("id", id)); + assertNull(mockUser); + } + + @Disabled //todo: known to break + @Test + 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) + .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"); + assertEquals(1, mockUser.nameUpdates.get()); + UUID newId = UUID.randomUUID(); + mockUser.id.set(newId); + assertEquals(newId, mockUser.id.get()); + 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"); + 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 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) + .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"); + + + 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.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"); + assertEquals(2, mockUser.nameUpdates.get()); + } + + @Test + 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) + .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")); + } + } + + @Test + 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 (?, ?)")) { + preparedStatement.setObject(1, id); + preparedStatement.setString(2, "blue"); + preparedStatement.executeUpdate(); + } + + MockUser user1 = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .favoriteColor("red") + .insert(InsertMode.SYNC); + assertEquals("red", user1.favoriteColor.get()); + MockUser user2 = MockUser.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); + dataManager.finishLoading(); + 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 = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .nameUpdates(10) + .insert(InsertMode.SYNC); + assertEquals(10, user1.nameUpdates.get()); + MockUser user2 = MockUser.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); + dataManager.finishLoading(); + Connection h2Connection = getH2Connection(dataManager); + Connection pgConnection = getConnection(); + UUID id = UUID.randomUUID(); + MockUser user = MockUser.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); + dataManager.finishLoading(); + Connection h2Connection = getH2Connection(dataManager); + Connection pgConnection = getConnection(); + UUID id = UUID.randomUUID(); + MockUser user = MockUser.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/core/src/test/java/net/staticstudios/data/PrimitivesTest.java b/core/src/test/java/net/staticstudios/data/PrimitivesTest.java new file mode 100644 index 00000000..c21001c8 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/QueryTest.java b/core/src/test/java/net/staticstudios/data/QueryTest.java new file mode 100644 index 00000000..d9db18cf --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/QueryTest.java @@ -0,0 +1,237 @@ +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.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); + dataManager.finishLoading(); + UUID id = UUID.randomUUID(); + MockUser original = MockUser.builder(dataManager) + .id(id) + .name("test user") + .insert(InsertMode.SYNC); + + MockUser got = MockUser.query(dataManager).where(w -> w.idIs(id)) + .findOne(); + assertSame(original, got); + } + + @Test + public void testFindAllLike() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + + MockUser original1 = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .age(0) + .insert(InsertMode.SYNC); + MockUser original2 = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user2") + .age(5) + .insert(InsertMode.SYNC); + + List got = MockUser.query(dataManager).where(w -> w.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 = MockUser.query(dataManager).where(w -> w.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); + dataManager.finishLoading(); + + MockUser likesRed = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("Likes Red") + .favoriteColor("red") + .insert(InsertMode.SYNC); + MockUser likesGreen = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("Likes Green") + .favoriteColor("green") + .insert(InsertMode.SYNC); + + assertNull(MockUser.query(dataManager).where(w -> w.favoriteColorIs("blue")) + .findOne()); + + assertSame(likesRed, MockUser.query(dataManager).where(w -> w.favoriteColorIs("red")) + .findOne()); + + assertSame(likesGreen, MockUser.query(dataManager).where(w -> w.favoriteColorIs("green")) + .findOne()); + + 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 = MockUser.query(dataManager).where(w -> w.favoriteColorIsNotNull()) + .orderByFavoriteColor(Order.DESCENDING) + .findAll(); + assertEquals(2, users.size()); + assertSame(likesRed, users.get(0)); + assertSame(likesGreen, users.get(1)); + } + + @Test + public void testEqualsClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + 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 ?", MockUser.query(dataManager).where(w -> w.ageIsBetween(0, 0)).toString()); + } + + @Test + public void testAgeIsLessThanClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + 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\" <= ?", MockUser.query(dataManager).where(w -> w.ageIsLessThanOrEqualTo(0)).toString()); + } + + @Test + public void testAgeIsGreaterThanClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + 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\" >= ?", 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", 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", MockUser.query(dataManager).where(w -> w.ageIsNotNull()).toString()); + } + + @Test + public void testNameIsLikeClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + 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 ?", MockUser.query(dataManager).where(w -> w.nameIsNotLike("%test%")).toString()); + } + + @Test + public void testNameIsInClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + 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 (?, ?, ?)", 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", 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", 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", MockUser.query(dataManager).where(w -> w.idIs(UUID.randomUUID())).orderByAge(Order.ASCENDING).toString()); + } + + @Test + public void testAndClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + 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 testOrClause() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + 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", + 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 new file mode 100644 index 00000000..b21c5c12 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/ReferenceTest.java @@ -0,0 +1,247 @@ +package net.staticstudios.data; + +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; +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 java.util.concurrent.CompletableFuture; + +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); + dataManager.finishLoading(); + + MockUserSettings settings = MockUserSettings.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + assertNotNull(settings); + } + + @Test + public void testCreateSettingsThenReference() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + + MockUserSettings settings = MockUserSettings.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + assertNotNull(settings); + + MockUser user = MockUser.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); + dataManager.finishLoading(); + + UUID settingsId = UUID.randomUUID(); + BatchInsert batch = dataManager.createBatchInsert(); + CompletableFuture settingsCf = MockUserSettings.builder(dataManager) + .id(settingsId) + .insert(batch); + + CompletableFuture userCf = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .settingsId(settingsId) + .insert(batch); + + batch.insert(InsertMode.SYNC); + MockUserSettings settings = settingsCf.join(); + MockUser user = userCf.join(); + + assertNotNull(settings); + assertNotNull(user); + assertSame(settings, user.settings.get()); + } + + @Test + public void testChangeReference() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + + MockUserSettings settings = MockUserSettings.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + assertNotNull(settings); + + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("test user") + .settingsId(settings.id.get()) + .insert(InsertMode.SYNC); + + assertNotNull(user); + assertSame(settings, user.settings.get()); + + MockUserSettings settings2 = MockUserSettings.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()); + } + + @Test + 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(); + MockUserSettings settings = MockUserSettings.builder(dataManager) + .id(id) + .insert(InsertMode.SYNC); + + MockUser user = MockUser.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()); + } + } + + @Test + public void testUpdateHandlerUpdate() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + + 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); + + assertEquals(1, user.settingsUpdates.get()); + + user.settings.set(settings); + + assertEquals(1, user.settingsUpdates.get()); + + user.settings.set(null); + + assertEquals(2, user.settingsUpdates.get()); + + user.settings.set(settings); + + assertEquals(3, user.settingsUpdates.get()); + + MockUserSettings settings2 = MockUserSettings.builder(dataManager) + .id(UUID.randomUUID()) + .insert(InsertMode.SYNC); + + user.settings.set(settings2); + + assertEquals(4, user.settingsUpdates.get()); + } + + @Test + public void testReferenceNoFkey() { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + + 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/SQLParseTest.java b/core/src/test/java/net/staticstudios/data/SQLParseTest.java new file mode 100644 index 00000000..1d312b3c --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/SQLParseTest.java @@ -0,0 +1,181 @@ +package net.staticstudios.data; + +import net.staticstudios.data.misc.DataTest; +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; +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; + +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.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; + +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; + }; + } + }; + } + + 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())); + } + + @Disabled("this test is so weird, it passes sometimes and fails other time.") + @Test + public void testParse() throws Exception { //todo: address flakiness + DataManager dm = getMockEnvironments().getFirst().dataManager(); + 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", + "--referringSchema-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 + ); + CREATE TABLE social_media.posts_metadata ( + 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_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_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_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_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; + """; + + 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 +} \ 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..d8f9b058 --- /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); + dataManager.finishLoading(); + + 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"); + + 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/ValueParseTest.java b/core/src/test/java/net/staticstudios/data/ValueParseTest.java new file mode 100644 index 00000000..ddc68a0e --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/misc/DataTest.java b/core/src/test/java/net/staticstudios/data/misc/DataTest.java new file mode 100644 index 00000000..7d654c9e --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/misc/DataTest.java @@ -0,0 +1,158 @@ +package net.staticstudios.data.misc; + +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.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.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.*; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +public class DataTest { + public static int NUM_ENVIRONMENTS = 1; + public static RedisContainer redis; + public static PostgreSQLContainer postgres = new PostgreSQLContainer<>( + "postgres:16.2" + ) + .withExposedPorts(5432) + .withPassword("password") + .withUsername("postgres") + .withDatabaseName("postgres"); + public static StaticDataConfig config; + 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"); + + 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(Runnable::run) + .build(); + + 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(config, false); + + MockEnvironment mockEnvironment = new MockEnvironment(config, dataManager); + mockEnvironments.add(mockEnvironment); + return mockEnvironment; + } + + @AfterEach + 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(); + } + + 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); + } + } + + 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/misc/MockEnvironment.java b/core/src/test/java/net/staticstudios/data/misc/MockEnvironment.java similarity index 61% rename from src/test/java/net/staticstudios/data/misc/MockEnvironment.java rename to core/src/test/java/net/staticstudios/data/misc/MockEnvironment.java index a5b37ed1..333e8f28 100644 --- a/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 ) { } 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 93% rename from src/test/java/net/staticstudios/data/misc/MockThreadProvider.java rename to core/src/test/java/net/staticstudios/data/misc/MockThreadProvider.java index b40bff74..6059b1bc 100644 --- a/src/test/java/net/staticstudios/data/misc/MockThreadProvider.java +++ b/core/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(); } diff --git a/core/src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java b/core/src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java new file mode 100644 index 00000000..50467070 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/misc/MultiEnvironmentTest.java @@ -0,0 +1,21 @@ +package net.staticstudios.data.misc; + +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/misc/TestUtils.java b/core/src/test/java/net/staticstudios/data/misc/TestUtils.java similarity index 100% rename from src/test/java/net/staticstudios/data/misc/TestUtils.java rename to core/src/test/java/net/staticstudios/data/misc/TestUtils.java diff --git a/core/src/test/java/net/staticstudios/data/mock/account/AccountDetails.java b/core/src/test/java/net/staticstudios/data/mock/account/AccountDetails.java new file mode 100644 index 00000000..e4b3bdfe --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java new file mode 100644 index 00000000..35f8ba5e --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/mock/account/AccountDetailsValueSerializer.java @@ -0,0 +1,35 @@ +package net.staticstudios.data.mock.account; + +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(@NotNull String serialized) { + if (serialized == null || serialized.isEmpty()) { + return null; + } + return GSON.fromJson(serialized, AccountDetails.class); + } + + @Override + public String serialize(@NotNull 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/core/src/test/java/net/staticstudios/data/mock/account/AccountSettings.java b/core/src/test/java/net/staticstudios/data/mock/account/AccountSettings.java new file mode 100644 index 00000000..8a63a906 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java new file mode 100644 index 00000000..eeb1f356 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/mock/account/AccountSettingsValueSerializer.java @@ -0,0 +1,35 @@ +package net.staticstudios.data.mock.account; + +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(@NotNull String serialized) { + if (serialized == null || serialized.isEmpty()) { + return null; + } + return GSON.fromJson(serialized, AccountSettings.class); + } + + @Override + public String serialize(@NotNull 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/core/src/test/java/net/staticstudios/data/mock/account/MockAccount.java b/core/src/test/java/net/staticstudios/data/mock/account/MockAccount.java new file mode 100644 index 00000000..305f4c51 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/post/MockPost.java b/core/src/test/java/net/staticstudios/data/mock/post/MockPost.java new file mode 100644 index 00000000..533b70ed --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/mock/post/MockPost.java @@ -0,0 +1,26 @@ +package net.staticstudios.data.mock.post; + +import net.staticstudios.data.*; + +/** + * Used to validate referringSchema generation. + */ +@Data(schema = "${POST_SCHEMA}", table = "${POST_TABLE}") +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", index = true) + public PersistentValue textContent; + @DefaultValue("0") + @Column(name = "likes") + public PersistentValue likes; + @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; +} 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 new file mode 100644 index 00000000..98fd3342 --- /dev/null +++ b/core/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 referringSchema 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; +} 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 new file mode 100644 index 00000000..559c9380 --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -0,0 +1,108 @@ +package net.staticstudios.data.mock.user; + +import net.staticstudios.data.*; + +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: 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); + + @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; + + @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 = 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") + @ForeignColumn(name = "name_updates", table = "user_metadata", link = "id=user_id") + public PersistentValue nameUpdates; + + @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; + + @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 = 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") + //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)); + + + @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 = 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); + @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(); + } +} diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUserSession.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUserSession.java new file mode 100644 index 00000000..ebdb3b1a --- /dev/null +++ b/core/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; +} diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java new file mode 100644 index 00000000..0e50f59a --- /dev/null +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUserSettings.java @@ -0,0 +1,14 @@ +package net.staticstudios.data.mock.user; + +import net.staticstudios.data.*; + +import java.util.UUID; + +@Data(schema = "public", table = "user_settings") +public class MockUserSettings extends UniqueData { + @IdColumn(name = "user_id") + public PersistentValue id; + @DefaultValue("10") + @Column(name = "font_size") + public PersistentValue fontSize; +} diff --git a/core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapper.java new file mode 100644 index 00000000..cbde13c7 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperDataClass.java new file mode 100644 index 00000000..0045d128 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/booleanprimitive/BooleanWrapperValueSerializer.java new file mode 100644 index 00000000..d3f01ab4 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapper.java new file mode 100644 index 00000000..85865ed0 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperDataClass.java new file mode 100644 index 00000000..e7a9afcd --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/bytearrayprimitive/ByteArrayWrapperValueSerializer.java new file mode 100644 index 00000000..a030a344 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapper.java new file mode 100644 index 00000000..bf6f7853 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperDataClass.java new file mode 100644 index 00000000..30d68cdf --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/doubleprimitive/DoubleWrapperValueSerializer.java new file mode 100644 index 00000000..372110e3 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapper.java new file mode 100644 index 00000000..aff0bae8 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperDataClass.java new file mode 100644 index 00000000..2d2dad73 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/floatprimitive/FloatWrapperValueSerializer.java new file mode 100644 index 00000000..8c0775c9 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapper.java new file mode 100644 index 00000000..113ac306 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperDataClass.java new file mode 100644 index 00000000..dad66f23 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/integerprimitive/IntegerWrapperValueSerializer.java new file mode 100644 index 00000000..52235850 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapper.java new file mode 100644 index 00000000..717843ff --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperDataClass.java new file mode 100644 index 00000000..507ef959 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/longprimitive/LongWrapperValueSerializer.java new file mode 100644 index 00000000..fd31f57d --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapper.java new file mode 100644 index 00000000..f6c3e403 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperDataClass.java new file mode 100644 index 00000000..58a94446 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/stringprimitive/StringWrapperValueSerializer.java new file mode 100644 index 00000000..68a7a858 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapper.java new file mode 100644 index 00000000..245d5ca1 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperDataClass.java new file mode 100644 index 00000000..846204fb --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/timestampprimitive/TimestampWrapperValueSerializer.java new file mode 100644 index 00000000..a93a113c --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapper.java new file mode 100644 index 00000000..e4e56c40 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperDataClass.java new file mode 100644 index 00000000..c9d33232 --- /dev/null +++ b/core/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/core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java b/core/src/test/java/net/staticstudios/data/mock/wrapper/uuidprimitive/UUIDWrapperValueSerializer.java new file mode 100644 index 00000000..bc81bcaf --- /dev/null +++ b/core/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; + } +} + diff --git a/src/test/resources/log4j.properties b/core/src/test/resources/log4j.properties similarity index 100% rename from src/test/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 249e5832..e6441136 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa3d32bb..2e111328 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Thu Oct 17 20:56:44 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..b740cf13 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# 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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /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..92790c1c --- /dev/null +++ b/intellij-plugin/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'org.jetbrains.intellij.platform' version '2.10.1' +} + +repositories { + mavenCentral() + + intellijPlatform { + defaultRepositories() + } +} + +dependencies { + implementation(project(":utils")) + intellijPlatform { + intellijIdeaCommunity('2025.2') + bundledPlugin("com.intellij.java") + } +} + + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} 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..a509dee2 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/DataPsiAugmentProvider.java @@ -0,0 +1,346 @@ +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 net.staticstudios.data.utils.Constants; +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 (!IntelliJPluginUtils.extendsClass(psiClass, Constants.UNIQUE_DATA_FQN)) { + return Collections.emptyList(); + } + + if (!IntelliJPluginUtils.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(getBuilderMethod2(psiClass)), type.cast(getQueryMethod(psiClass)), type.cast(getQueryMethod2(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 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); +// 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 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()) + .createType(builderClass, PsiSubstitutor.EMPTY); + for (PsiField psiField : parentClass.getAllFields()) { + PsiType type = psiField.getType(); + 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); + } + // 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 + } + + 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); + + 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; + } + + 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 (IntelliJPluginUtils.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 (!IntelliJPluginUtils.isValidPersistentValue(psiField) && !(isValidReference = IntelliJPluginUtils.isValidReference(psiField))) { + continue; //non-supported field type + } + if (!(type instanceof PsiClassType psiClassType)) { + continue; + } + PsiType innerType = IntelliJPluginUtils.getGenericParameter(psiClassType, parentClass.getManager()); + + List clauses = QueryBuilderUtils.getClausesForType(psiField, isValidReference || IntelliJPluginUtils.isNullable(psiField, type)); + for (QueryClause clause : clauses) { + String methodName = clause.getMethodName(psiField.getName()); + SyntheticMethod queryMethod = new SyntheticMethod(parentClass, whereClass, methodName, whereType); + List parameterTypes = clause.getMethodParamTypes(parentClass.getManager(), innerType, queryMethod); + for (PsiParameter parameterType : parameterTypes) { + queryMethod.addParameter(parameterType); + } + 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/IntelliJPluginUtils.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/IntelliJPluginUtils.java new file mode 100644 index 00000000..a773bef7 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/IntelliJPluginUtils.java @@ -0,0 +1,99 @@ +package net.staticstudios.data.ide.intellij; + +import com.intellij.psi.*; +import net.staticstudios.data.utils.Constants; + +import java.util.Objects; + +public class IntelliJPluginUtils { + public static boolean genericTypeIs(PsiType type, String classFqn) { + if (!(type instanceof PsiClassType psiClassType)) { + 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; + } + 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; + } + PsiClass superClass = superType.resolve(); + if (superClass != null && extendsClass(superClass, classFqn)) { + 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 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 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/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..3b02ddc1 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/SyntheticMethod.java @@ -0,0 +1,116 @@ +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() { + for (PsiParameter parameter : getParameterList().getParameters()) { + if (parameter.isVarArgs()) { + return true; + } + } + 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/query/NumericClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/NumericClause.java new file mode 100644 index 00000000..2604625d --- /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.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(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 new file mode 100644 index 00000000..9f2d3a0b --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryBuilderUtils.java @@ -0,0 +1,88 @@ +package net.staticstudios.data.ide.intellij.query; + +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiType; +import net.staticstudios.data.ide.intellij.IntelliJPluginUtils; +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 IsInCollectionClause()); + pvClauses.add(new IsNotInCollectionClause()); + + pvClauses.add(new IsInArrayClause()); + pvClauses.add(new IsNotInArrayClause()); + + pvClauses.add(new IsNullClause()); + pvClauses.add(new IsNotNullClause()); + + 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()); + pvClauses.add(new IsLessThanOrEqualToClause()); + pvClauses.add(new IsBetweenClause()); + pvClauses.add(new IsNotBetweenClause()); + + referenceClauses = new ArrayList<>(); + //todo: supporting these clauses in the java-c plugin is more involved than pvs, so until those are implemented + // these will remain diables. at the time of writing this, uncommenting this will cause IJ to behave as expected. +// 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 (IntelliJPluginUtils.isValidPersistentValue(psiField)) { + List applicableClauses = new ArrayList<>(); + for (QueryClause clause : pvClauses) { + if (clause.matches(psiField, nullable)) { + applicableClauses.add(clause); + } + } + return applicableClauses; + } + if (IntelliJPluginUtils.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..9bcc5bf2 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/QueryClause.java @@ -0,0 +1,14 @@ +package net.staticstudios.data.ide.intellij.query; + +import com.intellij.psi.*; + +import java.util.List; + +public interface QueryClause { + + boolean matches(PsiField psiField, boolean nullable); + + String getMethodName(String fieldName); + + 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 new file mode 100644 index 00000000..b7506e26 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsBetweenClause.java @@ -0,0 +1,26 @@ +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; + +public class IsBetweenClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsBetween"; + } + + @Override + 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 new file mode 100644 index 00000000..f8dfaf0d --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsClause.java @@ -0,0 +1,25 @@ +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.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, 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 new file mode 100644 index 00000000..a5e31952 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanClause.java @@ -0,0 +1,23 @@ +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; + +public class IsGreaterThanClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsGreaterThan"; + } + + @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/IsGreaterThanOrEqualToClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanOrEqualToClause.java new file mode 100644 index 00000000..475ef02c --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsGreaterThanOrEqualToClause.java @@ -0,0 +1,23 @@ +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; + +public class IsGreaterThanOrEqualToClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsGreaterThanOrEqualTo"; + } + + @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/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/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 new file mode 100644 index 00000000..66d9a80c --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanClause.java @@ -0,0 +1,23 @@ +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; + +public class IsLessThanClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsLessThan"; + } + + @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/IsLessThanOrEqualToClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanOrEqualToClause.java new file mode 100644 index 00000000..0e55cd62 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLessThanOrEqualToClause.java @@ -0,0 +1,23 @@ +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; + +public class IsLessThanOrEqualToClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsLessThanOrEqualTo"; + } + + @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/IsLikeClause.java b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.java new file mode 100644 index 00000000..c50eb7c1 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsLikeClause.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 IsLikeClause 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 + "IsLike"; + } + + @Override + 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 new file mode 100644 index 00000000..d2f7ae16 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotBetweenClause.java @@ -0,0 +1,26 @@ +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; + +public class IsNotBetweenClause implements NumericClause { + + @Override + public String getMethodName(String fieldName) { + return fieldName + "IsNotBetween"; + } + + @Override + 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 new file mode 100644 index 00000000..1026bc20 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotClause.java @@ -0,0 +1,25 @@ +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.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, 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/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 new file mode 100644 index 00000000..e78f18fa --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotLikeClause.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 IsNotLikeClause 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 + "IsNotLike"; + } + + @Override + 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 new file mode 100644 index 00000000..f4e843cb --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNotNullClause.java @@ -0,0 +1,24 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.*; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +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, 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 new file mode 100644 index 00000000..c2a79c30 --- /dev/null +++ b/intellij-plugin/src/main/java/net/staticstudios/data/ide/intellij/query/clause/IsNullClause.java @@ -0,0 +1,24 @@ +package net.staticstudios.data.ide.intellij.query.clause; + +import com.intellij.psi.*; +import net.staticstudios.data.ide.intellij.query.QueryClause; + +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, PsiElement scope) { + return List.of(); + } +} diff --git a/intellij-plugin/src/main/resources/META-INF/plugin.xml b/intellij-plugin/src/main/resources/META-INF/plugin.xml new file mode 100644 index 00000000..a46e8026 --- /dev/null +++ b/intellij-plugin/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,17 @@ + + 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/processor/build.gradle b/processor/build.gradle new file mode 100644 index 00000000..fbd9042a --- /dev/null +++ b/processor/build.gradle @@ -0,0 +1,72 @@ +plugins { + id 'java' + id 'maven-publish' + id 'com.gradleup.shadow' version '8.3.3' +} + +dependencies { + implementation project(":utils") + implementation project(":annotations") + implementation 'org.jetbrains:annotations:24.0.1' + implementation("com.google.guava:guava:33.5.0-jre") +} + +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', + '--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' + ] +} + +build { + dependsOn(shadowJar) +} + +tasks.withType(GenerateModuleMetadata).configureEach { + enabled = false +} + +java { + withSourcesJar() + withJavadocJar() +} + + +publishing { + publications { + maven(MavenPublication) { + artifactId = 'static-data-processor' + artifact(tasks.shadowJar) { + classifier = null + } + 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' + } + } + } + } +} + diff --git a/processor/src/main/java/net/staticstudios/data/compiler/javac/Parent.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/Parent.java new file mode 100644 index 00000000..8f2fc87e --- /dev/null +++ b/processor/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/processor/src/main/java/net/staticstudios/data/compiler/javac/Permit.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/Permit.java new file mode 100644 index 00000000..42755031 --- /dev/null +++ b/processor/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/processor/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/ProcessorContext.java new file mode 100644 index 00000000..0f60d890 --- /dev/null +++ b/processor/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/processor/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/StaticDataProcessor.java new file mode 100644 index 00000000..bd196d01 --- /dev/null +++ b/processor/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 -> { //todo: if abstract, skip + 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/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/AbstractBuilderProcessor.java new file mode 100644 index 00000000..80c76c41 --- /dev/null +++ b/processor/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/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 new file mode 100644 index 00000000..b605c8c0 --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/BuilderProcessor.java @@ -0,0 +1,739 @@ +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.InsertStrategy; +import net.staticstudios.data.compiler.javac.ProcessorContext; +import net.staticstudios.data.utils.Link; + +import java.util.ArrayList; +import java.util.Collection; + +public class BuilderProcessor extends AbstractBuilderProcessor { + private final Collection persistentValues; + private final Collection references; + + public BuilderProcessor(ProcessorContext processorContext) { + super(processorContext, "Builder", "builder"); + this.persistentValues = processorContext.persistentValues(); + this.references = processorContext.references(); + } + + 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")); + } + + @Override + protected void process() { + for (ParsedPersistentValue pv : persistentValues) { + processValue(pv); + } + +// for (ParsedReference ref : references) { +// processReference(ref); +// } + + makeInsertContextMethod(persistentValues, references); + makeInsertModeMethod(); + makeInsertBatchMethod(); + } + + + private void processValue(ParsedPersistentValue pv) { + storeSchema(pv.getFieldName(), pv.getSchema()); + storeTable(pv.getFieldName(), pv.getTable()); + storeColumn(pv.getFieldName(), pv.getColumn()); + + createField(VarDef( + Modifiers(Flags.PRIVATE), + names.fromString(pv.getFieldName()), + chainDots(pv.getTypeFQNParts()), + Literal(TypeTag.BOT, null) + ), builderClassDecl); + + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString(pv.getFieldName()), + Ident(names.fromString(getBuilderClassName())), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString(pv.getFieldName()), + chainDots(pv.getTypeFQNParts()), + null + ) + ), + List.nil(), + Block(0, List.of( + Exec( + Assign( + Select( + Ident(names.fromString("this")), + names.fromString(pv.getFieldName()) + ), + Ident(names.fromString(pv.getFieldName())) + ) + ), + Return( + Ident(names.fromString("this")) + ) + )), + null + ), builderClassDecl); + + + if (pv instanceof ParsedForeignPersistentValue fpv) { + processFpv(fpv); + } + } + + // 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) { + + + 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), + 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<>(); + + for (ParsedPersistentValue pv : parsedPersistentValues) { + 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 = Select( + Ident(names.fromString("this")), + names.fromString(pv.getFieldName()) + ); + + InsertStrategy insertStrategy = null; + if (pv instanceof ParsedForeignPersistentValue foreignPv) { + insertStrategy = foreignPv.getInsertStrategy(); + } + + JCTree.JCExpression insertStatement = Apply( + List.nil(), + Select( + Ident(names.fromString("ctx")), + names.fromString("set") + ), + List.of( + schemaFieldAccess, + tableFieldAccess, + columnFieldAccess, + fieldAccess, + 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.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() + ); + } + + + // 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), + names.fromString("insert"), + TypeIdent(TypeTag.VOID), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("ctx"), + chainDots("net", "staticstudios", "data", "insert", "InsertContext"), + null + ) + ), + List.nil(), + Block(0, List.from(bodyStatements)), + null + ), builderClassDecl); + + } + + public void makeInsertModeMethod() { + createMethod(MethodDef( + Modifiers(Flags.PUBLIC | Flags.FINAL), + names.fromString("insert"), + Ident(dataClassDecl.name), + List.nil(), + List.of( + VarDef( + Modifiers(Flags.PARAMETER), + names.fromString("mode"), + chainDots("net", "staticstudios", "data", "InsertMode"), + null + ) + ), + List.nil(), + Block(0, List.of( + 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")) + ) + ) + ), + Return( + Apply( + List.nil(), + Select( + Apply( + List.nil(), + Select( + Ident(names.fromString("ctx")), + names.fromString("insert") + ), + List.of( + Ident(names.fromString("mode")) + ) + ), + names.fromString("get") + ), + List.of( + Select( + Ident(dataClassDecl.name), + names.fromString("class") + ) + ) + ) + ) + )), + null + ), builderClassDecl); + + } +} 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 new file mode 100644 index 00000000..856246c8 --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedForeignPersistentValue.java @@ -0,0 +1,40 @@ +package net.staticstudios.data.compiler.javac.javac; + +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 implements ParsedRelation { + private final InsertStrategy insertStrategy; + private final 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 InsertStrategy getInsertStrategy() { + return insertStrategy; + } + + public List getLinks() { + return links; + } + + @Override + public String toString() { + return "ParsedForeignPersistentValue{" + + "fieldName='" + getFieldName() + '\'' + + ", schema='" + getSchema() + '\'' + + ", table='" + getTable() + '\'' + + ", column='" + getColumn() + '\'' + + ", nullable=" + isNullable() + + ", type=" + getType() + + ", insertStrategy='" + insertStrategy + '\'' + + ", links=" + 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 new file mode 100644 index 00000000..4f01405b --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedPersistentValue.java @@ -0,0 +1,142 @@ +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 extends ParsedValue { + private final String schema; + private final String table; + private final String column; + private final boolean nullable; + + public ParsedPersistentValue(String fieldName, String schema, String table, String column, boolean nullable, TypeElement type) { + super(fieldName, type); + this.schema = schema; + this.table = table; + this.column = column; + this.nullable = nullable; + } + + 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 getSchema() { + return schema; + } + + public String getTable() { + return table; + } + + public String getColumn() { + return column; + } + + public boolean isNullable() { + return nullable; + } + + + 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/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 new file mode 100644 index 00000000..0cdca70b --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/ParsedReference.java @@ -0,0 +1,70 @@ +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 extends ParsedValue implements ParsedRelation { + private final List links; + + public ParsedReference(String fieldName, List links, TypeElement type) { + super(fieldName, type); + this.links = links; + } + + 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 List getLinks() { + return links; + } + + public String[] getTypeFQNParts() { + return type.getQualifiedName().toString().split("\\."); + } + + @Override + public String toString() { + return "ParsedReference{" + + "fieldName='" + fieldName + '\'' + + ", links=" + links + + ", type=" + type + + '}'; + } +} 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; + } +} 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 new file mode 100644 index 00000000..3f09284d --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/PositionedTreeMaker.java @@ -0,0 +1,185 @@ +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); + } + + public JCTree.JCLambda Lambda(List params, JCTree body) { + return treeMaker.at(pos).Lambda(params, body); + } + +} 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 new file mode 100644 index 00000000..2479ea59 --- /dev/null +++ b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/QueryBuilderProcessor.java @@ -0,0 +1,1191 @@ +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()); + + if (pv instanceof ParsedForeignPersistentValue 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); + + addIsIgnoreCaseMethod(pv, schemaFieldName, tableFieldName, columnFieldName); + addIsNotIgnoreCaseMethod(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 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), + 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/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/javac/SuperClass.java new file mode 100644 index 00000000..99697d6a --- /dev/null +++ b/processor/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/processor/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/util/SimpleField.java new file mode 100644 index 00000000..3bea4b7e --- /dev/null +++ b/processor/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/processor/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java b/processor/src/main/java/net/staticstudios/data/compiler/javac/util/TypeUtils.java new file mode 100644 index 00000000..c39dade8 --- /dev/null +++ b/processor/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/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..f01241d9 --- /dev/null +++ b/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +net.staticstudios.data.compiler.javac.StaticDataProcessor diff --git a/settings.gradle b/settings.gradle index 1f62a038..0bc6bd07 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,9 @@ rootProject.name = 'static-data' + +include 'benchmark' +include 'core' +include 'processor' +include 'intellij-plugin' +include 'utils' +include 'annotations' \ No newline at end of file diff --git a/src/main/java/net/staticstudios/data/CachedValue.java b/src/main/java/net/staticstudios/data/CachedValue.java deleted file mode 100644 index eb72eb8f..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/DataManager.java b/src/main/java/net/staticstudios/data/DataManager.java deleted file mode 100644 index a02b1483..00000000 --- a/src/main/java/net/staticstudios/data/DataManager.java +++ /dev/null @@ -1,1149 +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.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/main/java/net/staticstudios/data/PersistentCollection.java deleted file mode 100644 index d6ffdbbd..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/PersistentValue.java b/src/main/java/net/staticstudios/data/PersistentValue.java deleted file mode 100644 index 2e51d70e..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/Reference.java b/src/main/java/net/staticstudios/data/Reference.java deleted file mode 100644 index 43d609da..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/UniqueData.java b/src/main/java/net/staticstudios/data/UniqueData.java deleted file mode 100644 index e6dde3cc..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/Data.java b/src/main/java/net/staticstudios/data/data/Data.java deleted file mode 100644 index f579c107..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/DataHolder.java b/src/main/java/net/staticstudios/data/data/DataHolder.java deleted file mode 100644 index 252b9689..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/Deletable.java b/src/main/java/net/staticstudios/data/data/Deletable.java deleted file mode 100644 index 48dea729..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/InitialValue.java b/src/main/java/net/staticstudios/data/data/InitialValue.java deleted file mode 100644 index a6ee9b06..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/collection/CollectionEntry.java b/src/main/java/net/staticstudios/data/data/collection/CollectionEntry.java deleted file mode 100644 index f7132189..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java b/src/main/java/net/staticstudios/data/data/collection/CollectionEntryIdentifier.java deleted file mode 100644 index 87988712..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java b/src/main/java/net/staticstudios/data/data/collection/PersistentCollectionChangeHandler.java deleted file mode 100644 index 800d90f0..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java b/src/main/java/net/staticstudios/data/data/collection/PersistentManyToManyCollection.java deleted file mode 100644 index 29f0610d..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java b/src/main/java/net/staticstudios/data/data/collection/PersistentUniqueDataCollection.java deleted file mode 100644 index d96f2982..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/collection/PersistentValueCollection.java b/src/main/java/net/staticstudios/data/data/collection/PersistentValueCollection.java deleted file mode 100644 index 87fa8de7..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java b/src/main/java/net/staticstudios/data/data/collection/SimplePersistentCollection.java deleted file mode 100644 index 545d60ff..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/value/InitialCachedValue.java b/src/main/java/net/staticstudios/data/data/value/InitialCachedValue.java deleted file mode 100644 index d651fb82..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/value/InitialPersistentValue.java b/src/main/java/net/staticstudios/data/data/value/InitialPersistentValue.java deleted file mode 100644 index fdb70db0..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/data/value/Value.java b/src/main/java/net/staticstudios/data/data/value/Value.java deleted file mode 100644 index 925257ea..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/impl/CachedValueManager.java b/src/main/java/net/staticstudios/data/impl/CachedValueManager.java deleted file mode 100644 index b222ea91..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/impl/PersistentCollectionManager.java b/src/main/java/net/staticstudios/data/impl/PersistentCollectionManager.java deleted file mode 100644 index a7c27972..00000000 --- a/src/main/java/net/staticstudios/data/impl/PersistentCollectionManager.java +++ /dev/null @@ -1,1075 +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 net.staticstudios.data.util.*; -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/src/main/java/net/staticstudios/data/impl/PersistentValueManager.java b/src/main/java/net/staticstudios/data/impl/PersistentValueManager.java deleted file mode 100644 index f26300dd..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/key/CellKey.java b/src/main/java/net/staticstudios/data/key/CellKey.java deleted file mode 100644 index 10d320c9..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/key/CollectionKey.java b/src/main/java/net/staticstudios/data/key/CollectionKey.java deleted file mode 100644 index 5b4e4d12..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/key/DataKey.java b/src/main/java/net/staticstudios/data/key/DataKey.java deleted file mode 100644 index 5e1857c4..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/key/DatabaseKey.java b/src/main/java/net/staticstudios/data/key/DatabaseKey.java deleted file mode 100644 index 26501312..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/key/RedisKey.java b/src/main/java/net/staticstudios/data/key/RedisKey.java deleted file mode 100644 index 7cd61b5e..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/key/UniqueIdentifier.java b/src/main/java/net/staticstudios/data/key/UniqueIdentifier.java deleted file mode 100644 index d9f8f0d3..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/primative/Primitive.java b/src/main/java/net/staticstudios/data/primative/Primitive.java deleted file mode 100644 index 2634fd64..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/BatchInsert.java b/src/main/java/net/staticstudios/data/util/BatchInsert.java deleted file mode 100644 index c197c3d7..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/CacheEntry.java b/src/main/java/net/staticstudios/data/util/CacheEntry.java deleted file mode 100644 index 7697c9be..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/DataDoesNotExistException.java b/src/main/java/net/staticstudios/data/util/DataDoesNotExistException.java deleted file mode 100644 index deff9cf4..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/DeleteContext.java b/src/main/java/net/staticstudios/data/util/DeleteContext.java deleted file mode 100644 index 99c42819..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/DeletionStrategy.java b/src/main/java/net/staticstudios/data/util/DeletionStrategy.java deleted file mode 100644 index 87143962..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/InsertContext.java b/src/main/java/net/staticstudios/data/util/InsertContext.java deleted file mode 100644 index 0cda8116..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/JunctionTable.java b/src/main/java/net/staticstudios/data/util/JunctionTable.java deleted file mode 100644 index 9cb08520..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/ReflectionUtils.java b/src/main/java/net/staticstudios/data/util/ReflectionUtils.java deleted file mode 100644 index c9b1eba9..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/SQLLogger.java b/src/main/java/net/staticstudios/data/util/SQLLogger.java deleted file mode 100644 index 3035439c..00000000 --- a/src/main/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/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java b/src/main/java/net/staticstudios/data/util/ValueUpdateHandler.java deleted file mode 100644 index 043bbbbd..00000000 --- a/src/main/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/src/test/java/net/staticstudios/data/CachedValueTest.java b/src/test/java/net/staticstudios/data/CachedValueTest.java deleted file mode 100644 index 0780080c..00000000 --- a/src/test/java/net/staticstudios/data/CachedValueTest.java +++ /dev/null @@ -1,226 +0,0 @@ -package net.staticstudios.data; - -import net.staticstudios.data.key.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 key - ); - """); - } 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 key 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 key = new RedisKey("reddit", "users", "id", id, "status"); - jedis.set(key.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/src/test/java/net/staticstudios/data/DeletionTest.java b/src/test/java/net/staticstudios/data/DeletionTest.java deleted file mode 100644 index b136f222..00000000 --- a/src/test/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.key.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 key, - name text not null - ); - create table if not exists minecraft.user_meta ( - id uuid primary key, - account_creation timestamp not null - ); - create table if not exists minecraft.user_stats ( - id uuid primary key - ); - create table if not exists minecraft.servers ( - id uuid primary key, - name text not null - ); - create table if not exists minecraft.skins ( - id uuid primary key, - 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) - ); - create table if not exists minecraft.worlds ( - id uuid primary key, - 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/src/test/java/net/staticstudios/data/InsertionTest.java b/src/test/java/net/staticstudios/data/InsertionTest.java deleted file mode 100644 index ef37441f..00000000 --- a/src/test/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 key, - name text not null - ); - create table if not exists twitch.chat_messages ( - id uuid primary key, - 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/src/test/java/net/staticstudios/data/PersistentCollectionTest.java b/src/test/java/net/staticstudios/data/PersistentCollectionTest.java deleted file mode 100644 index e62414a8..00000000 --- a/src/test/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 key - ); - create table if not exists facebook.posts ( - id uuid primary key, - 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, - 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/src/test/java/net/staticstudios/data/PersistentValueTest.java b/src/test/java/net/staticstudios/data/PersistentValueTest.java deleted file mode 100644 index dba1cfe1..00000000 --- a/src/test/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 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); - } - } - - @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/test/java/net/staticstudios/data/PostgresListenerTest.java deleted file mode 100644 index 8ca80952..00000000 --- a/src/test/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 key, - 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/src/test/java/net/staticstudios/data/PrimitivesTest.java b/src/test/java/net/staticstudios/data/PrimitivesTest.java deleted file mode 100644 index 1a3784de..00000000 --- a/src/test/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 key, - value text - ); - - create table if not exists primitive.character_test ( - id uuid primary key, - value char(1) not null - ); - - create table if not exists primitive.byte_test ( - id uuid primary key, - value smallint not null - ); - - create table if not exists primitive.short_test ( - id uuid primary key, - value smallint not null - ); - - create table if not exists primitive.integer_test ( - id uuid primary key, - value integer not null - ); - - create table if not exists primitive.long_test ( - id uuid primary key, - value bigint not null - ); - - create table if not exists primitive.float_test ( - id uuid primary key, - value real not null - ); - - create table if not exists primitive.double_test ( - id uuid primary key, - value double precision not null - ); - - create table if not exists primitive.boolean_test ( - id uuid primary key, - value boolean not null - ); - - create table if not exists primitive.uuid_test ( - id uuid primary key, - value uuid - ); - - create table if not exists primitive.timestamp_test ( - id uuid primary key, - value timestamp - ); - - create table if not exists primitive.byte_array_test ( - id uuid primary key, - 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/src/test/java/net/staticstudios/data/ReferenceTest.java b/src/test/java/net/staticstudios/data/ReferenceTest.java deleted file mode 100644 index af73563e..00000000 --- a/src/test/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 key, - favorite_user_id uuid - ); - create table if not exists snapchat.user_meta ( - id uuid primary key, - update_called integer - ); - create table if not exists snapchat.user_settings ( - user_id uuid primary key, - 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/src/test/java/net/staticstudios/data/misc/DataTest.java b/src/test/java/net/staticstudios/data/misc/DataTest.java deleted file mode 100644 index 0891cc29..00000000 --- a/src/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/src/test/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java b/src/test/java/net/staticstudios/data/mock/cachedvalue/RedditUser.java deleted file mode 100644 index 1fb1c107..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/deletions/MinecraftServer.java b/src/test/java/net/staticstudios/data/mock/deletions/MinecraftServer.java deleted file mode 100644 index 306c2999..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java b/src/test/java/net/staticstudios/data/mock/deletions/MinecraftSkin.java deleted file mode 100644 index 86d55682..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java b/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserStatistics.java deleted file mode 100644 index c8593a9e..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java b/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithCascadeDeletionStrategy.java deleted file mode 100644 index 3acbcece..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java b/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithNoActionDeletionStrategy.java deleted file mode 100644 index 98401a32..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java b/src/test/java/net/staticstudios/data/mock/deletions/MinecraftUserWithUnlinkDeletionStrategy.java deleted file mode 100644 index ebb08ab8..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java b/src/test/java/net/staticstudios/data/mock/insertions/TwitchChatMessage.java deleted file mode 100644 index df44c3dc..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/insertions/TwitchUser.java b/src/test/java/net/staticstudios/data/mock/insertions/TwitchUser.java deleted file mode 100644 index 378dc205..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java b/src/test/java/net/staticstudios/data/mock/persistentcollection/FacebookPost.java deleted file mode 100644 index f050be7c..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java b/src/test/java/net/staticstudios/data/mock/persistentcollection/FacebookUser.java deleted file mode 100644 index ecde9d6e..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java b/src/test/java/net/staticstudios/data/mock/persistentvalue/DiscordUser.java deleted file mode 100644 index efd19698..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java b/src/test/java/net/staticstudios/data/mock/persistentvalue/DiscordUserSettings.java deleted file mode 100644 index 645eb2e8..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/BooleanPrimitiveTestObject.java deleted file mode 100644 index 10dc6f66..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/ByteArrayPrimitiveTestObject.java deleted file mode 100644 index 7fa01bc4..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/BytePrimitiveTestObject.java deleted file mode 100644 index 5821011e..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/CharacterPrimitiveTestObject.java deleted file mode 100644 index 43a162c0..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/DoublePrimitiveTestObject.java deleted file mode 100644 index 92852b3b..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/FloatPrimitiveTestObject.java deleted file mode 100644 index f6e6b5c2..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/IntegerPrimitiveTestObject.java deleted file mode 100644 index 1bf29c99..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/LongPrimitiveTestObject.java deleted file mode 100644 index dbf567d0..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/ShortPrimitiveTestObject.java deleted file mode 100644 index f2993ecf..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/StringPrimitiveTestObject.java deleted file mode 100644 index 0ccd310f..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/TimestampPrimitiveTestObject.java deleted file mode 100644 index 75201457..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java b/src/test/java/net/staticstudios/data/mock/primative/UUIDPrimitiveTestObject.java deleted file mode 100644 index 93644074..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/reference/SnapchatUser.java b/src/test/java/net/staticstudios/data/mock/reference/SnapchatUser.java deleted file mode 100644 index ebe58e45..00000000 --- a/src/test/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/src/test/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java b/src/test/java/net/staticstudios/data/mock/reference/SnapchatUserSettings.java deleted file mode 100644 index 935d6efa..00000000 --- a/src/test/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/utils/build.gradle b/utils/build.gradle new file mode 100644 index 00000000..d75eed74 --- /dev/null +++ b/utils/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' +} + + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.google.guava:guava:33.5.0-jre") +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} diff --git a/utils/src/main/java/net/staticstudios/data/utils/Constants.java b/utils/src/main/java/net/staticstudios/data/utils/Constants.java new file mode 100644 index 00000000..3338afc2 --- /dev/null +++ b/utils/src/main/java/net/staticstudios/data/utils/Constants.java @@ -0,0 +1,22 @@ +package net.staticstudios.data.utils; + +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"; + public static final String INSERT_ANNOTATION_FQN = "net.staticstudios.data.Insert"; + 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"; + +} diff --git a/utils/src/main/java/net/staticstudios/data/utils/Link.java b/utils/src/main/java/net/staticstudios/data/utils/Link.java new file mode 100644 index 00000000..79d17cdb --- /dev/null +++ b/utils/src/main/java/net/staticstudios/data/utils/Link.java @@ -0,0 +1,31 @@ +package net.staticstudios.data.utils; + +import com.google.common.base.Preconditions; + +import java.util.ArrayList; +import java.util.List; + +public record Link(String columnInReferencedTable, String columnInReferringTable) { + + public static 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/utils/src/main/java/net/staticstudios/data/utils/StringUtils.java b/utils/src/main/java/net/staticstudios/data/utils/StringUtils.java new file mode 100644 index 00000000..ed6b30cc --- /dev/null +++ b/utils/src/main/java/net/staticstudios/data/utils/StringUtils.java @@ -0,0 +1,16 @@ +package net.staticstudios.data.utils; + +import java.util.List; + +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); + } +}