Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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;
}

/** The objects in {@code share}, ordered by the schema and table they are shared as. */
public List<SharedDataObjectEntity> 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);
}

private static Optional<SharedDataObjectEntity> 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();
}

private static Optional<SharedDataObjectEntity> 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();
}

/** 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"
+ " 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();
}

/** 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"
+ " 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() + "'");
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {
Expand All @@ -28,7 +31,7 @@ public static Builder builder() {

/** The entity classes the host's {@link Transactions} must register. */
public static List<Class<?>> entities() {
return List.of(ShareEntity.class);
return List.of(ShareEntity.class, SharedDataObjectEntity.class);
}

public CatalogConnector catalog() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
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:
*
* <pre>{@code
* {
* "id": "<share id>",
* "name": "<share name>",
* "displayName": "<display name>",
* "comment": "<comment>",
* "properties": {"<key>": "<value>"}
* "properties": {"<key>": "<value>"},
* "objects": [{"name": "<asset name>", "type": "<TABLE or SCHEMA>", "sharedAs": "<alias>"}]
* }
* }</pre>
*/
Expand All @@ -22,14 +27,30 @@ public record ShareResponse(
String name,
String displayName,
String comment,
Map<String, String> properties) {
Map<String, String> properties,
List<SharedDataObjectResponse> objects) {

public static ShareResponse from(ShareEntity share) {
return from(share, null);
}

public static ShareResponse from(ShareEntity share, List<SharedDataObjectEntity> 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());
}
}
}
Loading
Loading