This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new 39a19ea7c9 [Cherry-pick to branch-1.3] [#13312] fix(core): Validate
name and comment length before persisting entities (#13322) (#13324)
39a19ea7c9 is described below
commit 39a19ea7c96cb9d3a7a19d9862f80b9155ef2c8a
Author: Jerry Shao <[email protected]>
AuthorDate: Fri Sep 18 20:39:59 2026 +0800
[Cherry-pick to branch-1.3] [#13312] fix(core): Validate name and comment
length before persisting entities (#13322) (#13324)
**Cherry-pick Information:**
- Original PR: #13322 (not merged yet; this backport should be merged
after it)
- Target branch: `branch-1.3`
- Status: ⚠️ Cherry-picked with conflicts resolved
**Conflict resolution:**
- `TopicNormalizeDispatcher`: kept the branch-1.3-only array
`normalizeCaseSensitive` helper alongside the new comment check.
- `ModelCatalogOperationsIT`: dropped `assertPropertiesEqual`, a helper
that only exists on main and came in with the patch context.
- `TestFilesetMetaService`: kept as is on branch-1.3, since the rollback
test changed on main does not exist here.
- `TestFilesetCatalogOperations`: adapted the new tests to the
branch-1.3 helpers (`createSchema` takes a test id, and
`FilesetCatalogOperations` takes only the store).
- `TestModelNormalizeDispatcher`: uses a lower-case schema name to match
the name handling on branch-1.3.
**Testing on branch-1.3:**
- `./gradlew :core:test :server:test :catalogs:catalog-fileset:test
:catalogs:catalog-model:test -PskipITs` passes.
- `NameAndCommentLengthIT`, `MetalakeIT`, `AccessControlIT` and
`ModelCatalogOperationsIT` pass in embedded mode on H2.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 <[email protected]>
---
.../catalog/fileset/FilesetCatalogOperations.java | 11 +
.../fileset/TestFilesetCatalogOperations.java | 74 ++++++
.../integration/test/ModelCatalogOperationsIT.java | 66 ++++++
.../client/integration/test/MetalakeIT.java | 30 +++
.../integration/test/NameAndCommentLengthIT.java | 214 +++++++++++++++++
.../test/authorization/AccessControlIT.java | 44 ++++
.../src/main/java/org/apache/gravitino/Entity.java | 2 +-
.../org/apache/gravitino/EntityFieldLimits.java | 73 ++++++
core/src/main/java/org/apache/gravitino/Field.java | 70 ++++++
.../gravitino/catalog/CapabilityHelpers.java | 21 ++
.../catalog/ModelNormalizeDispatcher.java | 6 +-
.../catalog/TopicNormalizeDispatcher.java | 14 ++
.../org/apache/gravitino/meta/BaseMetalake.java | 4 +-
.../org/apache/gravitino/meta/CatalogEntity.java | 6 +-
.../org/apache/gravitino/meta/FilesetEntity.java | 6 +-
.../org/apache/gravitino/meta/GroupEntity.java | 3 +-
.../apache/gravitino/meta/JobTemplateEntity.java | 4 +-
.../apache/gravitino/meta/ModelVersionEntity.java | 7 +
.../org/apache/gravitino/meta/PolicyEntity.java | 3 +-
.../java/org/apache/gravitino/meta/RoleEntity.java | 3 +-
.../org/apache/gravitino/meta/SchemaEntity.java | 6 +-
.../java/org/apache/gravitino/meta/TagEntity.java | 6 +-
.../org/apache/gravitino/meta/TopicEntity.java | 6 +-
.../java/org/apache/gravitino/meta/UserEntity.java | 3 +-
.../converters/H2ExceptionConverter.java | 5 +
.../converters/MySQLExceptionConverter.java | 5 +
.../converters/PostgreSQLExceptionConverter.java | 5 +
.../converters/ValueTooLongExceptions.java | 52 +++++
.../test/java/org/apache/gravitino/TestField.java | 26 +++
.../catalog/TestModelNormalizeDispatcher.java | 84 +++++++
.../catalog/TestTopicNormalizeDispatcher.java | 34 +++
.../gravitino/meta/TestEntityFieldLimits.java | 258 +++++++++++++++++++++
.../converters/TestH2ExceptionConverter.java | 15 ++
.../converters/TestMySQLExceptionConverter.java | 16 ++
.../TestPostgreSQLExceptionConverter.java | 16 ++
.../org/apache/gravitino/tag/TestTagManager.java | 45 ++++
36 files changed, 1229 insertions(+), 14 deletions(-)
diff --git
a/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
b/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
index ac75ec03c2..b725c27125 100644
---
a/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
+++
b/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
@@ -68,6 +68,7 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.NameIdentifier;
@@ -430,6 +431,11 @@ public class FilesetCatalogOperations extends
ManagedSchemaOperations
Map<String, String> storageLocations,
Map<String, String> properties)
throws NoSuchSchemaException, FilesetAlreadyExistsException {
+ // Check the comment before the storage locations are created, the entity
validation only
+ // happens after that.
+ EntityFieldLimits.checkMaxLength(
+ comment, EntityFieldLimits.MAX_COMMENT_LENGTH, "comment",
Entity.EntityType.FILESET);
+
storageLocations.forEach(
(name, path) -> {
if (StringUtils.isBlank(name)) {
@@ -755,6 +761,11 @@ public class FilesetCatalogOperations extends
ManagedSchemaOperations
@Override
public Schema createSchema(NameIdentifier ident, String comment, Map<String,
String> properties)
throws NoSuchCatalogException, SchemaAlreadyExistsException {
+ // Check the comment before the schema directories are created, the entity
validation only
+ // happens after that.
+ EntityFieldLimits.checkMaxLength(
+ comment, EntityFieldLimits.MAX_COMMENT_LENGTH, "comment",
Entity.EntityType.SCHEMA);
+
if (disableFSOps) {
return super.createSchema(ident, comment, properties);
}
diff --git
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
index 3860e6f58e..d3f670070b 100644
---
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
+++
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
@@ -69,9 +69,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.EntityStoreFactory;
import org.apache.gravitino.GravitinoEnv;
@@ -432,6 +434,26 @@ public class TestFilesetCatalogOperations {
exception.getMessage());
}
+ @Test
+ public void testCreateSchemaWithTooLongComment() throws IOException {
+ final long testId = generateTestId();
+ String name = "schema" + testId;
+ String schemaPath = TEST_ROOT_PATH + "/" + name;
+ String tooLongComment = StringUtils.repeat("a",
EntityFieldLimits.MAX_COMMENT_LENGTH + 1);
+
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> createSchema(testId, name, tooLongComment, null,
schemaPath));
+ Assertions.assertEquals(
+ "The comment of the schema must not exceed 256 characters",
exception.getMessage());
+
+ // The schema directory must not be created for a rejected schema.
+ Path path = new Path(schemaPath);
+ FileSystem fs = path.getFileSystem(new Configuration());
+ Assertions.assertFalse(fs.exists(path));
+ }
+
@Test
public void testCreateSchemaWithCatalogLocation() throws IOException {
final long testId = generateTestId();
@@ -1264,6 +1286,58 @@ public class TestFilesetCatalogOperations {
}
}
+ @Test
+ public void testCreateFilesetWithTooLongComment() throws IOException {
+ final long testId = generateTestId();
+ final String schemaName = "schema" + testId;
+ final String name = "fileset" + testId;
+ final String schemaPath = TEST_ROOT_PATH + "/" + schemaName;
+ createSchema(testId, schemaName, "comment", null, schemaPath);
+
+ String tooLongComment = StringUtils.repeat("a",
EntityFieldLimits.MAX_COMMENT_LENGTH + 1);
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ createFileset(name, schemaName, tooLongComment,
Fileset.Type.MANAGED, null, null));
+ Assertions.assertEquals(
+ "The comment of the fileset must not exceed 256 characters",
exception.getMessage());
+
+ // The fileset directory must not be created for a rejected fileset.
+ Path filesetPath = new Path(schemaPath, name);
+ FileSystem fs = filesetPath.getFileSystem(new Configuration());
+ Assertions.assertFalse(fs.exists(filesetPath));
+
+ createFileset(name, schemaName, "comment", Fileset.Type.MANAGED, null,
null);
+ Assertions.assertTrue(fs.exists(filesetPath));
+ }
+
+ @Test
+ public void testUpdateFilesetCommentTooLong() throws IOException {
+ final long testId = generateTestId();
+ final String schemaName = "schema" + testId;
+ final String comment = "comment" + testId;
+ final String name = "fileset" + testId;
+ final String schemaPath = TEST_ROOT_PATH + "/" + schemaName;
+
+ createSchema(testId, schemaName, comment, null, schemaPath);
+ createFileset(name, schemaName, comment, Fileset.Type.MANAGED, null, null);
+
+ String tooLongComment = StringUtils.repeat("a",
EntityFieldLimits.MAX_COMMENT_LENGTH + 1);
+ try (FilesetCatalogOperations ops = new FilesetCatalogOperations(store)) {
+ ops.initialize(Maps.newHashMap(), randomCatalogInfo(),
FILESET_PROPERTIES_METADATA);
+ NameIdentifier filesetIdent = NameIdentifier.of("m1", "c1", schemaName,
name);
+
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> ops.alterFileset(filesetIdent,
FilesetChange.updateComment(tooLongComment)));
+ Assertions.assertEquals(
+ "The comment of the fileset must not exceed 256 characters",
exception.getMessage());
+ Assertions.assertEquals(comment,
ops.loadFileset(filesetIdent).comment());
+ }
+ }
+
@Test
public void testRemoveFilesetComment() throws IOException {
final long testId = generateTestId();
diff --git
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java
index 52bf115c41..099c18ef62 100644
---
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java
+++
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/integration/test/ModelCatalogOperationsIT.java
@@ -26,6 +26,7 @@ import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
@@ -1258,6 +1259,71 @@ public class ModelCatalogOperationsIT extends BaseIT {
"u2",
gravitinoCatalog.asModelCatalog().getModelVersionUri(modelIdent1, "alias3",
null));
}
+ @Test
+ public void testRenameModelWithIllegalName() {
+ String modelName = RandomNameUtils.genRandomName("model_rename_spec");
+ NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+ gravitinoCatalog.asModelCatalog().registerModel(modelIdent, null, null);
+
+ String tooLongName = StringUtils.repeat("m", 129);
+ IllegalArgumentException e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ gravitinoCatalog
+ .asModelCatalog()
+ .alterModel(modelIdent, ModelChange.rename(tooLongName)));
+ Assertions.assertTrue(e.getMessage().contains("is illegal"),
e.getMessage());
+
Assertions.assertTrue(gravitinoCatalog.asModelCatalog().modelExists(modelIdent));
+ }
+
+ @Test
+ public void testModelVersionAliasLength() {
+ String modelName = RandomNameUtils.genRandomName("model_alias_length");
+ NameIdentifier modelIdent = NameIdentifier.of(schemaName, modelName);
+ gravitinoCatalog.asModelCatalog().registerModel(modelIdent, null, null);
+
+ String tooLongAlias = StringUtils.repeat("a", 129);
+ IllegalArgumentException e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ gravitinoCatalog
+ .asModelCatalog()
+ .linkModelVersion(
+ modelIdent, "uri", new String[] {tooLongAlias},
"comment", null));
+ Assertions.assertTrue(
+ e.getMessage().contains("The alias of the model version must not
exceed 128 characters"),
+ e.getMessage());
+ Assertions.assertEquals(
+ 0,
gravitinoCatalog.asModelCatalog().listModelVersions(modelIdent).length);
+
+ String maxLengthAlias = StringUtils.repeat("a", 128);
+ gravitinoCatalog
+ .asModelCatalog()
+ .linkModelVersion(modelIdent, "uri", new String[] {maxLengthAlias},
"comment", null);
+ Assertions.assertArrayEquals(
+ new String[] {maxLengthAlias},
+ gravitinoCatalog.asModelCatalog().getModelVersion(modelIdent,
0).aliases());
+
+ e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ gravitinoCatalog
+ .asModelCatalog()
+ .alterModelVersion(
+ modelIdent,
+ 0,
+ ModelVersionChange.updateAliases(new String[]
{tooLongAlias}, null)));
+ Assertions.assertTrue(
+ e.getMessage().contains("The alias of the model version must not
exceed 128 characters"),
+ e.getMessage());
+ Assertions.assertArrayEquals(
+ new String[] {maxLengthAlias},
+ gravitinoCatalog.asModelCatalog().getModelVersion(modelIdent,
0).aliases());
+ }
+
private void createMetalake() {
GravitinoMetalake[] gravitinoMetalakes = client.listMetalakes();
Assertions.assertEquals(0, gravitinoMetalakes.length);
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/MetalakeIT.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/MetalakeIT.java
index 7911f02cdf..0fb415b258 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/MetalakeIT.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/MetalakeIT.java
@@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.CatalogChange;
import org.apache.gravitino.MetalakeChange;
@@ -357,4 +358,33 @@ public class MetalakeIT extends BaseIT {
Assertions.assertFalse(client.dropMetalake(metalake.name()));
}
}
+
+ @Test
+ public void testMetalakeCommentLength() {
+ String tooLongComment = StringUtils.repeat("c", 257);
+
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.createMetalake(metalakeNameA, tooLongComment,
Collections.emptyMap()));
+ assertTrue(
+ e.getMessage().contains("The comment of the metalake must not exceed
256 characters"),
+ e.getMessage());
+ assertThrows(NoSuchMetalakeException.class, () ->
client.loadMetalake(metalakeNameA));
+
+ String maxLengthComment = StringUtils.repeat("c", 256);
+ GravitinoMetalake metalake =
+ client.createMetalake(metalakeNameA, maxLengthComment,
Collections.emptyMap());
+ assertEquals(maxLengthComment, metalake.comment());
+
+ e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ client.alterMetalake(metalakeNameA,
MetalakeChange.updateComment(tooLongComment)));
+ assertTrue(
+ e.getMessage().contains("The comment of the metalake must not exceed
256 characters"),
+ e.getMessage());
+ assertEquals(maxLengthComment,
client.loadMetalake(metalakeNameA).comment());
+ }
}
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/NameAndCommentLengthIT.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/NameAndCommentLengthIT.java
new file mode 100644
index 0000000000..d0b7c69161
--- /dev/null
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/NameAndCommentLengthIT.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.client.integration.test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collections;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.CatalogChange;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.exceptions.NoSuchTagException;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.file.FilesetChange;
+import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.GravitinoITUtils;
+import org.apache.gravitino.policy.Policy;
+import org.apache.gravitino.policy.PolicyChange;
+import org.apache.gravitino.policy.PolicyContent;
+import org.apache.gravitino.policy.PolicyContents;
+import org.apache.gravitino.tag.Tag;
+import org.apache.gravitino.tag.TagChange;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+/**
+ * Verifies that names and comments longer than their metadata store columns
are rejected with a
+ * clear error instead of a database error, on every metadata store backend.
+ */
+public class NameAndCommentLengthIT extends BaseIT {
+
+ private static final String TOO_LONG_NAME = StringUtils.repeat("n", 129);
+ private static final String MAX_LENGTH_NAME = StringUtils.repeat("n", 128);
+ private static final String TOO_LONG_COMMENT = StringUtils.repeat("c", 257);
+ private static final String MAX_LENGTH_COMMENT = StringUtils.repeat("c",
256);
+
+ private final String metalakeName =
GravitinoITUtils.genRandomName("length_it_metalake");
+ private GravitinoMetalake metalake;
+ private File localStorage;
+
+ @BeforeAll
+ public void setUp() throws IOException {
+ metalake = client.createMetalake(metalakeName, "comment",
Collections.emptyMap());
+ localStorage = Files.createTempDirectory("length_it_storage").toFile();
+ }
+
+ @AfterAll
+ public void tearDown() throws IOException {
+ client.dropMetalake(metalakeName, true);
+ FileUtils.deleteDirectory(localStorage);
+ }
+
+ @Test
+ public void testTagNameAndCommentLength() {
+ assertTooLong(
+ "The name of the tag must not exceed 128 characters",
+ () -> metalake.createTag(TOO_LONG_NAME, "comment",
Collections.emptyMap()));
+
+ String tagName = GravitinoITUtils.genRandomName("length_it_tag");
+ assertTooLong(
+ "The comment of the tag must not exceed 256 characters",
+ () -> metalake.createTag(tagName, TOO_LONG_COMMENT,
Collections.emptyMap()));
+ Assertions.assertThrows(NoSuchTagException.class, () ->
metalake.getTag(tagName));
+
+ Tag tag = metalake.createTag(MAX_LENGTH_NAME, MAX_LENGTH_COMMENT,
Collections.emptyMap());
+ Assertions.assertEquals(MAX_LENGTH_NAME, tag.name());
+ Assertions.assertEquals(MAX_LENGTH_COMMENT, tag.comment());
+
+ assertTooLong(
+ "The name of the tag must not exceed 128 characters",
+ () -> metalake.alterTag(MAX_LENGTH_NAME,
TagChange.rename(TOO_LONG_NAME)));
+ assertTooLong(
+ "The comment of the tag must not exceed 256 characters",
+ () -> metalake.alterTag(MAX_LENGTH_NAME,
TagChange.updateComment(TOO_LONG_COMMENT)));
+ Assertions.assertEquals(MAX_LENGTH_COMMENT,
metalake.getTag(MAX_LENGTH_NAME).comment());
+
+ Assertions.assertTrue(metalake.deleteTag(MAX_LENGTH_NAME));
+ }
+
+ @Test
+ public void testPolicyNameLength() {
+ PolicyContent content =
+ PolicyContents.custom(
+ ImmutableMap.of("rule1", "value1"),
ImmutableSet.of(MetadataObject.Type.TABLE), null);
+
+ assertTooLong(
+ "The name of the policy must not exceed 128 characters",
+ () -> metalake.createPolicy(TOO_LONG_NAME, "custom", "comment", true,
content));
+
+ Policy policy = metalake.createPolicy(MAX_LENGTH_NAME, "custom",
"comment", true, content);
+ Assertions.assertEquals(MAX_LENGTH_NAME, policy.name());
+
+ assertTooLong(
+ "The name of the policy must not exceed 128 characters",
+ () -> metalake.alterPolicy(MAX_LENGTH_NAME,
PolicyChange.rename(TOO_LONG_NAME)));
+ Assertions.assertEquals(MAX_LENGTH_NAME,
metalake.getPolicy(MAX_LENGTH_NAME).name());
+
+ Assertions.assertTrue(metalake.deletePolicy(MAX_LENGTH_NAME));
+ }
+
+ @Test
+ public void testCatalogCommentLength() {
+ String catalogName = GravitinoITUtils.genRandomName("length_it_catalog");
+
+ assertTooLong(
+ "The comment of the catalog must not exceed 256 characters",
+ () -> createFilesetCatalog(catalogName, TOO_LONG_COMMENT));
+ Assertions.assertFalse(metalake.catalogExists(catalogName));
+
+ Catalog catalog = createFilesetCatalog(catalogName, MAX_LENGTH_COMMENT);
+ Assertions.assertEquals(MAX_LENGTH_COMMENT, catalog.comment());
+
+ assertTooLong(
+ "The comment of the catalog must not exceed 256 characters",
+ () -> metalake.alterCatalog(catalogName,
CatalogChange.updateComment(TOO_LONG_COMMENT)));
+ Assertions.assertEquals(MAX_LENGTH_COMMENT,
metalake.loadCatalog(catalogName).comment());
+
+ metalake.dropCatalog(catalogName, true);
+ }
+
+ @Test
+ public void testSchemaAndFilesetCommentLength() {
+ String catalogName =
GravitinoITUtils.genRandomName("length_it_fileset_catalog");
+ Catalog catalog = createFilesetCatalog(catalogName, "comment");
+
+ // A rejected schema must not leave its directory behind.
+ String schemaName = GravitinoITUtils.genRandomName("length_it_schema");
+ assertTooLong(
+ "The comment of the schema must not exceed 256 characters",
+ () -> catalog.asSchemas().createSchema(schemaName, TOO_LONG_COMMENT,
null));
+ Assertions.assertFalse(catalog.asSchemas().schemaExists(schemaName));
+ Assertions.assertFalse(new File(localStorage, catalogName + "/" +
schemaName).exists());
+
+ catalog.asSchemas().createSchema(schemaName, MAX_LENGTH_COMMENT, null);
+ Assertions.assertTrue(new File(localStorage, catalogName + "/" +
schemaName).exists());
+ Assertions.assertEquals(
+ MAX_LENGTH_COMMENT,
catalog.asSchemas().loadSchema(schemaName).comment());
+
+ // A rejected fileset must not leave its directory behind.
+ NameIdentifier filesetIdent =
+ NameIdentifier.of(schemaName,
GravitinoITUtils.genRandomName("length_it_fileset"));
+ assertTooLong(
+ "The comment of the fileset must not exceed 256 characters",
+ () ->
+ catalog
+ .asFilesetCatalog()
+ .createFileset(filesetIdent, TOO_LONG_COMMENT,
Fileset.Type.MANAGED, null, null));
+
Assertions.assertFalse(catalog.asFilesetCatalog().filesetExists(filesetIdent));
+ Assertions.assertFalse(
+ new File(localStorage, catalogName + "/" + schemaName + "/" +
filesetIdent.name())
+ .exists());
+
+ Fileset fileset =
+ catalog
+ .asFilesetCatalog()
+ .createFileset(filesetIdent, MAX_LENGTH_COMMENT,
Fileset.Type.MANAGED, null, null);
+ Assertions.assertEquals(MAX_LENGTH_COMMENT, fileset.comment());
+ Assertions.assertTrue(
+ new File(localStorage, catalogName + "/" + schemaName + "/" +
filesetIdent.name())
+ .exists());
+
+ assertTooLong(
+ "The comment of the fileset must not exceed 256 characters",
+ () ->
+ catalog
+ .asFilesetCatalog()
+ .alterFileset(filesetIdent,
FilesetChange.updateComment(TOO_LONG_COMMENT)));
+ Assertions.assertEquals(
+ MAX_LENGTH_COMMENT,
catalog.asFilesetCatalog().loadFileset(filesetIdent).comment());
+
+ metalake.dropCatalog(catalogName, true);
+ }
+
+ private Catalog createFilesetCatalog(String catalogName, String comment) {
+ String location = localStorage.toURI() + catalogName;
+ return metalake.createCatalog(
+ catalogName,
+ Catalog.Type.FILESET,
+ "hadoop",
+ comment,
+ ImmutableMap.of("location", location));
+ }
+
+ private static void assertTooLong(String expectedMessage, Executable
executable) {
+ IllegalArgumentException e =
+ Assertions.assertThrows(IllegalArgumentException.class, executable);
+ Assertions.assertTrue(e.getMessage().contains(expectedMessage),
e.getMessage());
+ }
+}
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
index dfcb0f8516..2522ab5e53 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
@@ -27,6 +27,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.Configs;
import org.apache.gravitino.MetadataObject;
@@ -736,4 +737,47 @@ public class AccessControlIT extends BaseIT {
// Cleanup.
metalake.deleteRole(roleName);
}
+
+ @Test
+ void testUserGroupAndRoleNameLength() {
+ String tooLongName = StringUtils.repeat("n", 129);
+ String maxLengthName = StringUtils.repeat("n", 128);
+
+ IllegalArgumentException e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
metalake.addUser(tooLongName));
+ Assertions.assertTrue(
+ e.getMessage().contains("The name of the user must not exceed 128
characters"),
+ e.getMessage());
+ Assertions.assertEquals(maxLengthName,
metalake.addUser(maxLengthName).name());
+ Assertions.assertTrue(metalake.removeUser(maxLengthName));
+
+ e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
metalake.addGroup(tooLongName));
+ Assertions.assertTrue(
+ e.getMessage().contains("The name of the group must not exceed 128
characters"),
+ e.getMessage());
+ Assertions.assertEquals(maxLengthName,
metalake.addGroup(maxLengthName).name());
+ Assertions.assertTrue(metalake.removeGroup(maxLengthName));
+
+ SecurableObject metalakeObject =
+ SecurableObjects.ofMetalake(
+ metalakeName,
Lists.newArrayList(Privileges.CreateCatalog.allow()));
+ e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ metalake.createRole(
+ tooLongName, Collections.emptyMap(),
Lists.newArrayList(metalakeObject)));
+ Assertions.assertTrue(
+ e.getMessage().contains("The name of the role must not exceed 128
characters"),
+ e.getMessage());
+ Assertions.assertEquals(
+ maxLengthName,
+ metalake
+ .createRole(maxLengthName, Collections.emptyMap(),
Lists.newArrayList(metalakeObject))
+ .name());
+ Assertions.assertTrue(metalake.deleteRole(maxLengthName));
+ }
}
diff --git a/core/src/main/java/org/apache/gravitino/Entity.java
b/core/src/main/java/org/apache/gravitino/Entity.java
index 3447b225b3..771f2e7f75 100644
--- a/core/src/main/java/org/apache/gravitino/Entity.java
+++ b/core/src/main/java/org/apache/gravitino/Entity.java
@@ -91,7 +91,7 @@ public interface Entity extends Serializable {
* @throws IllegalArgumentException If the validation fails.
*/
default void validate() throws IllegalArgumentException {
- fields().forEach(Field::validate);
+ fields().forEach((field, value) -> field.validate(value, type()));
}
/**
diff --git a/core/src/main/java/org/apache/gravitino/EntityFieldLimits.java
b/core/src/main/java/org/apache/gravitino/EntityFieldLimits.java
new file mode 100644
index 0000000000..8c7edab14c
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/EntityFieldLimits.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino;
+
+import java.util.Locale;
+import javax.annotation.Nullable;
+
+/**
+ * The maximum lengths of entity fields persisted by the relational entity
store.
+ *
+ * <p>These values must match the column definitions in {@code
+ * scripts/{h2,mysql,postgresql}/schema-*.sql}, so that values which cannot be
stored are rejected
+ * before they reach the database.
+ */
+public final class EntityFieldLimits {
+
+ /** The maximum number of characters of an entity name or a model version
alias. */
+ public static final int MAX_NAME_LENGTH = 128;
+
+ /** The maximum number of characters of an entity comment stored in a
256-character column. */
+ public static final int MAX_COMMENT_LENGTH = 256;
+
+ private EntityFieldLimits() {}
+
+ /**
+ * Checks that a value does not exceed the given maximum number of
characters, counted in Unicode
+ * code points.
+ *
+ * @param value The value to check, a null value always passes.
+ * @param maxLength The maximum number of characters allowed.
+ * @param fieldName The name of the field, used in the error message.
+ * @param entityType The type of the entity owning the field, used in the
error message.
+ * @throws IllegalArgumentException If the value exceeds the maximum length.
+ */
+ public static void checkMaxLength(
+ @Nullable String value,
+ int maxLength,
+ String fieldName,
+ @Nullable Entity.EntityType entityType) {
+ // Count code points rather than UTF-16 chars, since MySQL (utf8mb4) and
PostgreSQL count a
+ // supplementary character such as an emoji as one character.
+ if (value != null && value.codePointCount(0, value.length()) > maxLength) {
+ throw new IllegalArgumentException(exceedMaxLengthMessage(fieldName,
entityType, maxLength));
+ }
+ }
+
+ private static String exceedMaxLengthMessage(
+ String fieldName, @Nullable Entity.EntityType entityType, int maxLength)
{
+ if (entityType == null) {
+ return String.format("Field %s must not exceed %d characters",
fieldName, maxLength);
+ }
+
+ return String.format(
+ "The %s of the %s must not exceed %d characters",
+ fieldName, entityType.name().toLowerCase(Locale.ROOT).replace('_', '
'), maxLength);
+ }
+}
diff --git a/core/src/main/java/org/apache/gravitino/Field.java
b/core/src/main/java/org/apache/gravitino/Field.java
index 9306f61e10..2aba02e150 100644
--- a/core/src/main/java/org/apache/gravitino/Field.java
+++ b/core/src/main/java/org/apache/gravitino/Field.java
@@ -18,12 +18,15 @@
*/
package org.apache.gravitino;
+import javax.annotation.Nullable;
import lombok.EqualsAndHashCode;
/** This class represents a field in the Apache Gravitino framework. */
@EqualsAndHashCode
public class Field {
+ private static final int UNLIMITED_LENGTH = -1;
+
private String fieldName;
private Class<?> typeClass;
@@ -32,6 +35,8 @@ public class Field {
private boolean optional;
+ private int maxLength = UNLIMITED_LENGTH;
+
private Field() {}
/**
@@ -66,6 +71,40 @@ public class Field {
.build();
}
+ /**
+ * Creates a required String field instance whose value must not exceed the
given length.
+ *
+ * @param fieldName The name of the field.
+ * @param description The description of the field.
+ * @param maxLength The maximum number of characters of the field value.
+ * @return A required Field instance.
+ */
+ public static Field required(String fieldName, String description, int
maxLength) {
+ return new Builder(false)
+ .withName(fieldName)
+ .withTypeClass(String.class)
+ .withDescription(description)
+ .withMaxLength(maxLength)
+ .build();
+ }
+
+ /**
+ * Creates an optional String field instance whose value must not exceed the
given length.
+ *
+ * @param fieldName The name of the field.
+ * @param description The description of the field.
+ * @param maxLength The maximum number of characters of the field value.
+ * @return An optional Field instance.
+ */
+ public static Field optional(String fieldName, String description, int
maxLength) {
+ return new Builder(true)
+ .withName(fieldName)
+ .withTypeClass(String.class)
+ .withDescription(description)
+ .withMaxLength(maxLength)
+ .build();
+ }
+
/**
* Creates a required field instance.
*
@@ -96,6 +135,18 @@ public class Field {
* @throws IllegalArgumentException If the field value is invalid.
*/
public <T> void validate(T fieldValue) {
+ validate(fieldValue, null);
+ }
+
+ /**
+ * Validates a field value according to the field's requirements.
+ *
+ * @param fieldValue The value to be validated.
+ * @param entityType The type of the entity owning the field, used in the
error message.
+ * @param <T> The type of the field value.
+ * @throws IllegalArgumentException If the field value is invalid.
+ */
+ public <T> void validate(T fieldValue, @Nullable Entity.EntityType
entityType) {
if (fieldValue == null && !optional) {
throw new IllegalArgumentException("Field " + fieldName + " is
required");
}
@@ -104,6 +155,10 @@ public class Field {
throw new IllegalArgumentException(
"Field " + fieldName + " is not of type " + typeClass.getName());
}
+
+ if (maxLength != UNLIMITED_LENGTH && fieldValue instanceof String) {
+ EntityFieldLimits.checkMaxLength((String) fieldValue, maxLength,
fieldName, entityType);
+ }
}
/** Builder class for creating Field instances. */
@@ -153,6 +208,17 @@ public class Field {
return this;
}
+ /**
+ * Sets the maximum number of characters of the field value. It only
applies to String values.
+ *
+ * @param maxLength The maximum number of characters of the field value.
+ * @return The Builder instance.
+ */
+ public Builder withMaxLength(int maxLength) {
+ field.maxLength = maxLength;
+ return this;
+ }
+
/**
* Builds and returns the configured Field instance.
*
@@ -168,6 +234,10 @@ public class Field {
throw new IllegalArgumentException("Field type class is required");
}
+ if (field.maxLength != UNLIMITED_LENGTH && field.maxLength <= 0) {
+ throw new IllegalArgumentException("Field max length must be
positive");
+ }
+
return field;
}
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
b/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
index e29f2596af..39c1facc49 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
@@ -29,6 +29,7 @@ import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.file.FilesetChange;
+import org.apache.gravitino.model.ModelChange;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.TableChange;
import org.apache.gravitino.rel.ViewChange;
@@ -96,6 +97,18 @@ public class CapabilityHelpers {
.toArray(FilesetChange[]::new);
}
+ public static ModelChange[] applyCapabilities(Capability capabilities,
ModelChange... changes) {
+ return Arrays.stream(changes)
+ .map(
+ change -> {
+ if (change instanceof ModelChange.RenameModel) {
+ return applyCapabilities((ModelChange.RenameModel) change,
capabilities);
+ }
+ return change;
+ })
+ .toArray(ModelChange[]::new);
+ }
+
public static ViewChange[] applyCapabilities(Capability capabilities,
ViewChange... changes) {
return Arrays.stream(changes)
.map(
@@ -357,6 +370,14 @@ public class CapabilityHelpers {
return FilesetChange.rename(newName);
}
+ private static ModelChange applyCapabilities(
+ ModelChange.RenameModel renameModel, Capability capabilities) {
+ applyNameSpecification(Capability.Scope.MODEL, renameModel.newName(),
capabilities);
+ String newName =
+ applyCaseSensitiveOnName(Capability.Scope.MODEL,
renameModel.newName(), capabilities);
+ return ModelChange.rename(newName);
+ }
+
private static ViewChange applyCapabilities(
ViewChange.RenameView renameView, Capability capabilities) {
String newName =
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
index 5bf32f892b..f0c7a38e98 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
@@ -152,7 +152,11 @@ public class ModelNormalizeDispatcher implements
ModelDispatcher {
@Override
public Model alterModel(NameIdentifier ident, ModelChange... changes)
throws NoSuchModelException, IllegalArgumentException {
- return dispatcher.alterModel(normalizeCaseSensitive(ident), changes);
+ Capability capability = getCapability(ident, catalogManager);
+ return dispatcher.alterModel(
+ // The constraints of the name spec may be more strict than underlying
catalog,
+ // and for compatibility reasons, we only apply case-sensitive
capabilities here.
+ normalizeCaseSensitive(ident), applyCapabilities(capability, changes));
}
/** {@inheritDoc} */
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
index a104b4f671..eec94f8fb8 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
@@ -24,6 +24,8 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
@@ -70,12 +72,19 @@ public class TopicNormalizeDispatcher implements
TopicDispatcher {
public Topic createTopic(
NameIdentifier ident, String comment, DataLayout dataLayout, Map<String,
String> properties)
throws NoSuchSchemaException, TopicAlreadyExistsException {
+ // Check the comment before the underlying catalog creates the topic.
+ checkCommentLength(comment);
return dispatcher.createTopic(normalizeNameIdentifier(ident), comment,
dataLayout, properties);
}
@Override
public Topic alterTopic(NameIdentifier ident, TopicChange... changes)
throws NoSuchTopicException, IllegalArgumentException {
+ for (TopicChange change : changes) {
+ if (change instanceof TopicChange.UpdateTopicComment) {
+ checkCommentLength(((TopicChange.UpdateTopicComment)
change).getNewComment());
+ }
+ }
// The constraints of the name spec may be more strict than underlying
catalog,
// and for compatibility reasons, we only apply case-sensitive
capabilities here.
return dispatcher.alterTopic(normalizeCaseSensitive(ident), changes);
@@ -107,6 +116,11 @@ public class TopicNormalizeDispatcher implements
TopicDispatcher {
return applyCaseSensitive(topicIdents, Capability.Scope.TOPIC,
capabilities);
}
+ private static void checkCommentLength(String comment) {
+ EntityFieldLimits.checkMaxLength(
+ comment, EntityFieldLimits.MAX_COMMENT_LENGTH, "comment",
Entity.EntityType.TOPIC);
+ }
+
private NameIdentifier normalizeNameIdentifier(NameIdentifier topicIdent) {
Capability capability = getCapability(topicIdent, catalogManager);
return applyCapabilities(topicIdent, Capability.Scope.TOPIC, capability);
diff --git a/core/src/main/java/org/apache/gravitino/meta/BaseMetalake.java
b/core/src/main/java/org/apache/gravitino/meta/BaseMetalake.java
index 4e2ca045f2..8ee4aaed53 100644
--- a/core/src/main/java/org/apache/gravitino/meta/BaseMetalake.java
+++ b/core/src/main/java/org/apache/gravitino/meta/BaseMetalake.java
@@ -27,6 +27,7 @@ import lombok.Getter;
import lombok.ToString;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Metalake;
@@ -42,7 +43,8 @@ public class BaseMetalake implements Metalake, Entity,
Auditable, HasIdentifier
Field.required("id", Long.class, "The metalake's unique identifier");
public static final Field NAME = Field.required("name", String.class, "The
metalake's name");
public static final Field COMMENT =
- Field.optional("comment", String.class, "The metalake's comment or
description");
+ Field.optional(
+ "comment", "The metalake's comment or description",
EntityFieldLimits.MAX_COMMENT_LENGTH);
public static final Field PROPERTIES =
Field.optional("properties", Map.class, "The properties associated with
the metalake");
public static final Field AUDIT_INFO =
diff --git a/core/src/main/java/org/apache/gravitino/meta/CatalogEntity.java
b/core/src/main/java/org/apache/gravitino/meta/CatalogEntity.java
index d741370a3a..99c99e54bb 100644
--- a/core/src/main/java/org/apache/gravitino/meta/CatalogEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/CatalogEntity.java
@@ -30,6 +30,7 @@ import org.apache.gravitino.Audit;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -47,7 +48,10 @@ public class CatalogEntity implements Entity, Auditable,
HasIdentifier {
public static final Field PROVIDER =
Field.required("provider", String.class, "The provider of the catalog");
public static final Field COMMENT =
- Field.optional("comment", String.class, "The comment or description of
the catalog");
+ Field.optional(
+ "comment",
+ "The comment or description of the catalog",
+ EntityFieldLimits.MAX_COMMENT_LENGTH);
public static final Field PROPERTIES =
Field.optional("properties", Map.class, "The properties associated with
the catalog");
public static final Field AUDIT_INFO =
diff --git a/core/src/main/java/org/apache/gravitino/meta/FilesetEntity.java
b/core/src/main/java/org/apache/gravitino/meta/FilesetEntity.java
index c89a64f8c1..db8a466c33 100644
--- a/core/src/main/java/org/apache/gravitino/meta/FilesetEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/FilesetEntity.java
@@ -29,6 +29,7 @@ import java.util.Objects;
import lombok.ToString;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -42,7 +43,10 @@ public class FilesetEntity implements Entity, Auditable,
HasIdentifier {
public static final Field NAME =
Field.required("name", String.class, "The name of the fileset entity.");
public static final Field COMMENT =
- Field.optional("comment", String.class, "The comment or description of
the fileset entity.");
+ Field.optional(
+ "comment",
+ "The comment or description of the fileset entity.",
+ EntityFieldLimits.MAX_COMMENT_LENGTH);
public static final Field TYPE =
Field.required("type", Fileset.Type.class, "The type of the fileset
entity.");
public static final Field STORAGE_LOCATIONS =
diff --git a/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
b/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
index b8cac5ba4c..6344661445 100644
--- a/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/GroupEntity.java
@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Objects;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -37,7 +38,7 @@ public class GroupEntity implements Group, Entity, Auditable,
HasIdentifier {
Field.required("id", Long.class, " The unique id of the group entity.");
public static final Field NAME =
- Field.required("name", String.class, "The name of the group entity.");
+ Field.required("name", "The name of the group entity.",
EntityFieldLimits.MAX_NAME_LENGTH);
public static final Field ROLE_NAMES =
Field.optional("role_names", List.class, "The role names of the group
entity.");
diff --git
a/core/src/main/java/org/apache/gravitino/meta/JobTemplateEntity.java
b/core/src/main/java/org/apache/gravitino/meta/JobTemplateEntity.java
index fe60ea4678..7d0daaa92d 100644
--- a/core/src/main/java/org/apache/gravitino/meta/JobTemplateEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/JobTemplateEntity.java
@@ -32,6 +32,7 @@ import lombok.ToString;
import lombok.experimental.Accessors;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -101,7 +102,8 @@ public class JobTemplateEntity implements Entity,
Auditable, HasIdentifier {
public static final Field ID =
Field.required("id", Long.class, "The unique id of the job template
entity.");
public static final Field NAME =
- Field.required("name", String.class, "The name of the job template
entity.");
+ Field.required(
+ "name", "The name of the job template entity.",
EntityFieldLimits.MAX_NAME_LENGTH);
public static final Field COMMENT =
Field.optional(
"comment", String.class, "The comment or description of the job
template entity.");
diff --git
a/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java
b/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java
index 478b99946d..96d577b449 100644
--- a/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/ModelVersionEntity.java
@@ -28,6 +28,7 @@ import java.util.Objects;
import lombok.ToString;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.NameIdentifier;
@@ -139,6 +140,12 @@ public class ModelVersionEntity implements Entity,
Auditable, HasIdentifier {
Entity.super.validate();
Preconditions.checkArgument(
!uris.isEmpty(), "The uri of the model version entity must not be
empty.");
+ if (aliases != null) {
+ aliases.forEach(
+ alias ->
+ EntityFieldLimits.checkMaxLength(
+ alias, EntityFieldLimits.MAX_NAME_LENGTH, "alias", type()));
+ }
}
@Override
diff --git a/core/src/main/java/org/apache/gravitino/meta/PolicyEntity.java
b/core/src/main/java/org/apache/gravitino/meta/PolicyEntity.java
index 2c122c3ca8..c0916ac8f5 100644
--- a/core/src/main/java/org/apache/gravitino/meta/PolicyEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/PolicyEntity.java
@@ -29,6 +29,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Audit;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -42,7 +43,7 @@ public class PolicyEntity implements Entity, Auditable,
HasIdentifier {
public static final Field ID =
Field.required("id", Long.class, "The unique id of the policy entity.");
public static final Field NAME =
- Field.required("name", String.class, "The name of the policy entity.");
+ Field.required("name", "The name of the policy entity.",
EntityFieldLimits.MAX_NAME_LENGTH);
public static final Field POLICY_TYPE =
Field.required("policyType", Policy.BuiltInType.class, "The type of the
policy entity.");
public static final Field COMMENT =
diff --git a/core/src/main/java/org/apache/gravitino/meta/RoleEntity.java
b/core/src/main/java/org/apache/gravitino/meta/RoleEntity.java
index 6bde0291ec..9ded34692f 100644
--- a/core/src/main/java/org/apache/gravitino/meta/RoleEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/RoleEntity.java
@@ -26,6 +26,7 @@ import java.util.Map;
import java.util.Objects;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -39,7 +40,7 @@ public class RoleEntity implements Role, Entity, Auditable,
HasIdentifier {
Field.required("id", Long.class, " The unique id of the role entity.");
public static final Field NAME =
- Field.required("name", String.class, "The name of the role entity.");
+ Field.required("name", "The name of the role entity.",
EntityFieldLimits.MAX_NAME_LENGTH);
public static final Field PROPERTIES =
Field.optional("properties", Map.class, "The properties of the role
entity.");
diff --git a/core/src/main/java/org/apache/gravitino/meta/SchemaEntity.java
b/core/src/main/java/org/apache/gravitino/meta/SchemaEntity.java
index 7848e20d39..4e1f2653af 100644
--- a/core/src/main/java/org/apache/gravitino/meta/SchemaEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/SchemaEntity.java
@@ -25,6 +25,7 @@ import java.util.Map;
import lombok.ToString;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -38,7 +39,10 @@ public class SchemaEntity implements Entity, Auditable,
HasIdentifier {
public static final Field AUDIT_INFO =
Field.required("audit_info", AuditInfo.class, "The audit details of the
schema");
public static final Field COMMENT =
- Field.optional("comment", String.class, "The comment or description of
the schema");
+ Field.optional(
+ "comment",
+ "The comment or description of the schema",
+ EntityFieldLimits.MAX_COMMENT_LENGTH);
public static final Field PROPERTIES =
Field.optional("properties", Map.class, "The properties of the schema");
diff --git a/core/src/main/java/org/apache/gravitino/meta/TagEntity.java
b/core/src/main/java/org/apache/gravitino/meta/TagEntity.java
index 1eabad889a..49eb5aff67 100644
--- a/core/src/main/java/org/apache/gravitino/meta/TagEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/TagEntity.java
@@ -27,6 +27,7 @@ import java.util.Optional;
import org.apache.gravitino.Audit;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -38,10 +39,11 @@ public class TagEntity implements Tag, Entity, Auditable,
HasIdentifier {
Field.required("id", Long.class, "The unique id of the tag entity.");
public static final Field NAME =
- Field.required("name", String.class, "The name of the tag entity.");
+ Field.required("name", "The name of the tag entity.",
EntityFieldLimits.MAX_NAME_LENGTH);
public static final Field COMMENT =
- Field.optional("comment", String.class, "The comment of the tag
entity.");
+ Field.optional(
+ "comment", "The comment of the tag entity.",
EntityFieldLimits.MAX_COMMENT_LENGTH);
public static final Field PROPERTIES =
Field.optional("properties", Map.class, "The properties of the tag
entity.");
diff --git a/core/src/main/java/org/apache/gravitino/meta/TopicEntity.java
b/core/src/main/java/org/apache/gravitino/meta/TopicEntity.java
index a4479eeef2..03a3d58b09 100644
--- a/core/src/main/java/org/apache/gravitino/meta/TopicEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/TopicEntity.java
@@ -25,6 +25,7 @@ import java.util.Objects;
import lombok.ToString;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -37,7 +38,10 @@ public class TopicEntity implements Entity, Auditable,
HasIdentifier {
public static final Field NAME =
Field.required("name", String.class, "The name of the topic entity.");
public static final Field COMMENT =
- Field.optional("comment", String.class, "The comment or description of
the topic entity.");
+ Field.optional(
+ "comment",
+ "The comment or description of the topic entity.",
+ EntityFieldLimits.MAX_COMMENT_LENGTH);
public static final Field AUDIT_INFO =
Field.required("audit_info", AuditInfo.class, "The audit details of the
topic entity.");
public static final Field PROPERTIES =
diff --git a/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
b/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
index c9d7089f85..da4094aa13 100644
--- a/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
@@ -26,6 +26,7 @@ import java.util.Objects;
import lombok.ToString;
import org.apache.gravitino.Auditable;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.Field;
import org.apache.gravitino.HasIdentifier;
import org.apache.gravitino.Namespace;
@@ -40,7 +41,7 @@ public class UserEntity implements User, Entity, Auditable,
HasIdentifier {
Field.required("id", Long.class, " The unique id of the user entity.");
public static final Field NAME =
- Field.required("name", String.class, "The name of the user entity.");
+ Field.required("name", "The name of the user entity.",
EntityFieldLimits.MAX_NAME_LENGTH);
public static final Field AUDIT_INFO =
Field.required("audit_info", AuditInfo.class, "The audit details of the
user entity.");
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/converters/H2ExceptionConverter.java
b/core/src/main/java/org/apache/gravitino/storage/relational/converters/H2ExceptionConverter.java
index ff297d8d27..1c83d34a2d 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/converters/H2ExceptionConverter.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/converters/H2ExceptionConverter.java
@@ -31,6 +31,9 @@ public class H2ExceptionConverter implements
SQLExceptionConverter {
/** It means found a duplicated primary key or unique key entry in H2. */
private static final int DUPLICATED_ENTRY_ERROR_CODE = 23505;
+ /** It means a value is too long for its column in H2. */
+ private static final int VALUE_TOO_LONG_ERROR_CODE = 22001;
+
@SuppressWarnings("FormatStringAnnotation")
@Override
public void toGravitinoException(SQLException se, Entity.EntityType type,
String name)
@@ -41,6 +44,8 @@ public class H2ExceptionConverter implements
SQLExceptionConverter {
case MySQLExceptionConverter.DUPLICATED_ENTRY_ERROR_CODE:
throw new EntityAlreadyExistsException(
se, "The %s entity: %s already exists.", type.name(), name);
+ case VALUE_TOO_LONG_ERROR_CODE:
+ throw ValueTooLongExceptions.of(se, type, name);
default:
throw new IOException("error code: " + se.getErrorCode(), se);
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/converters/MySQLExceptionConverter.java
b/core/src/main/java/org/apache/gravitino/storage/relational/converters/MySQLExceptionConverter.java
index 6b5035f23a..b7c8208eca 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/converters/MySQLExceptionConverter.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/converters/MySQLExceptionConverter.java
@@ -32,6 +32,9 @@ public class MySQLExceptionConverter implements
SQLExceptionConverter {
/** It means found a duplicated primary key or unique key entry in MySQL. */
static final int DUPLICATED_ENTRY_ERROR_CODE = 1062;
+ /** It means a value is too long for its column in MySQL. */
+ private static final int DATA_TOO_LONG_ERROR_CODE = 1406;
+
@SuppressWarnings("FormatStringAnnotation")
@Override
public void toGravitinoException(SQLException se, Entity.EntityType type,
String name)
@@ -40,6 +43,8 @@ public class MySQLExceptionConverter implements
SQLExceptionConverter {
case DUPLICATED_ENTRY_ERROR_CODE:
throw new EntityAlreadyExistsException(
se, "The %s entity: %s already exists.", type.name(), name);
+ case DATA_TOO_LONG_ERROR_CODE:
+ throw ValueTooLongExceptions.of(se, type, name);
default:
throw new IOException(se);
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/converters/PostgreSQLExceptionConverter.java
b/core/src/main/java/org/apache/gravitino/storage/relational/converters/PostgreSQLExceptionConverter.java
index 318e3865d9..730b180bec 100644
---
a/core/src/main/java/org/apache/gravitino/storage/relational/converters/PostgreSQLExceptionConverter.java
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/converters/PostgreSQLExceptionConverter.java
@@ -31,6 +31,9 @@ import org.apache.gravitino.EntityAlreadyExistsException;
public class PostgreSQLExceptionConverter implements SQLExceptionConverter {
private static final String DUPLICATED_ENTRY_ERROR_CODE = "23505";
+ /** It means a value is too long for its column in PostgreSQL. */
+ private static final String STRING_DATA_RIGHT_TRUNCATION_ERROR_CODE =
"22001";
+
@Override
@SuppressWarnings("FormatStringAnnotation")
public void toGravitinoException(SQLException sqlException,
Entity.EntityType type, String name)
@@ -40,6 +43,8 @@ public class PostgreSQLExceptionConverter implements
SQLExceptionConverter {
case DUPLICATED_ENTRY_ERROR_CODE:
throw new EntityAlreadyExistsException(
sqlException, "The %s entity: %s already exists.", type.name(),
name);
+ case STRING_DATA_RIGHT_TRUNCATION_ERROR_CODE:
+ throw ValueTooLongExceptions.of(sqlException, type, name);
default:
throw new IOException(sqlException);
}
diff --git
a/core/src/main/java/org/apache/gravitino/storage/relational/converters/ValueTooLongExceptions.java
b/core/src/main/java/org/apache/gravitino/storage/relational/converters/ValueTooLongExceptions.java
new file mode 100644
index 0000000000..65901c4843
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/storage/relational/converters/ValueTooLongExceptions.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.storage.relational.converters;
+
+import java.sql.SQLException;
+import java.util.Locale;
+import org.apache.gravitino.Entity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Builds the exception for a SQL error reporting a value that is too long
for its column. */
+final class ValueTooLongExceptions {
+ private static final Logger LOG =
LoggerFactory.getLogger(ValueTooLongExceptions.class);
+
+ private ValueTooLongExceptions() {}
+
+ /**
+ * Creates the exception for a SQL error reporting a value that is too long
for its column.
+ *
+ * <p>The SQL exception is logged but not attached as the cause, since the
error response
+ * serializes the whole stack trace and would expose the database error
message to the client.
+ *
+ * @param sqlException The SQL exception reporting the value that is too
long.
+ * @param type The type of the entity being persisted.
+ * @param name The name of the entity being persisted.
+ * @return The {@link IllegalArgumentException} to throw.
+ */
+ static IllegalArgumentException of(
+ SQLException sqlException, Entity.EntityType type, String name) {
+ LOG.warn("Failed to persist the {} entity: {}", type, name, sqlException);
+ return new IllegalArgumentException(
+ String.format(
+ "The %s entity has a value that exceeds the maximum length of its
column.",
+ type.name().toLowerCase(Locale.ROOT)));
+ }
+}
diff --git a/core/src/test/java/org/apache/gravitino/TestField.java
b/core/src/test/java/org/apache/gravitino/TestField.java
index 28d9b7e79a..ad0d0a5823 100644
--- a/core/src/test/java/org/apache/gravitino/TestField.java
+++ b/core/src/test/java/org/apache/gravitino/TestField.java
@@ -53,6 +53,32 @@ class TestField {
assertEquals("Field name is not of type java.lang.String",
ex.getMessage());
}
+ @Test
+ void testFieldMaxLength() {
+ Field field = Field.required("name", "Name of the person", 3);
+
+ assertDoesNotThrow(() -> field.validate("abc"));
+
+ IllegalArgumentException ex =
+ assertThrows(IllegalArgumentException.class, () ->
field.validate("abcd"));
+ assertEquals("Field name must not exceed 3 characters", ex.getMessage());
+
+ ex =
+ assertThrows(
+ IllegalArgumentException.class, () -> field.validate("abcd",
Entity.EntityType.TAG));
+ assertEquals("The name of the tag must not exceed 3 characters",
ex.getMessage());
+
+ Field optional = Field.optional("comment", "Comment of the person", 3);
+ assertDoesNotThrow(() -> optional.validate(null));
+ }
+
+ @Test
+ void testBuilderInvalidMaxLength() {
+ IllegalArgumentException ex =
+ assertThrows(IllegalArgumentException.class, () ->
Field.required("name", "Name", 0));
+ assertEquals("Field max length must be positive", ex.getMessage());
+ }
+
@Test
void testBuilderMissingName() {
IllegalArgumentException ex =
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestModelNormalizeDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestModelNormalizeDispatcher.java
new file mode 100644
index 0000000000..dd6c30b9b2
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestModelNormalizeDispatcher.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.catalog;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.model.Model;
+import org.apache.gravitino.model.ModelChange;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class TestModelNormalizeDispatcher extends TestOperationDispatcher {
+ private static ModelNormalizeDispatcher modelNormalizeDispatcher;
+ private static SchemaNormalizeDispatcher schemaNormalizeDispatcher;
+
+ @BeforeAll
+ public static void initialize() throws IOException, IllegalAccessException {
+ TestModelOperationDispatcher.initialize();
+ schemaNormalizeDispatcher =
+ new SchemaNormalizeDispatcher(
+ TestModelOperationDispatcher.schemaOperationDispatcher,
catalogManager);
+ modelNormalizeDispatcher =
+ new ModelNormalizeDispatcher(
+ TestModelOperationDispatcher.modelOperationDispatcher,
catalogManager);
+ }
+
+ @Test
+ public void testRenameNameSpec() {
+ String schemaName = "test_rename_name_spec";
+ schemaNormalizeDispatcher.createSchema(
+ NameIdentifier.of(metalake, catalog, schemaName), "comment",
ImmutableMap.of("k1", "v1"));
+ NameIdentifier modelIdent = NameIdentifierUtil.ofModel(metalake, catalog,
schemaName, "model");
+ modelNormalizeDispatcher.registerModel(modelIdent, "comment",
ImmutableMap.of("k1", "v1"));
+
+ String tooLongName = StringUtils.repeat("m", 129);
+ Exception exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> modelNormalizeDispatcher.alterModel(modelIdent,
ModelChange.rename(tooLongName)));
+ Assertions.assertEquals(
+ String.format("The MODEL name '%s' is illegal. Illegal name: %s",
tooLongName, tooLongName),
+ exception.getMessage());
+
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ modelNormalizeDispatcher.alterModel(
+ modelIdent,
ModelChange.rename(MetadataObjects.METADATA_OBJECT_RESERVED_NAME)));
+ Assertions.assertEquals(
+ "The MODEL name '*' is reserved. Illegal name: *",
exception.getMessage());
+
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> modelNormalizeDispatcher.alterModel(modelIdent,
ModelChange.rename("a?")));
+ Assertions.assertEquals(
+ "The MODEL name 'a?' is illegal. Illegal name: a?",
exception.getMessage());
+
+ Model renamed = modelNormalizeDispatcher.alterModel(modelIdent,
ModelChange.rename("model_1"));
+ Assertions.assertEquals("model_1", renamed.name());
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestTopicNormalizeDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestTopicNormalizeDispatcher.java
index ef25892410..6b6d8e9608 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestTopicNormalizeDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestTopicNormalizeDispatcher.java
@@ -21,6 +21,8 @@ package org.apache.gravitino.catalog;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.MetadataObjects;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
@@ -101,4 +103,36 @@ public class TestTopicNormalizeDispatcher extends
TestOperationDispatcher {
Assertions.assertEquals(
"The TOPIC name 'a?' is illegal. Illegal name: a?",
exception.getMessage());
}
+
+ @Test
+ public void testCommentLength() {
+ Namespace topicNs = Namespace.of(metalake, catalog, "testCommentLength");
+ Map<String, String> props = ImmutableMap.of("k1", "v1");
+
schemaNormalizeDispatcher.createSchema(NameIdentifier.of(topicNs.levels()),
"comment", props);
+
+ NameIdentifier topicIdent = NameIdentifier.of(topicNs, "topic");
+ String tooLongComment = StringUtils.repeat("a",
EntityFieldLimits.MAX_COMMENT_LENGTH + 1);
+ Exception exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> topicNormalizeDispatcher.createTopic(topicIdent,
tooLongComment, null, props));
+ Assertions.assertEquals(
+ "The comment of the topic must not exceed 256 characters",
exception.getMessage());
+ Assertions.assertFalse(topicNormalizeDispatcher.topicExists(topicIdent));
+
+ String maxLengthComment = StringUtils.repeat("a",
EntityFieldLimits.MAX_COMMENT_LENGTH);
+ Topic topic = topicNormalizeDispatcher.createTopic(topicIdent,
maxLengthComment, null, props);
+ Assertions.assertEquals(maxLengthComment, topic.comment());
+
+ exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ topicNormalizeDispatcher.alterTopic(
+ topicIdent, TopicChange.updateComment(tooLongComment)));
+ Assertions.assertEquals(
+ "The comment of the topic must not exceed 256 characters",
exception.getMessage());
+ Assertions.assertEquals(
+ maxLengthComment,
topicNormalizeDispatcher.loadTopic(topicIdent).comment());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/meta/TestEntityFieldLimits.java
b/core/src/test/java/org/apache/gravitino/meta/TestEntityFieldLimits.java
new file mode 100644
index 0000000000..d2a77ae387
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/meta/TestEntityFieldLimits.java
@@ -0,0 +1,258 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.meta;
+
+import static org.apache.gravitino.file.Fileset.LOCATION_NAME_UNKNOWN;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.authorization.SecurableObjects;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.job.ShellJobTemplate;
+import org.apache.gravitino.model.ModelVersion;
+import org.apache.gravitino.policy.Policy;
+import org.apache.gravitino.policy.PolicyContents;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class TestEntityFieldLimits {
+
+ private static final Namespace NAMESPACE = Namespace.of("metalake");
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("nameBuilders")
+ public void testNameLength(String entityType, Function<String, Entity>
builder) {
+ assertLengthLimit(builder, "name", entityType,
EntityFieldLimits.MAX_NAME_LENGTH);
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("commentBuilders")
+ public void testCommentLength(String entityType, Function<String, Entity>
builder) {
+ assertLengthLimit(builder, "comment", entityType,
EntityFieldLimits.MAX_COMMENT_LENGTH);
+ }
+
+ @Test
+ public void testModelVersionAliasLength() {
+ Function<String, Entity> builder =
+ alias ->
+ ModelVersionEntity.builder()
+ .withModelIdentifier(NameIdentifier.of("m1", "c1", "s1",
"model1"))
+ .withVersion(1)
+ .withAliases(Lists.newArrayList("alias", alias))
+ .withUris(ImmutableMap.of(ModelVersion.URI_NAME_UNKNOWN,
"test_uri"))
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build();
+ assertLengthLimit(builder, "alias", "model version",
EntityFieldLimits.MAX_NAME_LENGTH);
+ }
+
+ @Test
+ public void testLengthCountsCodePoints() {
+ // An emoji is two UTF-16 chars but one character for MySQL (utf8mb4) and
PostgreSQL, where a
+ // name of 128 emojis fits the column and must still pass validation when
it is read back. H2
+ // counts UTF-16 chars, so it rejects such a name in the database instead,
which the SQL
+ // exception converter reports as an IllegalArgumentException as well.
+ String emoji = new String(Character.toChars(0x1F600));
+ String maxLengthName = StringUtils.repeat(emoji,
EntityFieldLimits.MAX_NAME_LENGTH);
+ Assertions.assertDoesNotThrow(() -> tagBuilder(maxLengthName, null));
+
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () -> tagBuilder(maxLengthName +
emoji, null));
+ Assertions.assertEquals(
+ "The name of the tag must not exceed 128 characters",
exception.getMessage());
+ }
+
+ @Test
+ public void testCheckMaxLength() {
+ Assertions.assertDoesNotThrow(() -> EntityFieldLimits.checkMaxLength(null,
1, "name", null));
+ Assertions.assertDoesNotThrow(() -> EntityFieldLimits.checkMaxLength("a",
1, "name", null));
+
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> EntityFieldLimits.checkMaxLength("ab", 1, "name", null));
+ Assertions.assertEquals("Field name must not exceed 1 characters",
exception.getMessage());
+ }
+
+ private static void assertLengthLimit(
+ Function<String, Entity> builder, String fieldName, String entityType,
int maxLength) {
+ Assertions.assertDoesNotThrow(() -> builder.apply(StringUtils.repeat("a",
maxLength)));
+
+ String tooLong = StringUtils.repeat("a", maxLength + 1);
+ IllegalArgumentException exception =
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
builder.apply(tooLong));
+ Assertions.assertEquals(
+ String.format(
+ "The %s of the %s must not exceed %d characters", fieldName,
entityType, maxLength),
+ exception.getMessage());
+ }
+
+ private static Stream<Arguments> nameBuilders() {
+ return Stream.of(
+ Arguments.of("tag", (Function<String, Entity>) name ->
tagBuilder(name, null)),
+ Arguments.of(
+ "policy",
+ (Function<String, Entity>)
+ name ->
+ PolicyEntity.builder()
+ .withId(1L)
+ .withName(name)
+ .withNamespace(NAMESPACE)
+ .withPolicyType(Policy.BuiltInType.CUSTOM)
+ .withEnabled(false)
+ .withContent(
+ PolicyContents.custom(
+ ImmutableMap.of("k", "v"),
+ ImmutableSet.of(MetadataObject.Type.TABLE),
+ ImmutableMap.of()))
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build()),
+ Arguments.of(
+ "role",
+ (Function<String, Entity>)
+ name ->
+ RoleEntity.builder()
+ .withId(1L)
+ .withName(name)
+ .withAuditInfo(AuditInfo.EMPTY)
+ .withSecurableObjects(
+ Lists.newArrayList(
+ SecurableObjects.ofCatalog(
+ "catalog",
Lists.newArrayList(Privileges.UseCatalog.allow()))))
+ .build()),
+ Arguments.of(
+ "user",
+ (Function<String, Entity>)
+ name ->
+ UserEntity.builder()
+ .withId(1L)
+ .withName(name)
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build()),
+ Arguments.of(
+ "group",
+ (Function<String, Entity>)
+ name ->
+ GroupEntity.builder()
+ .withId(1L)
+ .withName(name)
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build()),
+ Arguments.of(
+ "job template",
+ (Function<String, Entity>)
+ name ->
+ JobTemplateEntity.builder()
+ .withId(1L)
+ .withName(name)
+ .withNamespace(NAMESPACE)
+ .withTemplateContent(
+ JobTemplateEntity.TemplateContent.fromJobTemplate(
+ ShellJobTemplate.builder()
+ .withName("template")
+ .withExecutable("/bin/echo")
+ .build()))
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build()));
+ }
+
+ private static Stream<Arguments> commentBuilders() {
+ return Stream.of(
+ Arguments.of("tag", (Function<String, Entity>) comment ->
tagBuilder("tag", comment)),
+ Arguments.of(
+ "metalake",
+ (Function<String, Entity>)
+ comment ->
+ BaseMetalake.builder()
+ .withId(1L)
+ .withName("metalake")
+ .withComment(comment)
+ .withAuditInfo(AuditInfo.EMPTY)
+ .withVersion(SchemaVersion.V_0_1)
+ .build()),
+ Arguments.of(
+ "catalog",
+ (Function<String, Entity>)
+ comment ->
+ CatalogEntity.builder()
+ .withId(1L)
+ .withName("catalog")
+ .withComment(comment)
+ .withType(Catalog.Type.RELATIONAL)
+ .withProvider("test")
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build()),
+ Arguments.of(
+ "schema",
+ (Function<String, Entity>)
+ comment ->
+ SchemaEntity.builder()
+ .withId(1L)
+ .withName("schema")
+ .withComment(comment)
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build()),
+ Arguments.of(
+ "fileset",
+ (Function<String, Entity>)
+ comment ->
+ FilesetEntity.builder()
+ .withId(1L)
+ .withName("fileset")
+ .withComment(comment)
+ .withFilesetType(Fileset.Type.MANAGED)
+
.withStorageLocations(ImmutableMap.of(LOCATION_NAME_UNKNOWN, "location"))
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build()),
+ Arguments.of(
+ "topic",
+ (Function<String, Entity>)
+ comment ->
+ TopicEntity.builder()
+ .withId(1L)
+ .withName("topic")
+ .withComment(comment)
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build()));
+ }
+
+ private static TagEntity tagBuilder(String name, String comment) {
+ return TagEntity.builder()
+ .withId(1L)
+ .withName(name)
+ .withNamespace(NAMESPACE)
+ .withComment(comment)
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build();
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestH2ExceptionConverter.java
b/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestH2ExceptionConverter.java
index f07abd3b59..883a1c2f48 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestH2ExceptionConverter.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestH2ExceptionConverter.java
@@ -36,4 +36,19 @@ public class TestH2ExceptionConverter {
() -> converter.toGravitinoException(mockException,
Entity.EntityType.METALAKE, "test"),
String.format("The %s entity: %s already exists.",
Entity.EntityType.METALAKE, "test"));
}
+
+ @Test
+ public void testConvertValueTooLongException() {
+ SQLException sqlException = new SQLException("Value too long for column",
"22001", 22001);
+ H2ExceptionConverter converter = new H2ExceptionConverter();
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> converter.toGravitinoException(sqlException,
Entity.EntityType.TAG, "test"));
+ Assertions.assertEquals(
+ "The tag entity has a value that exceeds the maximum length of its
column.",
+ exception.getMessage());
+ // The database error must not be exposed to the client through the cause.
+ Assertions.assertNull(exception.getCause());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestMySQLExceptionConverter.java
b/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestMySQLExceptionConverter.java
index 9a94db6e10..d69300f5c8 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestMySQLExceptionConverter.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestMySQLExceptionConverter.java
@@ -37,4 +37,20 @@ public class TestMySQLExceptionConverter {
() -> converter.toGravitinoException(mockException,
Entity.EntityType.METALAKE, "test"),
String.format("The %s entity: %s already exists.",
Entity.EntityType.METALAKE, "test"));
}
+
+ @Test
+ public void testConvertValueTooLongException() {
+ SQLException sqlException =
+ new SQLException("Data too long for column 'tag_name'", "22001", 1406);
+ MySQLExceptionConverter converter = new MySQLExceptionConverter();
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> converter.toGravitinoException(sqlException,
Entity.EntityType.TAG, "test"));
+ Assertions.assertEquals(
+ "The tag entity has a value that exceeds the maximum length of its
column.",
+ exception.getMessage());
+ // The database error must not be exposed to the client through the cause.
+ Assertions.assertNull(exception.getCause());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestPostgreSQLExceptionConverter.java
b/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestPostgreSQLExceptionConverter.java
index 9c6d9c4ea2..d31eda0fd3 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestPostgreSQLExceptionConverter.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/converters/TestPostgreSQLExceptionConverter.java
@@ -45,4 +45,20 @@ public class TestPostgreSQLExceptionConverter {
IOException.class,
() -> converter.toGravitinoException(sqlException,
Entity.EntityType.METALAKE, "test"));
}
+
+ @Test
+ public void testConvertValueTooLongException() {
+ SQLException sqlException =
+ new SQLException("value too long for type character varying(128)",
"22001");
+ PostgreSQLExceptionConverter converter = new
PostgreSQLExceptionConverter();
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> converter.toGravitinoException(sqlException,
Entity.EntityType.TAG, "test"));
+ Assertions.assertEquals(
+ "The tag entity has a value that exceeds the maximum length of its
column.",
+ exception.getMessage());
+ // The database error must not be exposed to the client through the cause.
+ Assertions.assertNull(exception.getCause());
+ }
}
diff --git a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
index 775759f7d5..57182308b2 100644
--- a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
+++ b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
@@ -51,11 +51,13 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityFieldLimits;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.EntityStoreFactory;
import org.apache.gravitino.GravitinoEnv;
@@ -435,6 +437,49 @@ public class TestTagManager {
Assertions.assertEquals(expectedProp2, removedPropTag.properties());
}
+ @Test
+ public void testTagNameAndCommentLength() {
+ String maxLengthName = StringUtils.repeat("a",
EntityFieldLimits.MAX_NAME_LENGTH);
+ String tooLongName = maxLengthName + "a";
+ String maxLengthComment = StringUtils.repeat("c",
EntityFieldLimits.MAX_COMMENT_LENGTH);
+ String tooLongComment = maxLengthComment + "c";
+
+ Exception e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> tagManager.createTag(METALAKE, tooLongName, null, null));
+ Assertions.assertEquals("The name of the tag must not exceed 128
characters", e.getMessage());
+
+ e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> tagManager.createTag(METALAKE, "tag1", tooLongComment,
null));
+ Assertions.assertEquals(
+ "The comment of the tag must not exceed 256 characters",
e.getMessage());
+
+ Tag tag = tagManager.createTag(METALAKE, maxLengthName, maxLengthComment,
null);
+ Assertions.assertEquals(maxLengthName, tag.name());
+ Assertions.assertEquals(maxLengthComment, tag.comment());
+
+ e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> tagManager.alterTag(METALAKE, maxLengthName,
TagChange.rename(tooLongName)));
+ Assertions.assertEquals("The name of the tag must not exceed 128
characters", e.getMessage());
+
+ e =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ tagManager.alterTag(
+ METALAKE, maxLengthName,
TagChange.updateComment(tooLongComment)));
+ Assertions.assertEquals(
+ "The comment of the tag must not exceed 256 characters",
e.getMessage());
+
+ Tag unchanged = tagManager.getTag(METALAKE, maxLengthName);
+ Assertions.assertEquals(maxLengthComment, unchanged.comment());
+ }
+
@Test
public void testAlterTagRenameToExistingTag() {
tagManager.createTag(METALAKE, "tag1", null, null);