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
Expand Up @@ -14,6 +14,7 @@
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.catalog.ViewCatalog;
import org.apache.iceberg.exceptions.CommitFailedException;
import org.apache.iceberg.rest.RESTUtil;
import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.rest.requests.CreateViewRequest;
import org.apache.iceberg.rest.requests.ReportMetricsRequest;
Expand Down Expand Up @@ -101,7 +102,7 @@ public ListNamespacesResponse listNamespaces(String pageToken, Integer pageSize,
if (parent == null || parent.isEmpty()) {
namespaces = namespaceCatalog.listNamespaces();
} else {
namespaces = namespaceCatalog.listNamespaces(Namespace.of(parent.split("[.]")));
namespaces = namespaceCatalog.listNamespaces(RESTUtil.namespaceFromQueryParam(parent));
}

pageToken = pageToken == null ? "" : pageToken;
Expand Down
3 changes: 3 additions & 0 deletions modules/server/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.gradle.api.tasks.testing.Test
import java.time.Duration

plugins {
alias(libs.plugins.quarkus)
Expand Down Expand Up @@ -50,10 +51,12 @@ dependencies {
implementation(libs.jdbc.yugabytedb)

testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.quarkus:quarkus-junit5-mockito")
testImplementation("io.rest-assured:rest-assured")
}

tasks.named<Test>("test") {
useJUnitPlatform()
systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
timeout.set(Duration.ofMinutes(15))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package kasanari.server.bootstrap;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
import org.apache.iceberg.util.JsonUtil;

import java.io.IOException;
import java.util.HashMap;

/**
* Iceberg {@code RESTSerializers} registers a {@link Namespace} deserializer but not one for
* {@link CreateNamespaceRequest}. Jackson then uses the no-arg constructor and never binds
* {@code namespace} (there is no setter; {@code namespace()} is not a JavaBean getter), so the
* field stays null.
*/
public final class IcebergCreateNamespaceRequestDeserializer extends JsonDeserializer<CreateNamespaceRequest> {
@Override
public CreateNamespaceRequest deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
if (node == null || node.isNull()) {
return null;
}

var builder = CreateNamespaceRequest.builder();
var namespaceNode = node.get("namespace");
if (namespaceNode != null && !namespaceNode.isNull()) {
builder.withNamespace(Namespace.of(JsonUtil.getStringArray(namespaceNode)));
}
if (node.hasNonNull("properties")) {
builder.setProperties(new HashMap<>(JsonUtil.getStringMap("properties", node)));
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package kasanari.server.bootstrap;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.iceberg.PartitionSpecParser;
import org.apache.iceberg.Schema;
import org.apache.iceberg.SortOrderParser;
import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.util.JsonUtil;

import java.io.IOException;
import java.util.HashMap;

public final class IcebergCreateTableRequestDeserializer extends JsonDeserializer<CreateTableRequest> {
@Override
public CreateTableRequest deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
if (node == null || node.isNull()) {
return null;
}

var builder = CreateTableRequest.builder();
if (node.hasNonNull("name")) {
builder.withName(node.get("name").asText());
}
if (node.hasNonNull("location")) {
builder.withLocation(node.get("location").asText());
}

var schema = parser.getCodec().treeToValue(node.get("schema"), Schema.class);
if (schema != null) {
builder.withSchema(schema);
var specNode = first(node, "partition-spec", "partitionSpec");
if (specNode != null) {
builder.withPartitionSpec(PartitionSpecParser.fromJson(schema, specNode));
}
var orderNode = first(node, "write-order", "writeOrder");
if (orderNode != null) {
builder.withWriteOrder(SortOrderParser.fromJson(schema, orderNode));
}
}

if (node.hasNonNull("properties")) {
builder.setProperties(new HashMap<>(JsonUtil.getStringMap("properties", node)));
}
var stageCreate = first(node, "stage-create", "stageCreate");
if (stageCreate != null && stageCreate.asBoolean()) {
builder.stageCreate();
}
return builder.build();
}

private static JsonNode first(JsonNode node, String kebab, String camel) {
var value = node.get(kebab);
if (value != null && !value.isNull()) {
return value;
}
value = node.get(camel);
return value == null || value.isNull() ? null : value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package kasanari.server.bootstrap;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.rest.requests.RenameTableRequest;

import java.io.IOException;

public final class IcebergRenameTableRequestDeserializer extends JsonDeserializer<RenameTableRequest> {
@Override
public RenameTableRequest deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
if (node == null || node.isNull()) {
return null;
}

var builder = RenameTableRequest.builder();
var source = node.get("source");
if (source != null && !source.isNull()) {
builder.withSource(parser.getCodec().treeToValue(source, TableIdentifier.class));
}
var destination = node.get("destination");
if (destination != null && !destination.isNull()) {
builder.withDestination(parser.getCodec().treeToValue(destination, TableIdentifier.class));
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package kasanari.server.bootstrap;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.apache.iceberg.rest.RESTSerializers;

/**
* Mixins that force field-based access and explicit deserializers for Iceberg REST request types
* whose {@code foo()} accessors are treated as read-only getters.
*/
final class IcebergRestRequestMixins {
private IcebergRestRequestMixins() {}

@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
setterVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonDeserialize(using = IcebergCreateNamespaceRequestDeserializer.class)
abstract static class CreateNamespace {}

@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
setterVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonDeserialize(using = IcebergCreateTableRequestDeserializer.class)
abstract static class CreateTable {}

@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
setterVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonDeserialize(using = IcebergRenameTableRequestDeserializer.class)
abstract static class RenameTable {}

@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
setterVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonDeserialize(using = IcebergUpdateNamespacePropertiesRequestDeserializer.class)
abstract static class UpdateNamespaceProperties {}

@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
setterVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonDeserialize(using = RESTSerializers.UpdateTableRequestDeserializer.class)
abstract static class UpdateTable {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package kasanari.server.bootstrap;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
import org.apache.iceberg.util.JsonUtil;

import java.io.IOException;
import java.util.HashMap;

public final class IcebergUpdateNamespacePropertiesRequestDeserializer
extends JsonDeserializer<UpdateNamespacePropertiesRequest> {
@Override
public UpdateNamespacePropertiesRequest deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
if (node == null || node.isNull()) {
return null;
}

var builder = UpdateNamespacePropertiesRequest.builder();
if (node.hasNonNull("updates")) {
builder.updateAll(new HashMap<>(JsonUtil.getStringMap("updates", node)));
}
if (node.hasNonNull("removals")) {
builder.removeAll(JsonUtil.getStringList("removals", node));
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,49 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.module.SimpleModule;
import io.quarkus.jackson.ObjectMapperCustomizer;
import jakarta.inject.Singleton;
import org.apache.iceberg.rest.RESTSerializers;
import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
import org.apache.iceberg.rest.requests.UpdateTableRequest;

@Singleton
public class KasanariObjectMapperCustomizer implements ObjectMapperCustomizer {
@Override
public int priority() {
return MAXIMUM_PRIORITY;
}

@Override
public void customize(ObjectMapper objectMapper) {
// Match Iceberg RESTObjectMapper: foo() accessors must not hide private fields.
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
objectMapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE);
objectMapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE);
objectMapper.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE);
objectMapper.setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.ANY);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE);
RESTSerializers.registerAll(objectMapper);
objectMapper.addMixIn(CreateNamespaceRequest.class, IcebergRestRequestMixins.CreateNamespace.class);
objectMapper.addMixIn(CreateTableRequest.class, IcebergRestRequestMixins.CreateTable.class);
objectMapper.addMixIn(RenameTableRequest.class, IcebergRestRequestMixins.RenameTable.class);
objectMapper.addMixIn(UpdateNamespacePropertiesRequest.class, IcebergRestRequestMixins.UpdateNamespaceProperties.class);
objectMapper.addMixIn(UpdateTableRequest.class, IcebergRestRequestMixins.UpdateTable.class);
var icebergRequests = new SimpleModule("kasanari-iceberg-rest");
icebergRequests.addDeserializer(CreateNamespaceRequest.class, new IcebergCreateNamespaceRequestDeserializer());
icebergRequests.addDeserializer(CreateTableRequest.class, new IcebergCreateTableRequestDeserializer());
icebergRequests.addDeserializer(RenameTableRequest.class, new IcebergRenameTableRequestDeserializer());
icebergRequests.addDeserializer(
UpdateNamespacePropertiesRequest.class,
new IcebergUpdateNamespacePropertiesRequestDeserializer()
);
icebergRequests.addDeserializer(UpdateTableRequest.class, new RESTSerializers.UpdateTableRequestDeserializer());
objectMapper.registerModule(icebergRequests);
PaimonRESTSerializers.registerAll(objectMapper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ private static String resourceName(Identifier identifier) {

private static MappedError mapped(
Response.Status status, String message, String resourceType, String resourceName) {
var body = new ErrorResponse(message, resourceType, resourceName, status.getStatusCode());
var body = new ErrorResponse(resourceType, resourceName, message, status.getStatusCode());
return new MappedError(status, body);
}
}
Loading
Loading