Skip to content
Draft
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,113 @@
package com.linkedin.openhouse.tables.api.spec.v0.request;

import static com.linkedin.openhouse.common.api.validator.ValidatorConstants.*;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.google.gson.Gson;
import com.linkedin.openhouse.tables.api.spec.v0.request.components.ViewRepresentation;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* Request body for POST and PUT on /v1/databases/{databaseId}/views. Nullable fields are omitted
* from the serialized payload rather than emitted as JSON null, so an omitted {@code
* baseViewVersion} on create stays absent on the wire.
*/
@Builder(toBuilder = true)
@EqualsAndHashCode
@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CreateUpdateViewRequestBody {

@Schema(
description = "Unique Resource identifier for a view within a Database",
example = "my_view")
@NotEmpty(message = "viewId cannot be empty")
@Size(max = 128)
@Pattern(regexp = ALPHA_NUM_UNDERSCORE_REGEX, message = ALPHA_NUM_UNDERSCORE_ERROR_MSG)
private String viewId;

@Schema(
description = "Unique Resource identifier for the Database containing the View",
example = "my_database")
@NotEmpty(message = "databaseId cannot be empty")
@Size(max = 128)
@Pattern(regexp = ALPHA_NUM_UNDERSCORE_REGEX, message = ALPHA_NUM_UNDERSCORE_ERROR_MSG)
private String databaseId;

@Schema(
description = "Unique Resource identifier for the Cluster containing the Database",
example = "my_cluster")
@NotEmpty(message = "clusterId cannot be empty")
@Pattern(
regexp = ALPHA_NUM_UNDERSCORE_REGEX_HYPHEN_ALLOW,
message = ALPHA_NUM_UNDERSCORE_ERROR_MSG_HYPHEN_ALLOW)
private String clusterId;

@Schema(
description = "Schema of the view. OpenHouse views use Iceberg schema specification",
example =
"{\"type\": \"struct\", "
+ "\"fields\": [{\"id\": 1,\"required\": true,\"name\": \"id\",\"type\": \"string\"}, "
+ "{\"id\": 2,\"required\": true,\"name\": \"name\",\"type\": \"string\"}]}")
@NotEmpty(message = "schema cannot be empty")
private String schema;

@Schema(
description = "Engine-specific representations of the view definition",
example = "[{\"type\": \"sql\", \"sql\": \"SELECT 1\", \"dialect\": \"spark\"}]")
@NotEmpty(message = "representations cannot be empty")
@Valid
private List<ViewRepresentation> representations;

@Schema(description = "Dialect of the representation the view was authored in", example = "spark")
@NotEmpty(message = "sourceDialect cannot be empty")
private String sourceDialect;

@Schema(
nullable = true,
description = "Catalog used to resolve unqualified identifiers in the view SQL",
example = "openhouse")
private String defaultCatalog;

@Schema(
nullable = true,
description = "Namespace used to resolve unqualified identifiers in the view SQL",
example = "[\"my_database\"]")
private List<String> defaultNamespace;

@Schema(nullable = true, description = "View properties", example = "{\"key\": \"value\"}")
private Map<String, String> viewProperties;

/**
* Route-sensitive: absent or {@code INITIAL_VERSION} on create, and the current metadata pointer
* on replace. Intentionally carries no bean constraint because the rule differs per HTTP verb and
* is owned by the verb-aware view validator.
*/
@Schema(
nullable = true,
description = "The version of the view that the current update is based upon")
private String baseViewVersion;

/**
* Uses default Gson null handling rather than {@code serializeNulls()} so this stays consistent
* with the class-level {@link JsonInclude.Include#NON_NULL}: an omitted nullable field is absent
* from the payload, not present as JSON null.
*/
public String toJson() {
return new Gson().toJson(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.linkedin.openhouse.tables.api.spec.v0.request.components;

import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotEmpty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* ViewRepresentation is the entity holding a single engine-specific representation of a view in the
* /views API request body. SQL text is carried as opaque text; this class holds no parsing,
* translation or dialect-support logic. Byte-size, representation-type and dialect-support rules
* are owned by the manual view validator.
*/
@Builder(toBuilder = true)
@EqualsAndHashCode
@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ViewRepresentation {

@Schema(description = "Type of the view representation", example = "sql")
@NotEmpty(message = "type cannot be empty")
private String type;

@Schema(
description =
"SQL text of the view representation. This endpoint accepts it as opaque text: it is"
+ " stored as sent and is not parsed or rewritten here.",
example = "SELECT id, name FROM my_database.my_table")
@NotEmpty(message = "sql cannot be empty")
private String sql;

@Schema(description = "SQL dialect the representation is written in", example = "spark")
@NotEmpty(message = "dialect cannot be empty")
private String dialect;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.linkedin.openhouse.tables.api.spec.v0.response;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.Gson;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Value;
import org.springframework.data.domain.Page;

/**
* List contract for views. Paginated from the first release, so there is no unpaginated legacy
* {@code results} field to deprecate later.
*/
@Builder
@Value
public class GetAllViewsResponseBody {

@Schema(description = "Page of View objects in a database", example = "")
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private Page<GetViewResponseBody> pageResults;

public String toJson() {
return new Gson().toJson(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.linkedin.openhouse.tables.api.spec.v0.response;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.Gson;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Value;

/**
* Read contract for a view. Pointer-only today: this response omits the definition fields. The SQL,
* schema, representations, version history, UUID, properties and resolution context live in the
* view metadata file and are not returned by the item or list response in this milestone.
*/
@Builder(toBuilder = true)
@Value
public class GetViewResponseBody {

@Schema(
description = "Unique Resource identifier for a view within a Database",
example = "my_view")
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String viewId;

@Schema(
description = "Unique Resource identifier for the Database containing the View",
example = "my_database")
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String databaseId;

@Schema(
description = "Unique Resource identifier for the Cluster containing the Database",
example = "my_cluster")
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String clusterId;

@Schema(
description = "Fully Qualified Resource URI for the view",
example = "my_cluster.my_database.my_view")
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String viewUri;

@Schema(
description = "Location of the view metadata in File System / Blob Store",
example =
"<fs>://<hostname>/<openhouse_namespace>/<database_name>/<viewUUID>/metadata/<uuid>.metadata.json")
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String metadataLocation;

@Schema(description = "Current Version of the View.", example = "")
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String viewVersion;

@Schema(
description = "View creation epoch time measured in UTC in milliseconds of a view.",
example = "1651002318265")
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private long creationTime;

public String toJson() {
return new Gson().toJson(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.linkedin.openhouse.tables.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

/**
* Internal taxonomy of view failure modes. This enum is never serialized to the wire: it exists
* only to select the HTTP status of the response, and the error body shape stays unchanged.
*
* <p>The full set is declared up front, including codes M1 never emits, so later milestones (view
* admission, dependency analysis) add behavior without a breaking change to this enum.
*/
@AllArgsConstructor
@Getter
public enum ViewErrorCode {
NO_SUCH_VIEW(HttpStatus.NOT_FOUND),
VIEW_ALREADY_EXISTS(HttpStatus.CONFLICT),
NAME_ALREADY_EXISTS_AS_TABLE(HttpStatus.CONFLICT),
CONCURRENT_VIEW_MODIFICATION(HttpStatus.CONFLICT),
DATABASE_NOT_FOUND(HttpStatus.NOT_FOUND),
VIEWS_DISABLED(HttpStatus.NOT_FOUND),
INVALID_VIEW_DEFINITION(HttpStatus.BAD_REQUEST),
UNSUPPORTED_VIEW_DIALECT(HttpStatus.BAD_REQUEST),
UNSUPPORTED_VIEW_SCHEMA(HttpStatus.BAD_REQUEST),
VIEW_ADMISSION_FAILED(HttpStatus.UNPROCESSABLE_ENTITY),
REQUIRED_REPRESENTATION_MISSING(HttpStatus.UNPROCESSABLE_ENTITY),
DEPENDENCY_CYCLE(HttpStatus.UNPROCESSABLE_ENTITY),
MAX_VIEW_DEPTH_EXCEEDED(HttpStatus.UNPROCESSABLE_ENTITY),
ADMISSION_SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE);

private final HttpStatus httpStatus;
}
Loading