From 5983c248b99bc5792ce96de2c713fcb340a8f641 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 13 Oct 2026 16:30:10 -0700 Subject: [PATCH 1/3] Add shared objects to provider share updates, served by Armeria. --- .../asset/SharedDataObjectEntity.java | 125 ++++++++++++ .../asset/SharedDataObjectStore.java | 191 ++++++++++++++++++ .../io/opensharing/runtime/OpenSharing.java | 7 +- .../io/opensharing/share/ShareResponse.java | 29 ++- .../io/opensharing/share/ShareService.java | 166 +++++++++++++-- .../java/io/opensharing/share/ShareStore.java | 59 +++--- .../opensharing/share/UpdateShareRequest.java | 34 +++- .../opensharing/share/ShareServiceTest.java | 5 +- .../opensharing/share/ShareAdminService.java | 17 +- .../share/ShareAdminServiceTest.java | 168 ++++++++++++++- .../share/ShareUpdateTransactionTest.java | 120 +++++++++++ 11 files changed, 863 insertions(+), 58 deletions(-) create mode 100644 server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectEntity.java create mode 100644 server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectStore.java create mode 100644 server/opensharing-server/src/test/java/io/opensharing/share/ShareUpdateTransactionTest.java diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectEntity.java b/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectEntity.java new file mode 100644 index 000000000..8ac9ebd47 --- /dev/null +++ b/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectEntity.java @@ -0,0 +1,125 @@ +package io.opensharing.asset; + +import io.opensharing.BaseEntity; +import io.opensharing.ObjectNames; +import io.opensharing.catalog.AssetType; +import io.opensharing.catalog.DataSourceFormat; +import io.opensharing.share.ShareEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +/** + * A catalog object included in a share under a recipient-visible alias. A share may include each + * catalog object once and use each alias once. {@code ShareStore} deletes them with their share. + */ +@Entity +@Table( + name = "os_shared_data_objects", + uniqueConstraints = { + @UniqueConstraint(name = "uk_shared_objects_source", columnNames = {"share_id", "name"}), + @UniqueConstraint( + name = "uk_shared_objects_alias", + columnNames = {"share_id", "shared_as_schema", "shared_as_table"}) + }) +public class SharedDataObjectEntity extends BaseEntity { + + /** The owning share. Lazy because callers already hold the share they query by. */ + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "share_id", nullable = false) + private ShareEntity share; + + /** The catalog's own stable id for the object, when the catalog reports one. */ + @Column(name = "source_asset_id", length = 255) + private String sourceAssetId; + + /** Full catalog name as the provider gave it, such as {@code main.sales.orders}. */ + @Column(nullable = false, length = 512) + private String name; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 32) + private AssetType type; + + /** Data source format for tables, such as Delta; null for schemas. */ + @Enumerated(EnumType.STRING) + @Column(name = "source_format", length = 32) + private DataSourceFormat sourceFormat; + + /** Lowercase schema name recipients see. */ + @Column(name = "shared_as_schema", nullable = false, length = 255) + private String sharedAsSchema; + + /** Empty for schema-level shares; otherwise the lowercase table (or other asset) name. */ + @Column(name = "shared_as_table", nullable = false, length = 255) + private String sharedAsTable; + + public ShareEntity getShare() { + return share; + } + + public void setShare(ShareEntity share) { + this.share = share; + } + + public String getSourceAssetId() { + return sourceAssetId; + } + + public void setSourceAssetId(String sourceAssetId) { + this.sourceAssetId = sourceAssetId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public AssetType getType() { + return type; + } + + public void setType(AssetType type) { + this.type = type; + } + + public DataSourceFormat getSourceFormat() { + return sourceFormat; + } + + public void setSourceFormat(DataSourceFormat sourceFormat) { + this.sourceFormat = sourceFormat; + } + + public String getSharedAsSchema() { + return sharedAsSchema; + } + + public void setSharedAsSchema(String sharedAsSchema) { + this.sharedAsSchema = ObjectNames.normalize(sharedAsSchema); + } + + public String getSharedAsTable() { + return sharedAsTable; + } + + public void setSharedAsTable(String sharedAsTable) { + this.sharedAsTable = sharedAsTable == null ? "" : ObjectNames.normalize(sharedAsTable); + } + + /** Recipient-visible alias: {@code schema} or {@code schema.table}. */ + public String getSharedAs() { + return sharedAsTable == null || sharedAsTable.isEmpty() + ? sharedAsSchema + : sharedAsSchema + "." + sharedAsTable; + } +} diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectStore.java b/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectStore.java new file mode 100644 index 000000000..2d474e0bc --- /dev/null +++ b/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectStore.java @@ -0,0 +1,191 @@ +package io.opensharing.asset; + +import io.opensharing.Transactions; +import io.opensharing.catalog.AssetType; +import io.opensharing.catalog.ResolvedAsset; +import io.opensharing.catalog.TableProperties; +import io.opensharing.http.ApiException; +import io.opensharing.share.ShareEntity; +import jakarta.persistence.EntityManager; +import java.util.List; +import java.util.Optional; + +/** + * Persists catalog objects included in a share. Callers resolve the object in the catalog and parse + * its alias first; the store only enforces the share's alias rules. Adds and removes run in the + * caller's transaction, so a batch of them commits or rolls back together. A schema-level row has + * an empty {@code shared_as_table}. + */ +public class SharedDataObjectStore { + + private final Transactions tx; + + public SharedDataObjectStore(Transactions tx) { + this.tx = tx; + } + + /** + * Includes an object in the share. Fails when the alias or the object is already in the share, + * when a table is added under an already included schema, or when a schema is added while tables + * under it are already included. + */ + public SharedDataObjectEntity add( + EntityManager em, + ShareEntity share, + String name, + AssetType type, + ResolvedAsset resolved, + String sharedAsSchema, + String sharedAsTable) { + if (findByAlias(em, share, sharedAsSchema, sharedAsTable).isPresent()) { + String alias = + sharedAsTable.isEmpty() ? sharedAsSchema : sharedAsSchema + "." + sharedAsTable; + throw ApiException.alreadyExists( + "alias '" + alias + "' already exists in share '" + share.getName() + "'"); + } + if (findByName(em, share, name).isPresent()) { + throw ApiException.alreadyExists( + "'" + name + "' is already included in share '" + share.getName() + "'"); + } + if (!sharedAsTable.isEmpty() && schemaIncluded(em, share, sharedAsSchema)) { + throw ApiException.conflict( + "schema '" + sharedAsSchema + "' is already included in share '" + share.getName() + "'"); + } + if (sharedAsTable.isEmpty() && tablesIncluded(em, share, sharedAsSchema)) { + throw ApiException.conflict( + "tables under schema '" + + sharedAsSchema + + "' are already included in share '" + + share.getName() + + "'"); + } + + SharedDataObjectEntity object = new SharedDataObjectEntity(); + object.setShare(em.getReference(ShareEntity.class, share.getId())); + object.setSourceAssetId(resolved.catalogAssetId()); + object.setName(name); + object.setType(type); + if (resolved.additionalProperties() instanceof TableProperties table) { + object.setSourceFormat(table.dataSourceFormat()); + } + object.setSharedAsSchema(sharedAsSchema); + object.setSharedAsTable(sharedAsTable); + em.persist(object); + return object; + } + + /** + * {@code SELECT * FROM os_shared_data_objects WHERE share_id = ? ORDER BY shared_as_schema, + * shared_as_table} + */ + public List list(ShareEntity share) { + return tx.inTransaction( + true, + em -> + em.createQuery( + "select o from SharedDataObjectEntity o where o.share.id = :shareId" + + " order by o.sharedAsSchema, o.sharedAsTable", + SharedDataObjectEntity.class) + .setParameter("shareId", share.getId()) + .getResultList()); + } + + /** Removes the object shared under the alias. Fails unless it exists with the given type. */ + public void removeByAlias( + EntityManager em, + ShareEntity share, + AssetType type, + String sharedAsSchema, + String sharedAsTable) { + String alias = sharedAsTable.isEmpty() ? sharedAsSchema : sharedAsSchema + "." + sharedAsTable; + SharedDataObjectEntity object = + findByAlias(em, share, sharedAsSchema, sharedAsTable) + .orElseThrow(() -> notIncluded(share, alias)); + requireType(object, type, share); + em.remove(object); + } + + /** Removes the object by catalog name. Fails unless it exists with the given type. */ + public void removeByName(EntityManager em, ShareEntity share, AssetType type, String name) { + SharedDataObjectEntity object = + findByName(em, share, name).orElseThrow(() -> notIncluded(share, name)); + requireType(object, type, share); + em.remove(object); + } + + /** {@code SELECT * FROM os_shared_data_objects WHERE share_id = ? AND name = ?} */ + private static Optional findByName( + EntityManager em, ShareEntity share, String name) { + return em.createQuery( + "select o from SharedDataObjectEntity o where o.share.id = :shareId and o.name = :name", + SharedDataObjectEntity.class) + .setParameter("shareId", share.getId()) + .setParameter("name", name) + .getResultStream() + .findFirst(); + } + + /** + * {@code SELECT * FROM os_shared_data_objects WHERE share_id = ? AND shared_as_schema = ? AND + * shared_as_table = ?} + */ + private static Optional findByAlias( + EntityManager em, ShareEntity share, String sharedAsSchema, String sharedAsTable) { + return em.createQuery( + "select o from SharedDataObjectEntity o where o.share.id = :shareId" + + " and o.sharedAsSchema = :schema and o.sharedAsTable = :table", + SharedDataObjectEntity.class) + .setParameter("shareId", share.getId()) + .setParameter("schema", sharedAsSchema) + .setParameter("table", sharedAsTable) + .getResultStream() + .findFirst(); + } + + /** + * {@code SELECT id FROM os_shared_data_objects WHERE share_id = ? AND shared_as_schema = ? AND + * type = 'SCHEMA' AND shared_as_table = '' LIMIT 1} + */ + private static boolean schemaIncluded(EntityManager em, ShareEntity share, String schema) { + return !em.createQuery( + "select o.id from SharedDataObjectEntity o where o.share.id = :shareId" + + " and o.sharedAsSchema = :schema and o.type = :type and o.sharedAsTable = ''", + String.class) + .setParameter("shareId", share.getId()) + .setParameter("schema", schema) + .setParameter("type", AssetType.SCHEMA) + .setMaxResults(1) + .getResultList() + .isEmpty(); + } + + /** + * {@code SELECT id FROM os_shared_data_objects WHERE share_id = ? AND shared_as_schema = ? AND + * type = 'TABLE' AND shared_as_table <> '' LIMIT 1} + */ + private static boolean tablesIncluded(EntityManager em, ShareEntity share, String schema) { + return !em.createQuery( + "select o.id from SharedDataObjectEntity o where o.share.id = :shareId" + + " and o.sharedAsSchema = :schema and o.type = :type and o.sharedAsTable <> ''", + String.class) + .setParameter("shareId", share.getId()) + .setParameter("schema", schema) + .setParameter("type", AssetType.TABLE) + .setMaxResults(1) + .getResultList() + .isEmpty(); + } + + // An object of another type under the same name or alias counts as not included. + private static void requireType( + SharedDataObjectEntity object, AssetType type, ShareEntity share) { + if (object.getType() != type) { + throw notIncluded(share, object.getSharedAs()); + } + } + + private static ApiException notIncluded(ShareEntity share, String object) { + return ApiException.notFound( + "'" + object + "' is not included in share '" + share.getName() + "'"); + } +} diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java b/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java index 9d88eedef..dd495b8a3 100644 --- a/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java +++ b/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java @@ -1,6 +1,8 @@ package io.opensharing.runtime; import io.opensharing.Transactions; +import io.opensharing.asset.SharedDataObjectEntity; +import io.opensharing.asset.SharedDataObjectStore; import io.opensharing.catalog.CatalogConnector; import io.opensharing.share.ShareEntity; import io.opensharing.share.ShareService; @@ -19,7 +21,8 @@ public final class OpenSharing { private OpenSharing(CatalogConnector catalog, Transactions tx) { this.catalog = catalog; - this.shares = new ShareService(catalog, new ShareStore(tx)); + this.shares = + new ShareService(catalog, tx, new ShareStore(tx), new SharedDataObjectStore(tx)); } public static Builder builder() { @@ -28,7 +31,7 @@ public static Builder builder() { /** The entity classes the host's {@link Transactions} must register. */ public static List> entities() { - return List.of(ShareEntity.class); + return List.of(ShareEntity.class, SharedDataObjectEntity.class); } public CatalogConnector catalog() { diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareResponse.java b/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareResponse.java index 67d1db2fb..d30f3593f 100644 --- a/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareResponse.java +++ b/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareResponse.java @@ -1,10 +1,14 @@ package io.opensharing.share; import com.fasterxml.jackson.annotation.JsonInclude; +import io.opensharing.asset.SharedDataObjectEntity; +import io.opensharing.catalog.AssetType; +import java.util.List; import java.util.Map; /** - * A share as the protocol reports it. Fields without a value are omitted: + * A share as the provider API reports it. Fields without a value are omitted, and {@code objects} + * is omitted unless requested: * *
{@code
  * {
@@ -12,7 +16,8 @@
  *   "name": "",
  *   "displayName": "",
  *   "comment": "",
- *   "properties": {"": ""}
+ *   "properties": {"": ""},
+ *   "objects": [{"name": "", "type": "", "sharedAs": ""}]
  * }
  * }
  */
@@ -22,14 +27,30 @@ public record ShareResponse(
     String name,
     String displayName,
     String comment,
-    Map properties) {
+    Map properties,
+    List objects) {
 
   public static ShareResponse from(ShareEntity share) {
+    return from(share, null);
+  }
+
+  public static ShareResponse from(ShareEntity share, List objects) {
     return new ShareResponse(
         share.getId(),
         share.getName(),
         share.getDisplayName(),
         share.getComment(),
-        Map.copyOf(share.getProperties()));
+        Map.copyOf(share.getProperties()),
+        objects == null
+            ? null
+            : objects.stream().map(SharedDataObjectResponse::from).toList());
+  }
+
+  /** A catalog asset included in a share, with the alias recipients see it under. */
+  public record SharedDataObjectResponse(String name, AssetType type, String sharedAs) {
+
+    static SharedDataObjectResponse from(SharedDataObjectEntity object) {
+      return new SharedDataObjectResponse(object.getName(), object.getType(), object.getSharedAs());
+    }
   }
 }
diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareService.java b/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareService.java
index fc1c4d70d..d8074cf4b 100644
--- a/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareService.java
+++ b/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareService.java
@@ -1,12 +1,21 @@
 package io.opensharing.share;
 
 import io.opensharing.ObjectNames;
+import io.opensharing.Transactions;
+import io.opensharing.asset.SharedDataObjectStore;
 import io.opensharing.auth.AuthContext;
 import io.opensharing.auth.Privilege;
 import io.opensharing.auth.UserContext;
+import io.opensharing.catalog.Asset;
+import io.opensharing.catalog.AssetType;
 import io.opensharing.catalog.CatalogConnector;
+import io.opensharing.catalog.ResolvedAsset;
+import io.opensharing.exception.CatalogException;
 import io.opensharing.http.ApiException;
 import io.opensharing.http.ListResponse;
+import jakarta.persistence.EntityManager;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -16,11 +25,19 @@
 public class ShareService {
 
   private final CatalogConnector catalog;
+  private final Transactions tx;
   private final ShareStore shares;
+  private final SharedDataObjectStore objects;
 
-  public ShareService(CatalogConnector catalog, ShareStore shares) {
+  public ShareService(
+      CatalogConnector catalog,
+      Transactions tx,
+      ShareStore shares,
+      SharedDataObjectStore objects) {
     this.catalog = catalog;
+    this.tx = tx;
     this.shares = shares;
+    this.objects = objects;
   }
 
   /** Creates a share owned by the caller, who needs CREATE_SHARE from the catalog. */
@@ -44,20 +61,62 @@ public ListResponse list() {
     return ListResponse.of(shares.list().stream().map(ShareResponse::from).toList());
   }
 
-  /** Gets a share by name in any case. */
-  public ShareResponse get(String share) {
-    return ShareResponse.from(shares.getOrThrow(share));
+  /** Gets a share by name in any case, with the objects in it when {@code includeSharedData}. */
+  public ShareResponse get(String share, boolean includeSharedData) {
+    ShareEntity entity = shares.getOrThrow(share);
+    return includeSharedData
+        ? ShareResponse.from(entity, objects.list(entity))
+        : ShareResponse.from(entity);
   }
 
-  /** Updates the fields set in {@code request}. Owner only. */
+  /**
+   * Removes objects, then adds objects, then updates the fields set in {@code request}, all in one
+   * transaction: either every change commits or none does. Owner only.
+   */
   public ShareResponse update(UserContext user, String share, UpdateShareRequest request) {
-    return ShareResponse.from(
-        shares.update(
-            user,
-            share,
-            request.displayName(),
-            request.comment(),
-            validateProperties(request.properties())));
+    shares.getOwnedOrThrow(share, user);
+    Map properties = validateProperties(request.properties());
+    List removals = new ArrayList<>();
+    List adds = new ArrayList<>();
+    for (UpdateShareRequest.Update update : request.updates()) {
+      if (update == null) {
+        throw ApiException.invalidParameter("updates must not contain null");
+      }
+      if (update.action() == null) {
+        throw ApiException.invalidParameter("updates.action is required");
+      }
+      if (update.dataObject() == null) {
+        throw ApiException.invalidParameter("updates.dataObject is required");
+      }
+      switch (update.action()) {
+        case ADD -> adds.add(update.dataObject());
+        case REMOVE -> removals.add(removal(update.dataObject()));
+      }
+    }
+    // Catalog calls finish before the transaction opens, so it holds no connection while waiting.
+    List additions = adds.stream().map(dataObject -> addition(user, dataObject)).toList();
+    ShareEntity updated =
+        tx.inTransaction(
+            false,
+            em -> {
+              ShareEntity entity = shares.getOwnedOrThrow(em, share, user);
+              for (Removal removal : removals) {
+                removal.apply(em, objects, entity);
+              }
+              for (Addition addition : additions) {
+                objects.add(
+                    em,
+                    entity,
+                    addition.name(),
+                    addition.type(),
+                    addition.resolved(),
+                    addition.alias().schema(),
+                    addition.alias().table());
+              }
+              shares.update(entity, request.displayName(), request.comment(), properties);
+              return entity;
+            });
+    return ShareResponse.from(updated);
   }
 
   /** Deletes the share. Owner only. */
@@ -65,6 +124,73 @@ public void delete(UserContext user, String share) {
     shares.delete(share, user);
   }
 
+  // Resolves the object in the catalog on behalf of the caller and checks the declared type.
+  private Addition addition(UserContext user, UpdateShareRequest.DataObject dataObject) {
+    String name = requireText(dataObject.name(), "dataObject.name");
+    if (name.length() > 512) {
+      throw ApiException.invalidParameter("dataObject.name must not exceed 512 characters");
+    }
+    AssetType type = requireType(dataObject.type());
+    Alias alias = parseAlias(dataObject.sharedAs(), type, name);
+    // The catalog is responsible for checking that the caller holds SELECT on the asset. A host
+    // that already checks it at its endpoint can skip that check in resolveAsset.
+    ResolvedAsset resolved = catalog.resolveAsset(new Asset(type, name), AuthContext.of(user));
+    if (resolved.type() != type) {
+      throw new CatalogException(
+          "catalog resolved '" + name + "' as " + resolved.type() + " instead of " + type);
+    }
+    return new Addition(name, type, resolved, alias);
+  }
+
+  // Removes by alias when sharedAs is given, otherwise by catalog name.
+  private static Removal removal(UpdateShareRequest.DataObject dataObject) {
+    AssetType type = requireType(dataObject.type());
+    if (dataObject.sharedAs() != null && !dataObject.sharedAs().isBlank()) {
+      return new Removal(type, parseAlias(dataObject.sharedAs(), type, null), null);
+    }
+    return new Removal(
+        type, null, requireText(dataObject.name(), "dataObject.name or dataObject.sharedAs"));
+  }
+
+  /**
+   * Lowercased {@code sharedAs}, or {@code name} if omitted; catalog prefix is dropped.
+   *
+   * 

{@code Main.Sales.Orders} (TABLE) → {@code sales} / {@code orders}. {@code Main.Sales} + * (SCHEMA) → {@code sales}. {@code Sales.Orders} (TABLE) → {@code sales} / {@code orders}. + */ + private static Alias parseAlias(String sharedAs, AssetType type, String name) { + String raw = sharedAs == null || sharedAs.isBlank() ? name : sharedAs; + if (raw == null || raw.isBlank()) { + throw ApiException.invalidParameter("dataObject.sharedAs is required"); + } + raw = ObjectNames.normalize(raw); + String[] parts = raw.split("\\.", -1); + if (type == AssetType.SCHEMA) { + return new Alias(ObjectNames.validateSchemaName(parts[parts.length - 1]), ""); + } + if (parts.length < 2) { + throw ApiException.invalidParameter( + "dataObject.sharedAs must have at least 2 dot-separated names"); + } + return new Alias( + ObjectNames.validateSchemaName(parts[parts.length - 2]), + ObjectNames.validateAssetName(parts[parts.length - 1])); + } + + private static AssetType requireType(AssetType type) { + if (type == null) { + throw ApiException.invalidParameter("dataObject.type is required"); + } + return type; + } + + private static String requireText(String value, String field) { + if (value == null || value.isBlank()) { + throw ApiException.invalidParameter(field + " is required"); + } + return value; + } + private static Map validateProperties(Map properties) { if (properties != null) { for (Map.Entry property : properties.entrySet()) { @@ -76,4 +202,20 @@ private static Map validateProperties(Map proper } return properties; } + + private record Alias(String schema, String table) {} + + private record Addition(String name, AssetType type, ResolvedAsset resolved, Alias alias) {} + + // Exactly one of alias and name is set. + private record Removal(AssetType type, Alias alias, String name) { + + void apply(EntityManager em, SharedDataObjectStore objects, ShareEntity share) { + if (alias != null) { + objects.removeByAlias(em, share, type, alias.schema(), alias.table()); + } else { + objects.removeByName(em, share, type, name); + } + } + } } diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareStore.java b/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareStore.java index 93210f470..7aa8fa097 100644 --- a/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareStore.java +++ b/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareStore.java @@ -12,9 +12,9 @@ import java.util.Optional; /** - * Storage for shares. Names are stored lowercase and looked up case-insensitively. Each method runs - * in its own transaction and returns shares with their properties loaded, so callers can read them - * after it ends. + * Storage for shares. Names are stored lowercase and looked up case-insensitively. Each public + * method runs in its own transaction and returns shares with their properties loaded, so callers + * can read them after it ends. */ public class ShareStore { @@ -51,28 +51,21 @@ public ShareEntity create( }); } - /** Only non-null fields are applied. Only the owner may update the share. */ - public ShareEntity update( - UserContext user, - String name, - String displayName, - String comment, - Map properties) { - return tx.inTransaction( - false, - em -> { - ShareEntity share = getOwnedOrThrow(em, name, user); - if (displayName != null) { - share.setDisplayName(displayName); - } - if (comment != null) { - share.setComment(comment); - } - if (properties != null) { - share.setProperties(properties); - } - return share; - }); + /** + * Applies the non-null fields to {@code share}, which must be loaded in the caller's transaction, + * such as by {@link #getOwnedOrThrow(EntityManager, String, UserContext)}. + */ + void update( + ShareEntity share, String displayName, String comment, Map properties) { + if (displayName != null) { + share.setDisplayName(displayName); + } + if (comment != null) { + share.setComment(comment); + } + if (properties != null) { + share.setProperties(properties); + } } /** Looks up a share by name in any case. */ @@ -99,12 +92,17 @@ public List list() { .getResultList()); } - /** Deletes a share. Only the owner may delete it. */ + /** Deletes a share and the objects in it. Only the owner may delete it. */ public void delete(String name, UserContext user) { tx.inTransaction( false, em -> { - em.remove(getOwnedOrThrow(em, name, user)); + ShareEntity share = getOwnedOrThrow(em, name, user); + // DELETE FROM os_shared_data_objects WHERE share_id = ? + em.createQuery("delete from SharedDataObjectEntity o where o.share.id = :shareId") + .setParameter("shareId", share.getId()) + .executeUpdate(); + em.remove(share); return null; }); } @@ -131,10 +129,11 @@ private static ShareEntity getOrThrow(EntityManager em, String name, LockModeTyp } /** - * Locks the row of a share the caller owns, so changes to one share run one at a time and each - * sees what the one before it wrote. + * Like {@link #getOwnedOrThrow(String, UserContext)}, in the caller's transaction. Locks the + * share's row, so changes to one share run one at a time and each sees what the one before it + * wrote. */ - private static ShareEntity getOwnedOrThrow(EntityManager em, String name, UserContext user) { + ShareEntity getOwnedOrThrow(EntityManager em, String name, UserContext user) { return checkOwner(getOrThrow(em, name, LockModeType.PESSIMISTIC_WRITE), user); } diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/share/UpdateShareRequest.java b/server/opensharing-server-core/src/main/java/io/opensharing/share/UpdateShareRequest.java index a1c4266fe..19e0b2df7 100644 --- a/server/opensharing-server-core/src/main/java/io/opensharing/share/UpdateShareRequest.java +++ b/server/opensharing-server-core/src/main/java/io/opensharing/share/UpdateShareRequest.java @@ -1,7 +1,37 @@ package io.opensharing.share; +import io.opensharing.catalog.AssetType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; -/** PATCH body for share metadata. Null fields are left unchanged. */ +/** + * PATCH body for share metadata and content changes. Null fields are left unchanged; {@code + * updates} adds or removes objects, applying every remove before any add. + */ public record UpdateShareRequest( - String displayName, String comment, Map properties) {} + String displayName, + String comment, + Map properties, + List updates) { + + public UpdateShareRequest { + updates = updates == null ? List.of() : Collections.unmodifiableList(new ArrayList<>(updates)); + } + + /** One object to add to or remove from the share. */ + public record Update(Action action, DataObject dataObject) {} + + /** + * A catalog object. {@code name} is the full catalog name; {@code sharedAs} is the alias + * recipients see, {@code schema} or {@code schema.table}, and defaults to {@code name} without + * its catalog. A remove needs {@code name} or {@code sharedAs}. + */ + public record DataObject(String name, AssetType type, String sharedAs) {} + + public enum Action { + ADD, + REMOVE + } +} diff --git a/server/opensharing-server-core/src/test/java/io/opensharing/share/ShareServiceTest.java b/server/opensharing-server-core/src/test/java/io/opensharing/share/ShareServiceTest.java index a74366482..b1070f99a 100644 --- a/server/opensharing-server-core/src/test/java/io/opensharing/share/ShareServiceTest.java +++ b/server/opensharing-server-core/src/test/java/io/opensharing/share/ShareServiceTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import io.opensharing.Transactions; +import io.opensharing.asset.SharedDataObjectStore; import io.opensharing.auth.Privilege; import io.opensharing.auth.UserContext; import io.opensharing.catalog.CatalogConnector; @@ -32,7 +33,9 @@ void createsAShareOnlyWithCreateShareFromTheCatalog() { (proxy, method, args) -> { throw new UnsupportedOperationException(); }); - ShareService shares = new ShareService(catalog, new ShareStore(unused)); + ShareService shares = + new ShareService( + catalog, unused, new ShareStore(unused), new SharedDataObjectStore(unused)); UserContext alice = UserContext.fromUserIdAndName("alice-id", "alice"); assertThrows( diff --git a/server/opensharing-server/src/main/java/io/opensharing/share/ShareAdminService.java b/server/opensharing-server/src/main/java/io/opensharing/share/ShareAdminService.java index 478f4dcec..00a26fa58 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/share/ShareAdminService.java +++ b/server/opensharing-server/src/main/java/io/opensharing/share/ShareAdminService.java @@ -4,6 +4,7 @@ import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.server.annotation.Blocking; +import com.linecorp.armeria.server.annotation.Default; import com.linecorp.armeria.server.annotation.Delete; import com.linecorp.armeria.server.annotation.Get; import com.linecorp.armeria.server.annotation.Param; @@ -41,13 +42,21 @@ public ListResponse list() { return shares.list(); } - /** {@code GET /shares/{share}}: gets a share by name in any case. */ + /** + * {@code GET /shares/{share}}: gets a share by name in any case, with the objects in it when + * {@code include_shared_data} is true. + */ @Get("/shares/{share}") - public ShareResponse get(@Param String share) { - return shares.get(share); + public ShareResponse get( + @Param String share, + @Param("include_shared_data") @Default("false") boolean includeSharedData) { + return shares.get(share, includeSharedData); } - /** {@code PATCH /shares/{share}}: updates the fields set in the body. Owner only. */ + /** + * {@code PATCH /shares/{share}}: applies the object removes, then the adds, in {@code updates}, + * then the other fields set in the body, all or nothing. Owner only. + */ @Patch("/shares/{share}") public ShareResponse update( ServiceRequestContext ctx, @Param String share, UpdateShareRequest request) { diff --git a/server/opensharing-server/src/test/java/io/opensharing/share/ShareAdminServiceTest.java b/server/opensharing-server/src/test/java/io/opensharing/share/ShareAdminServiceTest.java index c50bfb44b..325457c66 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/share/ShareAdminServiceTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/share/ShareAdminServiceTest.java @@ -2,7 +2,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.fasterxml.jackson.databind.JsonNode; import io.opensharing.TestServer; import io.opensharing.TestServer.Response; import io.opensharing.http.ErrorCodes; @@ -139,18 +141,63 @@ void createsListsUpdatesAndDeletesAShare() { assertEquals(200, fetched.status()); assertEquals("sales", fetched.body().path("name").asText()); assertEquals("orders", fetched.body().path("comment").asText()); + assertTrue(fetched.body().path("objects").isMissingNode()); + + // include_shared_data returns objects; none are shared yet. + assertEquals(0, objects("sales").size()); // Non-owner cannot update. Response stolen = server.patch(SHARES + "/sales", "bob-token", "{\"comment\":\"stolen\"}"); assertEquals(403, stolen.status()); assertEquals(ErrorCodes.PERMISSION_DENIED, stolen.errorCode()); - // Owner updates the comment. - Response updated = server.patch(SHARES + "/sales", "alice-token", "{\"comment\":\"updated\"}"); + // Owner updates the comment and adds a table. + Response updated = + patch( + "sales", + """ + { + "comment": "updated", + "updates": [{ + "action": "ADD", + "dataObject": {"name": "Main.Sales.Orders", "type": "TABLE"} + }] + } + """); assertEquals(200, updated.status()); assertEquals("updated", updated.body().path("comment").asText()); - // Owner deletes the share. + // Default GET omits objects even after they are shared. + assertTrue(server.get(SHARES + "/sales", "bob-token").body().path("objects").isMissingNode()); + + // include_shared_data returns the added table under its default alias. + JsonNode objects = objects("sales"); + assertEquals(1, objects.size()); + assertEquals("Main.Sales.Orders", objects.get(0).path("name").asText()); + assertEquals("TABLE", objects.get(0).path("type").asText()); + assertEquals("sales.orders", objects.get(0).path("sharedAs").asText()); + + // Owner removes the table by sharedAs, in any case. + assertOk(update("sales", "REMOVE", "{\"type\":\"TABLE\",\"sharedAs\":\"SALES.ORDERS\"}")); + assertEquals(0, objects("sales").size()); + + // A schema is shared under the last segment of its name. + assertOk(update("sales", "ADD", "{\"name\":\"Main.Sales\",\"type\":\"SCHEMA\"}")); + assertEquals("sales", objects("sales").get(0).path("sharedAs").asText()); + + // Owner removes the schema by sharedAs; the catalog segment is dropped. + assertOk(update("sales", "REMOVE", "{\"type\":\"SCHEMA\",\"sharedAs\":\"MAIN.SALES\"}")); + assertEquals(0, objects("sales").size()); + + // Owner adds a table under an explicit alias, which is lowercased. + assertOk( + update( + "sales", + "ADD", + "{\"name\":\"main.sales.orders\",\"type\":\"TABLE\",\"sharedAs\":\"Sales.Orders\"}")); + assertEquals("sales.orders", objects("sales").get(0).path("sharedAs").asText()); + + // Owner deletes the share and the objects in it. assertEquals(204, server.delete(SHARES + "/sales", "alice-token").status()); // Deleted share is 404. @@ -159,10 +206,125 @@ void createsListsUpdatesAndDeletesAShare() { assertEquals(ErrorCodes.RESOURCE_DOES_NOT_EXIST, deleted.errorCode()); } + @Test + void rejectsUnsupportedAssetTypes() { + // VOLUME is not a type a share can hold. + createShare("edge-volume"); + Response volume = + update("edge-volume", "ADD", "{\"name\":\"main.sales.orders\",\"type\":\"VOLUME\"}"); + assertInvalid(volume); + assertEquals( + "updates[0].dataObject.type must be one of [TABLE, SCHEMA]", + volume.body().path("message").asText()); + } + + @Test + void rejectsTableNamesWithoutASchema() { + // A TABLE name must include a schema segment. + createShare("edge-nodot"); + assertInvalid(update("edge-nodot", "ADD", "{\"name\":\"Orders\",\"type\":\"TABLE\"}")); + } + + @Test + void rejectsATableUnderASharedSchema() { + createShare("edge-schema-table"); + assertOk(update("edge-schema-table", "ADD", "{\"name\":\"Main.Sales\",\"type\":\"SCHEMA\"}")); + assertConflict( + update("edge-schema-table", "ADD", "{\"name\":\"Main.Sales.Orders\",\"type\":\"TABLE\"}"), + ErrorCodes.RESOURCE_CONFLICT); + } + + @Test + void rejectsASchemaOverSharedTables() { + createShare("edge-table-schema"); + assertOk( + update("edge-table-schema", "ADD", "{\"name\":\"Main.Sales.Orders\",\"type\":\"TABLE\"}")); + assertConflict( + update("edge-table-schema", "ADD", "{\"name\":\"Main.Sales\",\"type\":\"SCHEMA\"}"), + ErrorCodes.RESOURCE_CONFLICT); + } + + @Test + void rejectsDuplicateAliasAndSource() { + // First ADD of the catalog table succeeds. + createShare("edge-dup"); + String orders = "{\"name\":\"Main.Sales.Orders\",\"type\":\"TABLE\"}"; + assertOk(update("edge-dup", "ADD", orders)); + + // The same catalog table cannot be added twice. + assertConflict(update("edge-dup", "ADD", orders), ErrorCodes.RESOURCE_ALREADY_EXISTS); + + // The same catalog table under a different alias is still a duplicate source. + assertConflict( + update( + "edge-dup", + "ADD", + "{\"name\":\"Main.Sales.Orders\",\"type\":\"TABLE\",\"sharedAs\":\"other.orders\"}"), + ErrorCodes.RESOURCE_ALREADY_EXISTS); + } + + @Test + void removeRequiresTypeAndAnExistingObject() { + // ADD a table to remove later. + createShare("edge-remove"); + String orders = "{\"name\":\"Main.Sales.Orders\",\"type\":\"TABLE\"}"; + assertOk(update("edge-remove", "ADD", orders)); + + // REMOVE without a type is invalid. + assertInvalid(update("edge-remove", "REMOVE", "{\"sharedAs\":\"sales.orders\"}")); + + // REMOVE of a missing alias is 404. + Response missing = + update("edge-remove", "REMOVE", "{\"type\":\"TABLE\",\"sharedAs\":\"missing.table\"}"); + assertEquals(404, missing.status()); + assertEquals(ErrorCodes.RESOURCE_DOES_NOT_EXIST, missing.errorCode()); + + // REMOVE by catalog name succeeds. + assertOk(update("edge-remove", "REMOVE", orders)); + } + + @Test + void rejectsNullUpdates() { + createShare("edge-null"); + Response response = patch("edge-null", "{\"updates\":[null]}"); + assertInvalid(response); + assertEquals("updates must not contain null", response.body().path("message").asText()); + } + private static Response create(String token, String body) { return server.post(SHARES, token, body); } + private static void createShare(String name) { + assertEquals(201, create("alice-token", "{\"name\":\"" + name + "\"}").status()); + } + + private static Response patch(String share, String body) { + return server.patch(SHARES + "/" + share, "alice-token", body); + } + + // Applies a single object update as the owner. + private static Response update(String share, String action, String dataObject) { + return patch( + share, + "{\"updates\":[{\"action\":\"" + action + "\",\"dataObject\":" + dataObject + "}]}"); + } + + private static JsonNode objects(String share) { + Response response = server.get(SHARES + "/" + share + "?include_shared_data=true", "bob-token"); + assertEquals(200, response.status()); + return response.body().path("objects"); + } + + private static void assertOk(Response response) { + assertEquals(200, response.status(), () -> response.body().toString()); + } + + private static void assertConflict(Response response, String errorCode) { + assertEquals(409, response.status()); + assertEquals(errorCode, response.errorCode()); + } + private static void assertInvalid(Response response) { assertEquals(400, response.status()); assertEquals(ErrorCodes.INVALID_PARAMETER_VALUE, response.errorCode()); diff --git a/server/opensharing-server/src/test/java/io/opensharing/share/ShareUpdateTransactionTest.java b/server/opensharing-server/src/test/java/io/opensharing/share/ShareUpdateTransactionTest.java new file mode 100644 index 000000000..2c9bbd243 --- /dev/null +++ b/server/opensharing-server/src/test/java/io/opensharing/share/ShareUpdateTransactionTest.java @@ -0,0 +1,120 @@ +package io.opensharing.share; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import io.opensharing.TestServer; +import io.opensharing.TestServer.Response; +import io.opensharing.http.ErrorCodes; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** A share update applies its removes, adds, and field changes together or not at all. */ +class ShareUpdateTransactionTest { + + private static final String SHARES = "/api/1.0/opensharing/provider/shares"; + private static final String ORDERS = "{\"name\":\"Main.Sales.Orders\",\"type\":\"TABLE\"}"; + + private static TestServer server; + + @BeforeAll + static void startServer() { + server = TestServer.start(); + } + + @AfterAll + static void stopServer() { + server.close(); + } + + @Test + void rollsBackEveryChangeWhenAnAddFails() { + createShareWithOrders("atomic"); + + // Removing orders and adding table1 succeed, but the schema over table1 then conflicts. + Response failed = + patch( + "atomic", + """ + { + "comment": "v2", + "updates": [ + {"action": "REMOVE", "dataObject": %s}, + {"action": "ADD", "dataObject": {"name": "main.sales.table1", "type": "TABLE"}}, + {"action": "ADD", "dataObject": {"name": "main.sales", "type": "SCHEMA"}} + ] + } + """ + .formatted(ORDERS)); + assertEquals(409, failed.status(), () -> failed.body().toString()); + assertEquals(ErrorCodes.RESOURCE_CONFLICT, failed.errorCode()); + + // Orders is still shared, table1 was not added, and the comment is unchanged. + JsonNode share = share("atomic"); + assertEquals(1, share.path("objects").size()); + assertEquals("Main.Sales.Orders", share.path("objects").get(0).path("name").asText()); + assertTrue(share.path("comment").isMissingNode() || share.path("comment").isNull()); + } + + @Test + void rollsBackWhenTheCatalogRejectsAnAdd() { + createShareWithOrders("atomic-catalog"); + + // The catalog has no main.sales.missing, so the remove before it is not applied either. + Response failed = + patch( + "atomic-catalog", + """ + {"updates": [ + {"action": "REMOVE", "dataObject": %s}, + {"action": "ADD", "dataObject": {"name": "main.sales.missing", "type": "TABLE"}} + ]} + """ + .formatted(ORDERS)); + assertEquals(404, failed.status(), () -> failed.body().toString()); + + assertEquals(1, share("atomic-catalog").path("objects").size()); + } + + @Test + void removesBeforeAddingWhateverTheRequestOrder() { + createShareWithOrders("reorder"); + + // The add reuses the alias of the object removed after it in the request. + Response updated = + patch( + "reorder", + """ + {"updates": [ + {"action": "ADD", "dataObject": + {"name": "main.sales.table1", "type": "TABLE", "sharedAs": "sales.orders"}}, + {"action": "REMOVE", "dataObject": {"type": "TABLE", "sharedAs": "sales.orders"}} + ]} + """); + assertEquals(200, updated.status(), () -> updated.body().toString()); + + JsonNode objects = share("reorder").path("objects"); + assertEquals(1, objects.size()); + assertEquals("main.sales.table1", objects.get(0).path("name").asText()); + assertEquals("sales.orders", objects.get(0).path("sharedAs").asText()); + } + + private static void createShareWithOrders(String name) { + assertEquals(201, server.post(SHARES, "alice-token", "{\"name\":\"" + name + "\"}").status()); + Response added = + patch(name, "{\"updates\":[{\"action\":\"ADD\",\"dataObject\":" + ORDERS + "}]}"); + assertEquals(200, added.status(), () -> added.body().toString()); + } + + private static Response patch(String share, String body) { + return server.patch(SHARES + "/" + share, "alice-token", body); + } + + private static JsonNode share(String name) { + Response response = server.get(SHARES + "/" + name + "?include_shared_data=true", "bob-token"); + assertEquals(200, response.status()); + return response.body(); + } +} From caab5a1c489dc9ff98aa86cfddf7a18a902d69ef Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Mon, 19 Oct 2026 14:04:02 -0700 Subject: [PATCH 2/3] Test concurrent changes to one share. --- .../share/ShareUpdateConcurrencyTest.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 server/opensharing-server/src/test/java/io/opensharing/share/ShareUpdateConcurrencyTest.java diff --git a/server/opensharing-server/src/test/java/io/opensharing/share/ShareUpdateConcurrencyTest.java b/server/opensharing-server/src/test/java/io/opensharing/share/ShareUpdateConcurrencyTest.java new file mode 100644 index 000000000..76d1d59fb --- /dev/null +++ b/server/opensharing-server/src/test/java/io/opensharing/share/ShareUpdateConcurrencyTest.java @@ -0,0 +1,104 @@ +package io.opensharing.share; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.opensharing.TestServer; +import io.opensharing.TestServer.Response; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** Concurrent changes to one share run one at a time, so each sees what the other wrote. */ +class ShareUpdateConcurrencyTest { + + private static final String SHARES = "/api/1.0/opensharing/provider/shares"; + private static final int RUNS = 5; + + private static TestServer server; + private static ExecutorService pool; + + @BeforeAll + static void startServer() { + server = TestServer.start(); + pool = Executors.newFixedThreadPool(2); + } + + @AfterAll + static void stopServer() { + pool.shutdownNow(); + server.close(); + } + + @Test + void concurrentSchemaAndTableAddsCannotBothSucceed() throws Exception { + for (int i = 0; i < RUNS; i++) { + String share = "racing-adds-" + i; + createShare(share); + + List results = + race( + () -> server.patch(SHARES + "/" + share, "alice-token", add("main.sales", "SCHEMA")), + () -> + server.patch( + SHARES + "/" + share, "alice-token", add("main.sales.table1", "TABLE"))); + + assertEquals( + List.of(200, 409), results.stream().map(Response::status).sorted().toList()); + Response got = server.get(SHARES + "/" + share + "?include_shared_data=true", "alice-token"); + assertEquals(1, got.body().path("objects").size()); + } + } + + @Test + void anUpdateRacingADeleteFindsNoShare() throws Exception { + for (int i = 0; i < RUNS; i++) { + String share = "racing-delete-" + i; + createShare(share); + + List results = + race( + () -> + server.patch( + SHARES + "/" + share, "alice-token", add("main.sales.table1", "TABLE")), + () -> server.delete(SHARES + "/" + share, "alice-token")); + + assertTrue(Set.of(200, 404).contains(results.get(0).status())); + assertEquals(204, results.get(1).status()); + } + } + + private static void createShare(String share) { + assertEquals(201, server.post(SHARES, "alice-token", "{\"name\":\"" + share + "\"}").status()); + } + + private static String add(String name, String type) { + return "{\"updates\":[{\"action\":\"ADD\",\"dataObject\":{\"name\":\"" + + name + + "\",\"type\":\"" + + type + + "\"}}]}"; + } + + private static List race(Callable first, Callable second) + throws Exception { + CountDownLatch start = new CountDownLatch(1); + Future a = pool.submit(() -> awaitThen(start, first)); + Future b = pool.submit(() -> awaitThen(start, second)); + start.countDown(); + return List.of(a.get(), b.get()); + } + + private static Response awaitThen(CountDownLatch start, Callable call) + throws Exception { + start.await(); + return call.call(); + } +} From 4d4f55e985a5be41ebd0ddc0a2eea0b6705a17ee Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Mon, 19 Oct 2026 16:58:34 -0700 Subject: [PATCH 3/3] Describe store queries in prose rather than SQL. --- .../asset/SharedDataObjectStore.java | 20 +++---------------- .../java/io/opensharing/share/ShareStore.java | 1 - 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectStore.java b/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectStore.java index 2d474e0bc..28d589973 100644 --- a/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectStore.java +++ b/server/opensharing-server-core/src/main/java/io/opensharing/asset/SharedDataObjectStore.java @@ -74,10 +74,7 @@ public SharedDataObjectEntity add( return object; } - /** - * {@code SELECT * FROM os_shared_data_objects WHERE share_id = ? ORDER BY shared_as_schema, - * shared_as_table} - */ + /** The objects in {@code share}, ordered by the schema and table they are shared as. */ public List list(ShareEntity share) { return tx.inTransaction( true, @@ -113,7 +110,6 @@ public void removeByName(EntityManager em, ShareEntity share, AssetType type, St em.remove(object); } - /** {@code SELECT * FROM os_shared_data_objects WHERE share_id = ? AND name = ?} */ private static Optional findByName( EntityManager em, ShareEntity share, String name) { return em.createQuery( @@ -125,10 +121,6 @@ private static Optional findByName( .findFirst(); } - /** - * {@code SELECT * FROM os_shared_data_objects WHERE share_id = ? AND shared_as_schema = ? AND - * shared_as_table = ?} - */ private static Optional findByAlias( EntityManager em, ShareEntity share, String sharedAsSchema, String sharedAsTable) { return em.createQuery( @@ -142,10 +134,7 @@ private static Optional findByAlias( .findFirst(); } - /** - * {@code SELECT id FROM os_shared_data_objects WHERE share_id = ? AND shared_as_schema = ? AND - * type = 'SCHEMA' AND shared_as_table = '' LIMIT 1} - */ + /** Whether {@code share} includes all of {@code schema}. */ private static boolean schemaIncluded(EntityManager em, ShareEntity share, String schema) { return !em.createQuery( "select o.id from SharedDataObjectEntity o where o.share.id = :shareId" @@ -159,10 +148,7 @@ private static boolean schemaIncluded(EntityManager em, ShareEntity share, Strin .isEmpty(); } - /** - * {@code SELECT id FROM os_shared_data_objects WHERE share_id = ? AND shared_as_schema = ? AND - * type = 'TABLE' AND shared_as_table <> '' LIMIT 1} - */ + /** Whether {@code share} includes any single table of {@code schema}. */ private static boolean tablesIncluded(EntityManager em, ShareEntity share, String schema) { return !em.createQuery( "select o.id from SharedDataObjectEntity o where o.share.id = :shareId" diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareStore.java b/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareStore.java index 7aa8fa097..5dcffc3fa 100644 --- a/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareStore.java +++ b/server/opensharing-server-core/src/main/java/io/opensharing/share/ShareStore.java @@ -98,7 +98,6 @@ public void delete(String name, UserContext user) { false, em -> { ShareEntity share = getOwnedOrThrow(em, name, user); - // DELETE FROM os_shared_data_objects WHERE share_id = ? em.createQuery("delete from SharedDataObjectEntity o where o.share.id = :shareId") .setParameter("shareId", share.getId()) .executeUpdate();