Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
`Unsupported access delegation mode: REMOTE_SIGNING`.
- Iceberg REST: renaming a table or view with a missing `source` or `destination` now returns `400 Bad Request` instead of `500 Internal Server Error`.
- Async file-cleanup tasks now bound how long they wait for object-store deletions via the new `polaris.tasks.file-deletion-timeout` (default 1h), so a stalled storage endpoint can no longer pin a task-executor thread indefinitely; a timeout is terminal for the current run rather than immediately retried, so it does not stack more deletions onto the stalled endpoint.
- Semantic-model create and update requests now require `semantic_model` JSON to be an object and
validate every dataset source. Previously, invalid root shapes could bypass source validation or
be accepted under the obsolete array contract.
- Python CLI `catalogs create --type external` now validates `--storage-type` and `--default-base-location` up front, matching the behavior for internal catalogs and the flags' documented "(Required)" status. Previously, omitting either produced an opaque pydantic `ValidationError` at request-build time.
- Iceberg REST: server-side JSON processing failures (HTTP 500) now return the standard Iceberg
error envelope (`{"error": {...}}`) instead of a flat `{"code", "message"}` body, so Iceberg
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,25 +310,22 @@ private JsonNode validateDocument(SemanticModelDocument document) {
* to the offending dataset.
*/
private void resolveAndValidateSources(JsonNode semanticModel) {
if (!semanticModel.isArray()) {
return;
if (!semanticModel.isObject()) {
throw new BadRequestException("Field 'semantic_model' must be a JSON object");
}

for (int modelIdx = 0; modelIdx < semanticModel.size(); modelIdx++) {
JsonNode datasets = semanticModel.get(modelIdx).get("datasets");
if (datasets == null || !datasets.isArray()) {
continue;
}
for (int datasetIdx = 0; datasetIdx < datasets.size(); datasetIdx++) {
String pointer =
String.format("/semantic_model/%d/datasets/%d/source", modelIdx, datasetIdx);
JsonNode source = datasets.get(datasetIdx).get("source");
if (source == null || !source.isTextual()) {
throw new BadRequestException(
"Semantic model dataset at %s must define a string 'source'", pointer);
}
resolveSourceOrThrow(source.asText(), pointer);
JsonNode datasets = semanticModel.get("datasets");
if (datasets == null || !datasets.isArray()) {
return;
}
for (int datasetIdx = 0; datasetIdx < datasets.size(); datasetIdx++) {
String pointer = String.format("/semantic_model/datasets/%d/source", datasetIdx);
JsonNode source = datasets.get(datasetIdx).get("source");
if (source == null || !source.isTextual()) {
throw new BadRequestException(
"Semantic model dataset at %s must define a string 'source'", pointer);
}
resolveSourceOrThrow(source.asText(), pointer);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ protected static CreateSemanticModelRequest createRequest(String name, String mo
}

protected static SemanticModelDocument doc(String model) {
return SemanticModelDocument.builder().setVersion("0.1.1").setSemanticModel(model).build();
return SemanticModelDocument.builder().setVersion("0.2.0.dev0").setSemanticModel(model).build();
}

protected static String modelJson(String source) {
return "[{\"name\":\"m\",\"datasets\":[{\"name\":\"d\",\"source\":\"" + source + "\"}]}]";
return "{\"name\":\"m\",\"datasets\":[{\"name\":\"d\",\"source\":\"" + source + "\"}]}";
}

private void createCatalogNamespaceAndTable() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class SemanticModelCatalogTest {
private static final SemanticModelIdentifier IDENTIFIER =
SemanticModelIdentifier.builder().setNamespace(List.of("sales")).setName(MODEL).build();
private static final String VALID_MODEL_JSON =
"[{\"name\":\"m\",\"datasets\":[{\"name\":\"d\",\"source\":\"sales.store_sales\"}]}]";
"{\"name\":\"m\",\"datasets\":[{\"name\":\"d\",\"source\":\"sales.store_sales\"}]}";

private PolarisResolutionManifestCatalogView view;
private PolarisMetaStoreManager metaStoreManager;
Expand Down Expand Up @@ -119,7 +119,7 @@ void setUp() {

private SemanticModelDocument doc(String semanticModelJson) {
return SemanticModelDocument.builder()
.setVersion("0.1.1")
.setVersion("0.2.0.dev0")
.setSemanticModel(semanticModelJson)
.build();
}
Expand All @@ -143,7 +143,7 @@ private void stubResolvableSource() {
private void stubExistingModel(int entityVersion) {
SemanticModelEntity stored =
new SemanticModelEntity.Builder(NS, MODEL)
.setSpecVersion("0.1.1")
.setSpecVersion("0.2.0.dev0")
.setContent(VALID_MODEL_JSON)
.setId(10L)
.setCatalogId(CATALOG_ID)
Expand All @@ -167,7 +167,7 @@ void createResolvesSourcesAndPersists() {
catalog.createSemanticModel(IDENTIFIER, doc(VALID_MODEL_JSON));

assertThat(response.getDocument().getSemanticModel()).isEqualTo(VALID_MODEL_JSON);
assertThat(response.getDocument().getVersion()).isEqualTo("0.1.1");
assertThat(response.getDocument().getVersion()).isEqualTo("0.2.0.dev0");
assertThat(response.getEntityVersion()).isEqualTo("1");
}

Expand All @@ -178,12 +178,20 @@ void createRejectsEmptyDocument() {
.hasMessageContaining("must not be empty");
}

@Test
void createRejectsArrayDocument() {
assertThatThrownBy(
() -> catalog.createSemanticModel(IDENTIFIER, doc("[" + VALID_MODEL_JSON + "]")))
.isInstanceOf(BadRequestException.class)
.hasMessageContaining("must be a JSON object");
}

@Test
void createRejectsDatasetWithoutSource() {
String noSource = "[{\"name\":\"m\",\"datasets\":[{\"name\":\"d\"}]}]";
String noSource = "{\"name\":\"m\",\"datasets\":[{\"name\":\"d\"}]}";
assertThatThrownBy(() -> catalog.createSemanticModel(IDENTIFIER, doc(noSource)))
.isInstanceOf(BadRequestException.class)
.hasMessageContaining("/semantic_model/0/datasets/0/source")
.hasMessageContaining("/semantic_model/datasets/0/source")
.hasMessageContaining("must define a string 'source'");
}

Expand All @@ -192,7 +200,7 @@ void createRejectsUnresolvedSource() {
// No stub for the source table -> passthrough resolution returns null.
assertThatThrownBy(() -> catalog.createSemanticModel(IDENTIFIER, doc(VALID_MODEL_JSON)))
.isInstanceOf(BadRequestException.class)
.hasMessageContaining("/semantic_model/0/datasets/0/source")
.hasMessageContaining("/semantic_model/datasets/0/source")
.hasMessageContaining("store_sales");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
*/
public class SemanticModelEntity extends PolarisEntity {

/** The declared Ossie spec version of the stored document (e.g. {@code 0.1.1}). */
/** The declared Ossie spec version of the stored document (e.g. {@code 0.2.0.dev0}). */
public static final String SPEC_VERSION_KEY = "semantic-model.spec-version";

/** The Ossie document serialized as a JSON string. */
Expand Down
6 changes: 3 additions & 3 deletions spec/polaris-catalog-apis/semantic-models-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,12 @@ components:
version:
type: string
description: The Apache Ossie spec version.
example: "0.1.1"
example: "0.2.0.dev0"
semantic_model:
type: string
description: The Apache Ossie semantic model serialized as a JSON string.
description: The Apache Ossie semantic model serialized as a JSON object string.
example:
version: "0.1.1"
version: "0.2.0.dev0"
semantic_model: '{"name":"tpcds_retail_model","description":"retail analytics","datasets":[{"name":"store_sales","source":"public.store_sales","primary_key":["ss_item_sk","ss_ticket_number"]}],"metrics":[{"name":"total_sales","expression":{"dialects":[{"dialect":"ANSI_SQL","expression":"SUM(ss_ext_sales_price)"}]}}]}'

SemanticModelIdentifier:
Expand Down
Loading